From 55a86b8a00aa8bd57881a94bc98d71a97b7ff08d Mon Sep 17 00:00:00 2001 From: Kasem SAEED Date: Tue, 5 Oct 2021 10:35:07 -0700 Subject: [PATCH 01/41] Add support for RF64 wave files --- .../android/exoplayer2/audio/WavUtil.java | 4 ++++ .../extractor/wav/WavHeaderReader.java | 19 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 208989124a..ff8f5175bf 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -22,6 +22,10 @@ import com.google.android.exoplayer2.util.Util; /** Utilities for handling WAVE files. */ public final class WavUtil { + /** Four character code for "RF64". */ + public static final int RF64_FOURCC = 0x52463634; + /** Four character code for "ds64". */ + public static final int DS64_FOURCC = 0x64733634; /** Four character code for "RIFF". */ public static final int RIFF_FOURCC = 0x52494646; /** Four character code for "WAVE". */ diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index f794933d16..b5813ae2c2 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -50,7 +50,7 @@ import java.io.IOException; // Attempt to read the RIFF chunk. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - if (chunkHeader.id != WavUtil.RIFF_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.RF64_FOURCC) { return null; } @@ -117,14 +117,23 @@ import java.io.IOException; ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we find the data header. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); + long dataSize = -1; while (chunkHeader.id != WavUtil.DATA_FOURCC) { if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) { Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); } long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; // Override size of RIFF chunk, since it describes its size as the entire file. - if (chunkHeader.id == WavUtil.RIFF_FOURCC) { + // Also, ignore the size of RF64 chunk, since its always going to be 0xFFFFFFFF + if (chunkHeader.id == WavUtil.RIFF_FOURCC || chunkHeader.id == WavUtil.RF64_FOURCC) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; + } else if (chunkHeader.id == WavUtil.DS64_FOURCC) { + int ds64Size = (int) chunkHeader.size; + ParsableByteArray ds64Bytes = new ParsableByteArray(ds64Size); + input.peekFully(ds64Bytes.getData(), 0, ds64Size); + // ds64 chunk contains 64bit sizes. From position 12 to 20 is the data size + ds64Bytes.setPosition(12); + dataSize = ds64Bytes.readLong(); } if (bytesToSkip > Integer.MAX_VALUE) { throw ParserException.createForUnsupportedContainerFeature( @@ -133,11 +142,15 @@ import java.io.IOException; input.skipFully((int) bytesToSkip); chunkHeader = ChunkHeader.peek(input, scratch); } + // Use size from data chunk if it wasn't determined from ds64 chunk + if (dataSize == -1) { + dataSize = chunkHeader.size; + } // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); long dataStartPosition = input.getPosition(); - long dataEndPosition = dataStartPosition + chunkHeader.size; + long dataEndPosition = dataStartPosition + dataSize; long inputLength = input.getLength(); if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); From 8501e997e036c99e128781e6d1658d0326dcd1a1 Mon Sep 17 00:00:00 2001 From: Kasem Date: Thu, 14 Oct 2021 19:21:30 -0700 Subject: [PATCH 02/41] Fix wrong RF64 data size and add unit test --- .../extractor/wav/WavHeaderReader.java | 11 +++++++---- .../extractor/wav/WavExtractorTest.java | 6 ++++++ .../src/test/assets/media/wav/sample_rf64.wav | Bin 0 -> 67016 bytes 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 testdata/src/test/assets/media/wav/sample_rf64.wav diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index b5813ae2c2..a004b808d3 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -48,7 +48,7 @@ import java.io.IOException; // Allocate a scratch buffer large enough to store the format chunk. ParsableByteArray scratch = new ParsableByteArray(16); - // Attempt to read the RIFF chunk. + // Attempt to read the RIFF or RF64 chunk. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.RF64_FOURCC) { return null; @@ -117,7 +117,10 @@ import java.io.IOException; ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we find the data header. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); + + // Data size holder. To be determined from data chunk or ds64 chunk in case of RF64. long dataSize = -1; + while (chunkHeader.id != WavUtil.DATA_FOURCC) { if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) { Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); @@ -131,9 +134,9 @@ import java.io.IOException; int ds64Size = (int) chunkHeader.size; ParsableByteArray ds64Bytes = new ParsableByteArray(ds64Size); input.peekFully(ds64Bytes.getData(), 0, ds64Size); - // ds64 chunk contains 64bit sizes. From position 12 to 20 is the data size - ds64Bytes.setPosition(12); - dataSize = ds64Bytes.readLong(); + // ds64 chunk contains 64bit sizes. From position 8 to 16 is the data size + ds64Bytes.setPosition(8); + dataSize = ds64Bytes.readLittleEndianLong(); } if (bytesToSkip > Integer.MAX_VALUE) { throw ParserException.createForUnsupportedContainerFeature( diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index 4217a1528a..211380463d 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -53,4 +53,10 @@ public final class WavExtractorTest { ExtractorAsserts.assertBehavior( WavExtractor::new, "media/wav/sample_ima_adpcm.wav", simulationConfig); } + + @Test + public void sample_RF64() throws Exception { + ExtractorAsserts + .assertBehavior(WavExtractor::new, "media/wav/sample_rf64.wav", simulationConfig); + } } diff --git a/testdata/src/test/assets/media/wav/sample_rf64.wav b/testdata/src/test/assets/media/wav/sample_rf64.wav new file mode 100644 index 0000000000000000000000000000000000000000..b2dd53c687267d970199d63cba3aa6df98b7e9f0 GIT binary patch literal 67016 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

$*;Exa$j;#{{pW=2 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

N!H@pSCpG2buDxf5k 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 literal 0 HcmV?d00001 From 69b2dd0fbf10930a0d567b892e5e53a07189e46b Mon Sep 17 00:00:00 2001 From: Kasem SAEED Date: Fri, 15 Oct 2021 08:48:41 -0700 Subject: [PATCH 03/41] Change condition to not log warning for RF64 files --- .../android/exoplayer2/extractor/wav/WavHeaderReader.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index a004b808d3..27bd28ad5e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -122,7 +122,8 @@ import java.io.IOException; long dataSize = -1; while (chunkHeader.id != WavUtil.DATA_FOURCC) { - if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC + && chunkHeader.id != WavUtil. RF64_FOURCC && chunkHeader.id != WavUtil. DS64_FOURCC) { Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); } long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; From 5b05d640c6b6ebaa9a8b85d64f94e4a5eba4612c Mon Sep 17 00:00:00 2001 From: Kasem SAEED Date: Fri, 15 Oct 2021 09:00:07 -0700 Subject: [PATCH 04/41] Add RF64 dump files for testing --- .../extractordumps/wav/sample_rf64.wav.0.dump | 36 +++++++++++++++++++ .../extractordumps/wav/sample_rf64.wav.1.dump | 32 +++++++++++++++++ .../extractordumps/wav/sample_rf64.wav.2.dump | 28 +++++++++++++++ .../extractordumps/wav/sample_rf64.wav.3.dump | 24 +++++++++++++ .../wav/sample_rf64.wav.unknown_length.dump | 36 +++++++++++++++++++ 5 files changed, 156 insertions(+) create mode 100644 testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump create mode 100644 testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump create mode 100644 testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump create mode 100644 testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump create mode 100644 testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump new file mode 100644 index 0000000000..1e129acd70 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump @@ -0,0 +1,36 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 66936 + sample count = 4 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 19200, hash EF6C7C27 + sample 1: + time = 100000 + flags = 1 + data = length 19200, hash 5AB97AFC + sample 2: + time = 200000 + flags = 1 + data = length 19200, hash 37920F33 + sample 3: + time = 300000 + flags = 1 + data = length 9336, hash 135F1C30 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump new file mode 100644 index 0000000000..f4d925bad3 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump @@ -0,0 +1,32 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 44628 + sample count = 3 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 116208 + flags = 1 + data = length 19200, hash E4B962ED + sample 1: + time = 216208 + flags = 1 + data = length 19200, hash 4F13D6CF + sample 2: + time = 316208 + flags = 1 + data = length 6228, hash 3FB5F446 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump new file mode 100644 index 0000000000..2b42e93b5a --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 22316 + sample count = 2 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 232416 + flags = 1 + data = length 19200, hash F82E494B + sample 1: + time = 332416 + flags = 1 + data = length 3116, hash 93C99CFD +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump new file mode 100644 index 0000000000..2a6345d4a8 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump @@ -0,0 +1,24 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 4 + sample count = 1 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 348625 + flags = 1 + data = length 4, hash FFD4C53F +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump new file mode 100644 index 0000000000..1e129acd70 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump @@ -0,0 +1,36 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 66936 + sample count = 4 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 19200, hash EF6C7C27 + sample 1: + time = 100000 + flags = 1 + data = length 19200, hash 5AB97AFC + sample 2: + time = 200000 + flags = 1 + data = length 19200, hash 37920F33 + sample 3: + time = 300000 + flags = 1 + data = length 9336, hash 135F1C30 +tracksEnded = true From 0e88f13e0f83e8a04678664dcf96e4245f5bd392 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Nov 2021 10:15:21 +0000 Subject: [PATCH 05/41] Update security mailing list PiperOrigin-RevId: 407540705 --- SECURITY.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 4ec8ec4a3b..7a1b043f8b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,8 +1,9 @@ # Security policy # -To report a security issue, please email exoplayer-support+security@google.com -with a description of the issue, the steps you took to create the issue, -affected versions, and, if known, mitigations for the issue. Our vulnerability -management team will respond within 3 working days of your email. If the issue -is confirmed as a vulnerability, we will open a Security Advisory. This project -follows a 90 day disclosure timeline. +To report a security issue, please email +android-media-support+security@google.com with a description of the issue, the +steps you took to create the issue, affected versions, and, if known, +mitigations for the issue. Our vulnerability management team will respond within +3 working days of your email. If the issue is confirmed as a vulnerability, we +will open a Security Advisory. This project follows a 90 day disclosure +timeline. From 62a35018f8e4fe0e8e7ea468311eb4b3d9c2a791 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Thu, 4 Nov 2021 12:06:10 +0000 Subject: [PATCH 06/41] Separate TransformerAudioRenderer and new AudioSamplePipeline. `TransformerAudioRenderer` reads input and passes `DecoderInputBuffer`s to the `AudioSamplePipeline`. The `AudioSamplePipeline` handles all steps from decoding to encoding. `TransformerAudioRenderer` receives `DecoderInputBuffer`s from the `AudioSamplePipeline` and passes their data to the muxer. `AudioSamplePipeline` implements a new interface `SamplePipeline`. A pass-through pipeline will be added in a future cl. PiperOrigin-RevId: 407555102 --- .../transformer/AudioSamplePipeline.java | 373 +++++++++++++++++ .../transformer/SamplePipeline.java | 69 ++++ .../transformer/TransformerAudioRenderer.java | 379 +++--------------- .../transformerdumps/mp4/sample.mp4.dump | 48 +-- .../mp4/sample_sef_slow_motion.mp4.dump | 116 +++++- 5 files changed, 612 insertions(+), 373 deletions(-) create mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java create mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SamplePipeline.java diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java new file mode 100644 index 0000000000..a4eb090191 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java @@ -0,0 +1,373 @@ +/* + * Copyright 2021 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.transformer; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; +import static java.lang.Math.min; + +import android.media.MediaCodec.BufferInfo; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.audio.AudioProcessor; +import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; +import com.google.android.exoplayer2.audio.SonicAudioProcessor; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * Pipeline to decode audio samples, apply transformations on the raw samples, and re-encode them. + */ +@RequiresApi(18) +/* package */ final class AudioSamplePipeline implements SamplePipeline { + + private static final String TAG = "AudioSamplePipeline"; + private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024; + + private final MediaCodecAdapterWrapper decoder; + private final Format decoderInputFormat; + private final DecoderInputBuffer decoderInputBuffer; + + private final SonicAudioProcessor sonicAudioProcessor; + private final SpeedProvider speedProvider; + + private final DecoderInputBuffer encoderInputBuffer; + private final DecoderInputBuffer encoderOutputBuffer; + + private final Transformation transformation; + private final int rendererIndex; + + private @MonotonicNonNull AudioFormat encoderInputAudioFormat; + private @MonotonicNonNull MediaCodecAdapterWrapper encoder; + private long nextEncoderInputBufferTimeUs; + + private ByteBuffer sonicOutputBuffer; + private boolean drainingSonicForSpeedChange; + private float currentSpeed; + + public AudioSamplePipeline( + Format decoderInputFormat, Transformation transformation, int rendererIndex) + throws ExoPlaybackException { + this.decoderInputFormat = decoderInputFormat; + this.transformation = transformation; + this.rendererIndex = rendererIndex; + decoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + encoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + encoderOutputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + sonicAudioProcessor = new SonicAudioProcessor(); + sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; + nextEncoderInputBufferTimeUs = 0; + speedProvider = new SegmentSpeedProvider(decoderInputFormat); + currentSpeed = speedProvider.getSpeed(0); + try { + this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(decoderInputFormat); + } catch (IOException e) { + // TODO (internal b/184262323): Assign an adequate error code. + throw ExoPlaybackException.createForRenderer( + e, + TAG, + rendererIndex, + decoderInputFormat, + /* rendererFormatSupport= */ C.FORMAT_HANDLED, + /* isRecoverable= */ false, + PlaybackException.ERROR_CODE_UNSPECIFIED); + } + } + + @Override + public void release() { + sonicAudioProcessor.reset(); + decoder.release(); + if (encoder != null) { + encoder.release(); + } + } + + @Override + public boolean processData() throws ExoPlaybackException { + if (!ensureEncoderAndAudioProcessingConfigured()) { + return false; + } + if (sonicAudioProcessor.isActive()) { + return feedEncoderFromSonic() || feedSonicFromDecoder(); + } else { + return feedEncoderFromDecoder(); + } + } + + @Override + @Nullable + public DecoderInputBuffer dequeueInputBuffer() { + return decoder.maybeDequeueInputBuffer(decoderInputBuffer) ? decoderInputBuffer : null; + } + + @Override + public void queueInputBuffer() { + decoder.queueInputBuffer(decoderInputBuffer); + } + + @Override + @Nullable + public Format getOutputFormat() { + return encoder != null ? encoder.getOutputFormat() : null; + } + + @Override + public boolean isEnded() { + return encoder != null && encoder.isEnded(); + } + + @Override + @Nullable + public DecoderInputBuffer getOutputBuffer() { + if (encoder != null) { + encoderOutputBuffer.data = encoder.getOutputBuffer(); + if (encoderOutputBuffer.data != null) { + encoderOutputBuffer.timeUs = checkNotNull(encoder.getOutputBufferInfo()).presentationTimeUs; + return encoderOutputBuffer; + } + } + return null; + } + + @Override + public void releaseOutputBuffer() { + if (encoder != null) { + encoder.releaseOutputBuffer(); + } + } + + /** + * Attempts to pass decoder output data to the encoder, and returns whether it may be possible to + * pass more data immediately by calling this method again. + */ + @RequiresNonNull({"encoderInputAudioFormat", "encoder"}) + private boolean feedEncoderFromDecoder() { + if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { + return false; + } + + if (decoder.isEnded()) { + queueEndOfStreamToEncoder(); + return false; + } + + @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); + if (decoderOutputBuffer == null) { + return false; + } + if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) { + flushSonicAndSetSpeed(currentSpeed); + return false; + } + feedEncoder(decoderOutputBuffer); + if (!decoderOutputBuffer.hasRemaining()) { + decoder.releaseOutputBuffer(); + } + return true; + } + + /** + * Attempts to pass audio processor output data to the encoder, and returns whether it may be + * possible to pass more data immediately by calling this method again. + */ + @RequiresNonNull({"encoderInputAudioFormat", "encoder"}) + private boolean feedEncoderFromSonic() { + if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { + return false; + } + + if (!sonicOutputBuffer.hasRemaining()) { + sonicOutputBuffer = sonicAudioProcessor.getOutput(); + if (!sonicOutputBuffer.hasRemaining()) { + if (decoder.isEnded() && sonicAudioProcessor.isEnded()) { + queueEndOfStreamToEncoder(); + } + return false; + } + } + + feedEncoder(sonicOutputBuffer); + return true; + } + + /** + * Attempts to process decoder output data, and returns whether it may be possible to process more + * data immediately by calling this method again. + */ + private boolean feedSonicFromDecoder() { + if (drainingSonicForSpeedChange) { + if (sonicAudioProcessor.isEnded() && !sonicOutputBuffer.hasRemaining()) { + flushSonicAndSetSpeed(currentSpeed); + drainingSonicForSpeedChange = false; + } + return false; + } + + // Sonic invalidates any previous output buffer when more input is queued, so we don't queue if + // there is output still to be processed. + if (sonicOutputBuffer.hasRemaining()) { + return false; + } + + if (decoder.isEnded()) { + sonicAudioProcessor.queueEndOfStream(); + return false; + } + checkState(!sonicAudioProcessor.isEnded()); + + @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); + if (decoderOutputBuffer == null) { + return false; + } + if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) { + sonicAudioProcessor.queueEndOfStream(); + drainingSonicForSpeedChange = true; + return false; + } + sonicAudioProcessor.queueInput(decoderOutputBuffer); + if (!decoderOutputBuffer.hasRemaining()) { + decoder.releaseOutputBuffer(); + } + return true; + } + + /** + * Feeds as much data as possible between the current position and limit of the specified {@link + * ByteBuffer} to the encoder, and advances its position by the number of bytes fed. + */ + @RequiresNonNull({"encoder", "encoderInputAudioFormat"}) + private void feedEncoder(ByteBuffer inputBuffer) { + ByteBuffer encoderInputBufferData = checkNotNull(encoderInputBuffer.data); + int bufferLimit = inputBuffer.limit(); + inputBuffer.limit(min(bufferLimit, inputBuffer.position() + encoderInputBufferData.capacity())); + encoderInputBufferData.put(inputBuffer); + encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; + nextEncoderInputBufferTimeUs += + getBufferDurationUs( + /* bytesWritten= */ encoderInputBufferData.position(), + encoderInputAudioFormat.bytesPerFrame, + encoderInputAudioFormat.sampleRate); + encoderInputBuffer.setFlags(0); + encoderInputBuffer.flip(); + inputBuffer.limit(bufferLimit); + encoder.queueInputBuffer(encoderInputBuffer); + } + + @RequiresNonNull("encoder") + private void queueEndOfStreamToEncoder() { + checkState(checkNotNull(encoderInputBuffer.data).position() == 0); + encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; + encoderInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + encoderInputBuffer.flip(); + // Queuing EOS should only occur with an empty buffer. + encoder.queueInputBuffer(encoderInputBuffer); + } + + /** + * Attempts to configure the {@link #encoder} and Sonic (if applicable), if they have not been + * configured yet, and returns whether they have been configured. + */ + @EnsuresNonNullIf( + expression = {"encoder", "encoderInputAudioFormat"}, + result = true) + private boolean ensureEncoderAndAudioProcessingConfigured() throws ExoPlaybackException { + if (encoder != null && encoderInputAudioFormat != null) { + return true; + } + @Nullable Format decoderOutputFormat = decoder.getOutputFormat(); + if (decoderOutputFormat == null) { + return false; + } + AudioFormat outputAudioFormat = + new AudioFormat( + decoderOutputFormat.sampleRate, + decoderOutputFormat.channelCount, + decoderOutputFormat.pcmEncoding); + if (transformation.flattenForSlowMotion) { + try { + outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat); + flushSonicAndSetSpeed(currentSpeed); + } catch (AudioProcessor.UnhandledAudioFormatException e) { + // TODO(internal b/192864511): Assign an adequate error code. + throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); + } + } + String audioMimeType = + transformation.audioMimeType == null + ? decoderInputFormat.sampleMimeType + : transformation.audioMimeType; + try { + encoder = + MediaCodecAdapterWrapper.createForAudioEncoding( + new Format.Builder() + .setSampleMimeType(audioMimeType) + .setSampleRate(outputAudioFormat.sampleRate) + .setChannelCount(outputAudioFormat.channelCount) + .setAverageBitrate(DEFAULT_ENCODER_BITRATE) + .build()); + } catch (IOException e) { + // TODO(internal b/192864511): Assign an adequate error code. + throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); + } + encoderInputAudioFormat = outputAudioFormat; + return true; + } + + private boolean isSpeedChanging(BufferInfo bufferInfo) { + if (!transformation.flattenForSlowMotion) { + return false; + } + float newSpeed = speedProvider.getSpeed(bufferInfo.presentationTimeUs); + boolean speedChanging = newSpeed != currentSpeed; + currentSpeed = newSpeed; + return speedChanging; + } + + private void flushSonicAndSetSpeed(float speed) { + sonicAudioProcessor.setSpeed(speed); + sonicAudioProcessor.setPitch(speed); + sonicAudioProcessor.flush(); + } + + private ExoPlaybackException createRendererException(Throwable cause, int errorCode) { + return ExoPlaybackException.createForRenderer( + cause, + TAG, + rendererIndex, + decoderInputFormat, + /* rendererFormatSupport= */ C.FORMAT_HANDLED, + /* isRecoverable= */ false, + errorCode); + } + + private static long getBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) { + long framesWritten = bytesWritten / bytesPerFrame; + return framesWritten * C.MICROS_PER_SECOND / sampleRate; + } +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SamplePipeline.java new file mode 100644 index 0000000000..6407b4f440 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SamplePipeline.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 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.transformer; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; + +/** + * Pipeline for processing {@link DecoderInputBuffer DecoderInputBuffers}. + * + *

This pipeline can be used to implement transformations of audio or video samples. + */ +/* package */ interface SamplePipeline { + + /** Returns a buffer if the pipeline is ready to accept input, and {@code null} otherwise. */ + @Nullable + DecoderInputBuffer dequeueInputBuffer(); + + /** + * Informs the pipeline that its input buffer contains new input. + * + *

Should be called after filling the input buffer from {@link #dequeueInputBuffer()} with new + * input. + */ + void queueInputBuffer(); + + /** + * Process the input data and returns whether more data can be processed by calling this method + * again. + */ + boolean processData() throws ExoPlaybackException; + + /** Returns the output format of the pipeline if available, and {@code null} otherwise. */ + @Nullable + Format getOutputFormat(); + + /** Returns an output buffer if the pipeline has produced output, and {@code null} otherwise */ + @Nullable + DecoderInputBuffer getOutputBuffer(); + + /** + * Releases the pipeline's output buffer. + * + *

Should be called when the output buffer from {@link #getOutputBuffer()} is no longer needed. + */ + void releaseOutputBuffer(); + + /** Returns whether the pipeline has ended. */ + boolean isEnded(); + + /** Releases all resources held by the pipeline. */ + void release(); +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java index 7efa0eb780..18b3b489a9 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java @@ -18,24 +18,15 @@ package com.google.android.exoplayer2.transformer; import static com.google.android.exoplayer2.source.SampleStream.FLAG_REQUIRE_FORMAT; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; -import static java.lang.Math.min; -import android.media.MediaCodec.BufferInfo; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.PlaybackException; -import com.google.android.exoplayer2.audio.AudioProcessor; -import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; -import com.google.android.exoplayer2.audio.SonicAudioProcessor; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; -import java.io.IOException; -import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -44,37 +35,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /* package */ final class TransformerAudioRenderer extends TransformerBaseRenderer { private static final String TAG = "TransformerAudioRenderer"; - private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024; - private static final float SPEED_UNSET = -1f; private final DecoderInputBuffer decoderInputBuffer; - private final DecoderInputBuffer encoderInputBuffer; - private final SonicAudioProcessor sonicAudioProcessor; - @Nullable private MediaCodecAdapterWrapper decoder; - @Nullable private MediaCodecAdapterWrapper encoder; - @Nullable private SpeedProvider speedProvider; - private @MonotonicNonNull Format decoderInputFormat; - private @MonotonicNonNull AudioFormat encoderInputAudioFormat; - - private ByteBuffer sonicOutputBuffer; - private long nextEncoderInputBufferTimeUs; - private float currentSpeed; + private @MonotonicNonNull SamplePipeline samplePipeline; + private boolean muxerWrapperTrackAdded; private boolean muxerWrapperTrackEnded; - private boolean hasEncoderOutputFormat; - private boolean drainingSonicForSpeedChange; public TransformerAudioRenderer( MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, Transformation transformation) { super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformation); decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); - encoderInputBuffer = - new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); - sonicAudioProcessor = new SonicAudioProcessor(); - sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; - nextEncoderInputBufferTimeUs = 0; - currentSpeed = SPEED_UNSET; } @Override @@ -89,201 +61,94 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override protected void onReset() { - decoderInputBuffer.clear(); - decoderInputBuffer.data = null; - encoderInputBuffer.clear(); - encoderInputBuffer.data = null; - sonicAudioProcessor.reset(); - if (decoder != null) { - decoder.release(); - decoder = null; + if (samplePipeline != null) { + samplePipeline.release(); } - if (encoder != null) { - encoder.release(); - encoder = null; - } - speedProvider = null; - sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; - nextEncoderInputBufferTimeUs = 0; - currentSpeed = SPEED_UNSET; + muxerWrapperTrackAdded = false; muxerWrapperTrackEnded = false; - hasEncoderOutputFormat = false; - drainingSonicForSpeedChange = false; } @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (!isRendererStarted || isEnded()) { + if (!isRendererStarted || isEnded() || !ensureRendererConfigured()) { return; } - if (ensureDecoderConfigured()) { - MediaCodecAdapterWrapper decoder = this.decoder; - if (ensureEncoderAndAudioProcessingConfigured()) { - MediaCodecAdapterWrapper encoder = this.encoder; - while (feedMuxerFromEncoder(encoder)) {} - if (sonicAudioProcessor.isActive()) { - while (feedEncoderFromSonic(decoder, encoder)) {} - while (feedSonicFromDecoder(decoder)) {} - } else { - while (feedEncoderFromDecoder(decoder, encoder)) {} - } - } - while (feedDecoderFromInput(decoder)) {} + while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {} + } + + /** Attempts to read the input format and to initialize the sample pipeline. */ + @EnsuresNonNullIf(expression = "samplePipeline", result = true) + private boolean ensureRendererConfigured() throws ExoPlaybackException { + if (samplePipeline != null) { + return true; } + FormatHolder formatHolder = getFormatHolder(); + @ReadDataResult + int result = readSource(formatHolder, decoderInputBuffer, /* readFlags= */ FLAG_REQUIRE_FORMAT); + if (result != C.RESULT_FORMAT_READ) { + return false; + } + samplePipeline = + new AudioSamplePipeline(checkNotNull(formatHolder.format), transformation, getIndex()); + return true; } /** - * Attempts to write encoder output data to the muxer, and returns whether it may be possible to - * write more data immediately by calling this method again. + * Attempts to write sample pipeline output data to the muxer, and returns whether it may be + * possible to write more data immediately by calling this method again. */ - private boolean feedMuxerFromEncoder(MediaCodecAdapterWrapper encoder) { - if (!hasEncoderOutputFormat) { - @Nullable Format encoderOutputFormat = encoder.getOutputFormat(); - if (encoderOutputFormat == null) { + @RequiresNonNull("samplePipeline") + private boolean feedMuxerFromPipeline() { + if (!muxerWrapperTrackAdded) { + @Nullable Format samplePipelineOutputFormat = samplePipeline.getOutputFormat(); + if (samplePipelineOutputFormat == null) { return false; } - hasEncoderOutputFormat = true; - muxerWrapper.addTrackFormat(encoderOutputFormat); + muxerWrapperTrackAdded = true; + muxerWrapper.addTrackFormat(samplePipelineOutputFormat); } - if (encoder.isEnded()) { + if (samplePipeline.isEnded()) { muxerWrapper.endTrack(getTrackType()); muxerWrapperTrackEnded = true; return false; } - @Nullable ByteBuffer encoderOutputBuffer = encoder.getOutputBuffer(); - if (encoderOutputBuffer == null) { + @Nullable DecoderInputBuffer samplePipelineOutputBuffer = samplePipeline.getOutputBuffer(); + if (samplePipelineOutputBuffer == null) { return false; } - BufferInfo encoderOutputBufferInfo = checkNotNull(encoder.getOutputBufferInfo()); if (!muxerWrapper.writeSample( getTrackType(), - encoderOutputBuffer, + samplePipelineOutputBuffer.data, /* isKeyFrame= */ true, - encoderOutputBufferInfo.presentationTimeUs)) { + samplePipelineOutputBuffer.timeUs)) { return false; } - encoder.releaseOutputBuffer(); + samplePipeline.releaseOutputBuffer(); return true; } /** - * Attempts to pass decoder output data to the encoder, and returns whether it may be possible to + * Attempts to pass input data to the sample pipeline, and returns whether it may be possible to * pass more data immediately by calling this method again. */ - @RequiresNonNull({"encoderInputAudioFormat"}) - private boolean feedEncoderFromDecoder( - MediaCodecAdapterWrapper decoder, MediaCodecAdapterWrapper encoder) { - if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { + @RequiresNonNull("samplePipeline") + private boolean feedPipelineFromInput() { + @Nullable DecoderInputBuffer samplePipelineInputBuffer = samplePipeline.dequeueInputBuffer(); + if (samplePipelineInputBuffer == null) { return false; } - if (decoder.isEnded()) { - queueEndOfStreamToEncoder(encoder); - return false; - } - - @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); - if (decoderOutputBuffer == null) { - return false; - } - if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) { - flushSonicAndSetSpeed(currentSpeed); - return false; - } - feedEncoder(encoder, decoderOutputBuffer); - if (!decoderOutputBuffer.hasRemaining()) { - decoder.releaseOutputBuffer(); - } - return true; - } - - /** - * Attempts to pass audio processor output data to the encoder, and returns whether it may be - * possible to pass more data immediately by calling this method again. - */ - @RequiresNonNull({"encoderInputAudioFormat"}) - private boolean feedEncoderFromSonic( - MediaCodecAdapterWrapper decoder, MediaCodecAdapterWrapper encoder) { - if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { - return false; - } - - if (!sonicOutputBuffer.hasRemaining()) { - sonicOutputBuffer = sonicAudioProcessor.getOutput(); - if (!sonicOutputBuffer.hasRemaining()) { - if (decoder.isEnded() && sonicAudioProcessor.isEnded()) { - queueEndOfStreamToEncoder(encoder); - } - return false; - } - } - - feedEncoder(encoder, sonicOutputBuffer); - return true; - } - - /** - * Attempts to process decoder output data, and returns whether it may be possible to process more - * data immediately by calling this method again. - */ - private boolean feedSonicFromDecoder(MediaCodecAdapterWrapper decoder) { - if (drainingSonicForSpeedChange) { - if (sonicAudioProcessor.isEnded() && !sonicOutputBuffer.hasRemaining()) { - flushSonicAndSetSpeed(currentSpeed); - drainingSonicForSpeedChange = false; - } - return false; - } - - // Sonic invalidates any previous output buffer when more input is queued, so we don't queue if - // there is output still to be processed. - if (sonicOutputBuffer.hasRemaining()) { - return false; - } - - if (decoder.isEnded()) { - sonicAudioProcessor.queueEndOfStream(); - return false; - } - checkState(!sonicAudioProcessor.isEnded()); - - @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); - if (decoderOutputBuffer == null) { - return false; - } - if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) { - sonicAudioProcessor.queueEndOfStream(); - drainingSonicForSpeedChange = true; - return false; - } - sonicAudioProcessor.queueInput(decoderOutputBuffer); - if (!decoderOutputBuffer.hasRemaining()) { - decoder.releaseOutputBuffer(); - } - return true; - } - - /** - * Attempts to pass input data to the decoder, and returns whether it may be possible to pass more - * data immediately by calling this method again. - */ - private boolean feedDecoderFromInput(MediaCodecAdapterWrapper decoder) { - if (!decoder.maybeDequeueInputBuffer(decoderInputBuffer)) { - return false; - } - - decoderInputBuffer.clear(); @ReadDataResult - int result = readSource(getFormatHolder(), decoderInputBuffer, /* readFlags= */ 0); + int result = readSource(getFormatHolder(), samplePipelineInputBuffer, /* readFlags= */ 0); switch (result) { case C.RESULT_BUFFER_READ: - mediaClock.updateTimeForTrackType(getTrackType(), decoderInputBuffer.timeUs); - decoderInputBuffer.timeUs -= streamOffsetUs; - decoderInputBuffer.flip(); - decoder.queueInputBuffer(decoderInputBuffer); - return !decoderInputBuffer.isEndOfStream(); + mediaClock.updateTimeForTrackType(getTrackType(), samplePipelineInputBuffer.timeUs); + samplePipelineInputBuffer.timeUs -= streamOffsetUs; + samplePipelineInputBuffer.flip(); + samplePipeline.queueInputBuffer(); + return !samplePipelineInputBuffer.isEndOfStream(); case C.RESULT_FORMAT_READ: throw new IllegalStateException("Format changes are not supported."); case C.RESULT_NOTHING_READ: @@ -291,150 +156,4 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return false; } } - - /** - * Feeds as much data as possible between the current position and limit of the specified {@link - * ByteBuffer} to the encoder, and advances its position by the number of bytes fed. - */ - @RequiresNonNull({"encoderInputAudioFormat"}) - private void feedEncoder(MediaCodecAdapterWrapper encoder, ByteBuffer inputBuffer) { - ByteBuffer encoderInputBufferData = checkNotNull(encoderInputBuffer.data); - int bufferLimit = inputBuffer.limit(); - inputBuffer.limit(min(bufferLimit, inputBuffer.position() + encoderInputBufferData.capacity())); - encoderInputBufferData.put(inputBuffer); - encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; - nextEncoderInputBufferTimeUs += - getBufferDurationUs( - /* bytesWritten= */ encoderInputBufferData.position(), - encoderInputAudioFormat.bytesPerFrame, - encoderInputAudioFormat.sampleRate); - encoderInputBuffer.setFlags(0); - encoderInputBuffer.flip(); - inputBuffer.limit(bufferLimit); - encoder.queueInputBuffer(encoderInputBuffer); - } - - private void queueEndOfStreamToEncoder(MediaCodecAdapterWrapper encoder) { - checkState(checkNotNull(encoderInputBuffer.data).position() == 0); - encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; - encoderInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); - encoderInputBuffer.flip(); - // Queuing EOS should only occur with an empty buffer. - encoder.queueInputBuffer(encoderInputBuffer); - } - - /** - * Attempts to configure the {@link #encoder} and Sonic (if applicable), if they have not been - * configured yet, and returns whether they have been configured. - */ - @RequiresNonNull({"decoder", "decoderInputFormat"}) - @EnsuresNonNullIf( - expression = {"encoder", "encoderInputAudioFormat"}, - result = true) - private boolean ensureEncoderAndAudioProcessingConfigured() throws ExoPlaybackException { - if (encoder != null && encoderInputAudioFormat != null) { - return true; - } - MediaCodecAdapterWrapper decoder = this.decoder; - @Nullable Format decoderOutputFormat = decoder.getOutputFormat(); - if (decoderOutputFormat == null) { - return false; - } - AudioFormat outputAudioFormat = - new AudioFormat( - decoderOutputFormat.sampleRate, - decoderOutputFormat.channelCount, - decoderOutputFormat.pcmEncoding); - if (transformation.flattenForSlowMotion) { - try { - outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat); - flushSonicAndSetSpeed(currentSpeed); - } catch (AudioProcessor.UnhandledAudioFormatException e) { - // TODO(internal b/192864511): Assign an adequate error code. - throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); - } - } - String audioMimeType = - transformation.audioMimeType == null - ? decoderInputFormat.sampleMimeType - : transformation.audioMimeType; - try { - encoder = - MediaCodecAdapterWrapper.createForAudioEncoding( - new Format.Builder() - .setSampleMimeType(audioMimeType) - .setSampleRate(outputAudioFormat.sampleRate) - .setChannelCount(outputAudioFormat.channelCount) - .setAverageBitrate(DEFAULT_ENCODER_BITRATE) - .build()); - } catch (IOException e) { - // TODO(internal b/192864511): Assign an adequate error code. - throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); - } - encoderInputAudioFormat = outputAudioFormat; - return true; - } - - /** - * Attempts to configure the {@link #decoder} if it has not been configured yet, and returns - * whether the decoder has been configured. - */ - @EnsuresNonNullIf( - expression = {"decoderInputFormat", "decoder"}, - result = true) - private boolean ensureDecoderConfigured() throws ExoPlaybackException { - if (decoder != null && decoderInputFormat != null) { - return true; - } - - FormatHolder formatHolder = getFormatHolder(); - @ReadDataResult int result = readSource(formatHolder, decoderInputBuffer, FLAG_REQUIRE_FORMAT); - if (result != C.RESULT_FORMAT_READ) { - return false; - } - decoderInputFormat = checkNotNull(formatHolder.format); - MediaCodecAdapterWrapper decoder; - try { - decoder = MediaCodecAdapterWrapper.createForAudioDecoding(decoderInputFormat); - } catch (IOException e) { - // TODO (internal b/184262323): Assign an adequate error code. - throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); - } - speedProvider = new SegmentSpeedProvider(decoderInputFormat); - currentSpeed = speedProvider.getSpeed(0); - this.decoder = decoder; - return true; - } - - private boolean isSpeedChanging(BufferInfo bufferInfo) { - if (!transformation.flattenForSlowMotion) { - return false; - } - float newSpeed = checkNotNull(speedProvider).getSpeed(bufferInfo.presentationTimeUs); - boolean speedChanging = newSpeed != currentSpeed; - currentSpeed = newSpeed; - return speedChanging; - } - - private void flushSonicAndSetSpeed(float speed) { - sonicAudioProcessor.setSpeed(speed); - sonicAudioProcessor.setPitch(speed); - sonicAudioProcessor.flush(); - } - - private ExoPlaybackException createRendererException(Throwable cause, int errorCode) { - return ExoPlaybackException.createForRenderer( - cause, - TAG, - getIndex(), - decoderInputFormat, - /* rendererFormatSupport= */ C.FORMAT_HANDLED, - /* isRecoverable= */ false, - errorCode); - } - - private static long getBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) { - long framesWritten = bytesWritten / bytesPerFrame; - return framesWritten * C.MICROS_PER_SECOND / sampleRate; - } } diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump index dd820ba6ab..c5991e7a4b 100644 --- a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump +++ b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump @@ -285,30 +285,6 @@ sample: size = 230 isKeyFrame = true presentationTimeUs = 67627 -sample: - trackIndex = 1 - dataHashCode = -1830836678 - size = 1051 - isKeyFrame = false - presentationTimeUs = 500500 -sample: - trackIndex = 1 - dataHashCode = 1767407540 - size = 874 - isKeyFrame = false - presentationTimeUs = 467133 -sample: - trackIndex = 1 - dataHashCode = 918440283 - size = 781 - isKeyFrame = false - presentationTimeUs = 533866 -sample: - trackIndex = 1 - dataHashCode = -1408463661 - size = 4725 - isKeyFrame = false - presentationTimeUs = 700700 sample: trackIndex = 0 dataHashCode = -997198863 @@ -399,6 +375,30 @@ sample: size = 6 isKeyFrame = true presentationTimeUs = 107644 +sample: + trackIndex = 1 + dataHashCode = -1830836678 + size = 1051 + isKeyFrame = false + presentationTimeUs = 500500 +sample: + trackIndex = 1 + dataHashCode = 1767407540 + size = 874 + isKeyFrame = false + presentationTimeUs = 467133 +sample: + trackIndex = 1 + dataHashCode = 918440283 + size = 781 + isKeyFrame = false + presentationTimeUs = 533866 +sample: + trackIndex = 1 + dataHashCode = -1408463661 + size = 4725 + isKeyFrame = false + presentationTimeUs = 700700 sample: trackIndex = 1 dataHashCode = 1569455924 diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump b/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump index 6115358157..707be77d53 100644 --- a/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump +++ b/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump @@ -132,64 +132,148 @@ sample: presentationTimeUs = 0 sample: trackIndex = 0 - dataHashCode = -833872563 - size = 1732 + dataHashCode = 1000136444 + size = 140 isKeyFrame = true presentationTimeUs = 416 sample: trackIndex = 0 - dataHashCode = -135901925 - size = 380 + dataHashCode = 217961709 + size = 172 isKeyFrame = true - presentationTimeUs = 36499 + presentationTimeUs = 3332 +sample: + trackIndex = 0 + dataHashCode = -879376936 + size = 176 + isKeyFrame = true + presentationTimeUs = 6915 +sample: + trackIndex = 0 + dataHashCode = 1259979587 + size = 192 + isKeyFrame = true + presentationTimeUs = 10581 +sample: + trackIndex = 0 + dataHashCode = 907407225 + size = 188 + isKeyFrame = true + presentationTimeUs = 14581 +sample: + trackIndex = 0 + dataHashCode = -904354707 + size = 176 + isKeyFrame = true + presentationTimeUs = 18497 +sample: + trackIndex = 0 + dataHashCode = 1001385853 + size = 172 + isKeyFrame = true + presentationTimeUs = 22163 +sample: + trackIndex = 0 + dataHashCode = 1545716086 + size = 196 + isKeyFrame = true + presentationTimeUs = 25746 +sample: + trackIndex = 0 + dataHashCode = 358710839 + size = 180 + isKeyFrame = true + presentationTimeUs = 29829 +sample: + trackIndex = 0 + dataHashCode = -671124798 + size = 140 + isKeyFrame = true + presentationTimeUs = 33579 +sample: + trackIndex = 0 + dataHashCode = -945404910 + size = 120 + isKeyFrame = true + presentationTimeUs = 36495 +sample: + trackIndex = 0 + dataHashCode = 1881048379 + size = 88 + isKeyFrame = true + presentationTimeUs = 38995 +sample: + trackIndex = 0 + dataHashCode = 1059579897 + size = 88 + isKeyFrame = true + presentationTimeUs = 40828 +sample: + trackIndex = 0 + dataHashCode = 1496098648 + size = 84 + isKeyFrame = true + presentationTimeUs = 42661 sample: trackIndex = 0 dataHashCode = 250093960 size = 751 isKeyFrame = true - presentationTimeUs = 44415 + presentationTimeUs = 44411 sample: trackIndex = 0 dataHashCode = 1895536226 size = 1045 isKeyFrame = true - presentationTimeUs = 59998 + presentationTimeUs = 59994 sample: trackIndex = 0 dataHashCode = 1723596464 size = 947 isKeyFrame = true - presentationTimeUs = 81748 + presentationTimeUs = 81744 sample: trackIndex = 0 dataHashCode = -978803114 size = 946 isKeyFrame = true - presentationTimeUs = 101414 + presentationTimeUs = 101410 sample: trackIndex = 0 dataHashCode = 387377078 size = 946 isKeyFrame = true - presentationTimeUs = 121080 + presentationTimeUs = 121076 sample: trackIndex = 0 dataHashCode = -132658698 size = 901 isKeyFrame = true - presentationTimeUs = 140746 + presentationTimeUs = 140742 sample: trackIndex = 0 dataHashCode = 1495036471 size = 899 isKeyFrame = true - presentationTimeUs = 159496 + presentationTimeUs = 159492 sample: trackIndex = 0 dataHashCode = 304440590 size = 878 isKeyFrame = true - presentationTimeUs = 178162 + presentationTimeUs = 178158 +sample: + trackIndex = 0 + dataHashCode = -1955900344 + size = 112 + isKeyFrame = true + presentationTimeUs = 196408 +sample: + trackIndex = 0 + dataHashCode = 88896626 + size = 116 + isKeyFrame = true + presentationTimeUs = 198741 sample: trackIndex = 1 dataHashCode = 2139021989 @@ -214,12 +298,6 @@ sample: size = 1193 isKeyFrame = false presentationTimeUs = 734083 -sample: - trackIndex = 0 - dataHashCode = -752661703 - size = 228 - isKeyFrame = true - presentationTimeUs = 196412 sample: trackIndex = 1 dataHashCode = -1554795381 From 14b42a4ed1a3d0375605f579609a257f39d3e4c1 Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Thu, 4 Nov 2021 13:58:55 +0000 Subject: [PATCH 07/41] GL: Misc GL refactoring. * Remove GlUtil.Program String[] constructor to unify and just use the String constructor. * Add getAttributeArrayLocationAndEnable() to simplify things a tiny bit. * Increase usage of constant values. PiperOrigin-RevId: 407570340 --- .../android/exoplayer2/util/GlUtil.java | 30 +++++----- .../video/VideoDecoderGLSurfaceView.java | 19 +++--- .../video/spherical/ProjectionRenderer.java | 59 ++++++++----------- .../TransformerTranscodingVideoRenderer.java | 21 ++++--- 4 files changed, 60 insertions(+), 69 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index a8e2d5aa48..84794d6550 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -26,7 +26,6 @@ import android.opengl.EGLDisplay; import android.opengl.EGLSurface; import android.opengl.GLES11Ext; import android.opengl.GLES20; -import android.text.TextUtils; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -87,18 +86,6 @@ public final class GlUtil { this(loadAsset(context, vertexShaderFilePath), loadAsset(context, fragmentShaderFilePath)); } - /** - * Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code. - * - * @param vertexShaderGlsl The vertex shader program as arrays of strings. Strings are joined by - * adding a new line character in between each of them. - * @param fragmentShaderGlsl The fragment shader program as arrays of strings. Strings are - * joined by adding a new line character in between each of them. - */ - public Program(String[] vertexShaderGlsl, String[] fragmentShaderGlsl) { - this(TextUtils.join("\n", vertexShaderGlsl), TextUtils.join("\n", fragmentShaderGlsl)); - } - /** Uses the program. */ public void use() { // Link and check for errors. @@ -119,8 +106,19 @@ public final class GlUtil { GLES20.glDeleteProgram(programId); } + /** + * Returns the location of an {@link Attribute}, which has been enabled as a vertex attribute + * array. + */ + public int getAttributeArrayLocationAndEnable(String attributeName) { + int location = getAttributeLocation(attributeName); + GLES20.glEnableVertexAttribArray(location); + checkGlError(); + return location; + } + /** Returns the location of an {@link Attribute}. */ - public int getAttribLocation(String attributeName) { + private int getAttributeLocation(String attributeName) { return GLES20.glGetAttribLocation(programId, attributeName); } @@ -134,7 +132,7 @@ public final class GlUtil { int[] attributeCount = new int[1]; GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, 0); if (attributeCount[0] != 2) { - throw new IllegalStateException("Expected two attributes."); + throw new IllegalStateException("Expected two attributes but found " + attributeCount[0]); } Attribute[] attributes = new Attribute[attributeCount[0]]; @@ -169,7 +167,7 @@ public final class GlUtil { GLES20.glGetActiveAttrib( programId, index, length[0], ignore, 0, size, 0, type, 0, nameBytes, 0); String name = new String(nameBytes, 0, strlen(nameBytes)); - int location = getAttribLocation(name); + int location = getAttributeLocation(name); return new Attribute(name, index, location); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java index 1d9dcadbfb..8f61630e19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java @@ -167,8 +167,7 @@ public final class VideoDecoderGLSurfaceView extends GLSurfaceView public void onSurfaceCreated(GL10 unused, EGLConfig config) { program = new GlUtil.Program(VERTEX_SHADER, FRAGMENT_SHADER); program.use(); - int posLocation = program.getAttribLocation("in_pos"); - GLES20.glEnableVertexAttribArray(posLocation); + int posLocation = program.getAttributeArrayLocationAndEnable("in_pos"); GLES20.glVertexAttribPointer( posLocation, 2, @@ -176,13 +175,9 @@ public final class VideoDecoderGLSurfaceView extends GLSurfaceView /* normalized= */ false, /* stride= */ 0, TEXTURE_VERTICES); - texLocations[0] = program.getAttribLocation("in_tc_y"); - GLES20.glEnableVertexAttribArray(texLocations[0]); - texLocations[1] = program.getAttribLocation("in_tc_u"); - GLES20.glEnableVertexAttribArray(texLocations[1]); - texLocations[2] = program.getAttribLocation("in_tc_v"); - GLES20.glEnableVertexAttribArray(texLocations[2]); - GlUtil.checkGlError(); + texLocations[0] = program.getAttributeArrayLocationAndEnable("in_tc_y"); + texLocations[1] = program.getAttributeArrayLocationAndEnable("in_tc_u"); + texLocations[2] = program.getAttributeArrayLocationAndEnable("in_tc_v"); colorMatrixLocation = program.getUniformLocation("mColorConversion"); GlUtil.checkGlError(); setupTextures(); @@ -255,9 +250,9 @@ public final class VideoDecoderGLSurfaceView extends GLSurfaceView int[] widths = new int[3]; widths[0] = outputBuffer.width; - // TODO: Handle streams where chroma channels are not stored at half width and height - // compared to luma channel. See [Internal: b/142097774]. - // U and V planes are being stored at half width compared to Y. + // TODO(b/142097774): Handle streams where chroma channels are not stored at half width and + // height compared to the luma channel. U and V planes are being stored at half width compared + // to Y. widths[1] = widths[2] = (widths[0] + 1) / 2; for (int i = 0; i < 3; i++) { // Set cropping of stride if either width or stride has changed. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionRenderer.java index c52e5ddd57..5f88b05488 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionRenderer.java @@ -46,33 +46,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } // Basic vertex & fragment shaders to render a mesh with 3D position & 2D texture data. - private static final String[] VERTEX_SHADER_CODE = - new String[] { - "uniform mat4 uMvpMatrix;", - "uniform mat3 uTexMatrix;", - "attribute vec4 aPosition;", - "attribute vec2 aTexCoords;", - "varying vec2 vTexCoords;", - - // Standard transformation. - "void main() {", - " gl_Position = uMvpMatrix * aPosition;", - " vTexCoords = (uTexMatrix * vec3(aTexCoords, 1)).xy;", - "}" - }; - private static final String[] FRAGMENT_SHADER_CODE = - new String[] { - // This is required since the texture data is GL_TEXTURE_EXTERNAL_OES. - "#extension GL_OES_EGL_image_external : require", - "precision mediump float;", - - // Standard texture rendering shader. - "uniform samplerExternalOES uTexture;", - "varying vec2 vTexCoords;", - "void main() {", - " gl_FragColor = texture2D(uTexture, vTexCoords);", - "}" - }; + private static final String VERTEX_SHADER = + "uniform mat4 uMvpMatrix;\n" + + "uniform mat3 uTexMatrix;\n" + + "attribute vec4 aPosition;\n" + + "attribute vec2 aTexCoords;\n" + + "varying vec2 vTexCoords;\n" + + "// Standard transformation.\n" + + "void main() {\n" + + " gl_Position = uMvpMatrix * aPosition;\n" + + " vTexCoords = (uTexMatrix * vec3(aTexCoords, 1)).xy;\n" + + "}\n"; + private static final String FRAGMENT_SHADER = + "// This is required since the texture data is GL_TEXTURE_EXTERNAL_OES.\n" + + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "// Standard texture rendering shader.\n" + + "uniform samplerExternalOES uTexture;\n" + + "varying vec2 vTexCoords;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(uTexture, vTexCoords);\n" + + "}\n"; // Texture transform matrices. private static final float[] TEX_MATRIX_WHOLE = { @@ -121,11 +115,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Initializes of the GL components. */ /* package */ void init() { - program = new GlUtil.Program(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE); + program = new GlUtil.Program(VERTEX_SHADER, FRAGMENT_SHADER); mvpMatrixHandle = program.getUniformLocation("uMvpMatrix"); uTexMatrixHandle = program.getUniformLocation("uTexMatrix"); - positionHandle = program.getAttribLocation("aPosition"); - texCoordsHandle = program.getAttribLocation("aTexCoords"); + positionHandle = program.getAttributeArrayLocationAndEnable("aPosition"); + texCoordsHandle = program.getAttributeArrayLocationAndEnable("aTexCoords"); textureHandle = program.getUniformLocation("uTexture"); } @@ -148,10 +142,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; checkNotNull(program).use(); checkGlError(); - GLES20.glEnableVertexAttribArray(positionHandle); - GLES20.glEnableVertexAttribArray(texCoordsHandle); - checkGlError(); - float[] texMatrix; if (stereoMode == C.STEREO_MODE_TOP_BOTTOM) { texMatrix = rightEye ? TEX_MATRIX_BOTTOM : TEX_MATRIX_TOP; @@ -162,6 +152,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } GLES20.glUniformMatrix3fv(uTexMatrixHandle, 1, false, texMatrix, 0); + // TODO(b/205002913): Update to use GlUtil.Uniform.bind(). GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java index f4836e49df..80b5ada468 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java @@ -56,6 +56,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final String TAG = "TransformerTranscodingVideoRenderer"; + // Predefined shader values. + private static final String VERTEX_SHADER_FILE_PATH = "shaders/blit_vertex_shader.glsl"; + private static final String FRAGMENT_SHADER_FILE_PATH = + "shaders/copy_external_fragment_shader.glsl"; + private static final int EXPECTED_NUMBER_OF_ATTRIBUTES = 2; + private static final int EXPECTED_NUMBER_OF_UNIFORMS = 2; + private final Context context; private final DecoderInputBuffer decoderInputBuffer; @@ -231,18 +238,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; decoderTextureId = GlUtil.createExternalTexture(); GlUtil.Program copyProgram; try { - copyProgram = - new GlUtil.Program( - context, - /* vertexShaderFilePath= */ "shaders/blit_vertex_shader.glsl", - /* fragmentShaderFilePath= */ "shaders/copy_external_fragment_shader.glsl"); + copyProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH); } catch (IOException e) { throw new IllegalStateException(e); } copyProgram.use(); GlUtil.Attribute[] copyAttributes = copyProgram.getAttributes(); - checkState(copyAttributes.length == 2, "Expected program to have two vertex attributes."); + checkState( + copyAttributes.length == EXPECTED_NUMBER_OF_ATTRIBUTES, + "Expected program to have " + EXPECTED_NUMBER_OF_ATTRIBUTES + " vertex attributes."); for (GlUtil.Attribute copyAttribute : copyAttributes) { if (copyAttribute.name.equals("a_position")) { copyAttribute.setBuffer( @@ -268,7 +273,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; copyAttribute.bind(); } GlUtil.Uniform[] copyUniforms = copyProgram.getUniforms(); - checkState(copyUniforms.length == 2, "Expected program to have two uniforms."); + checkState( + copyUniforms.length == EXPECTED_NUMBER_OF_UNIFORMS, + "Expected program to have " + EXPECTED_NUMBER_OF_UNIFORMS + " uniforms."); for (GlUtil.Uniform copyUniform : copyUniforms) { if (copyUniform.name.equals("tex_sampler")) { copyUniform.setSamplerTexId(decoderTextureId, 0); From 85e222a7859a4dfc8572db18966ae211dd874f27 Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 4 Nov 2021 18:53:11 +0000 Subject: [PATCH 08/41] Minor fix in AsynchronousMediaCodecAdapter.signalEndOfInputStream() PiperOrigin-RevId: 407635099 --- .../exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java | 1 + 1 file changed, 1 insertion(+) 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 8302bd2903..63b4f65a4c 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 @@ -289,6 +289,7 @@ import java.nio.ByteBuffer; @Override public void signalEndOfInputStream() { + maybeBlockOnQueueing(); codec.signalEndOfInputStream(); } From 8552345f8fb77f30d7c569b2da6cd9263295727a Mon Sep 17 00:00:00 2001 From: hschlueter Date: Fri, 5 Nov 2021 11:13:26 +0000 Subject: [PATCH 09/41] Add PassthroughSamplePipeline for audio. When no transformation is needed, the passthrough pipeline allows us to skip decoding and re-encoding. PiperOrigin-RevId: 407789767 --- .../transformer/AudioSamplePipeline.java | 1 + .../PassthroughSamplePipeline.java | 77 ++++ .../transformer/TransformerAudioRenderer.java | 10 +- .../transformerdumps/amr/sample_nb.amr.dump | 436 +++++++++--------- .../transformerdumps/mp4/sample.mp4.dump | 338 +++++++------- .../mp4/sample.mp4.novideo.dump | 98 ++-- 6 files changed, 528 insertions(+), 432 deletions(-) create mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/PassthroughSamplePipeline.java diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java index a4eb090191..167d1725bc 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java @@ -366,6 +366,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; errorCode); } + // TODO(internal b/204978301): Ensure encoder and decoder timestamps match when no speed change. private static long getBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) { long framesWritten = bytesWritten / bytesPerFrame; return framesWritten * C.MICROS_PER_SECOND / sampleRate; diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/PassthroughSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/PassthroughSamplePipeline.java new file mode 100644 index 0000000000..8b540ee105 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/PassthroughSamplePipeline.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021 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.transformer; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; + +/** Pipeline that passes through the samples without any re-encoding or transformation. */ +/* package */ final class PassthroughSamplePipeline implements SamplePipeline { + + private final DecoderInputBuffer buffer; + private final Format format; + + private boolean hasPendingBuffer; + + public PassthroughSamplePipeline(Format format) { + this.format = format; + buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + hasPendingBuffer = false; + } + + @Override + @Nullable + public DecoderInputBuffer dequeueInputBuffer() { + return hasPendingBuffer ? null : buffer; + } + + @Override + public void queueInputBuffer() { + hasPendingBuffer = true; + } + + @Override + public boolean processData() { + return false; + } + + @Override + public Format getOutputFormat() { + return format; + } + + @Override + @Nullable + public DecoderInputBuffer getOutputBuffer() { + return hasPendingBuffer ? buffer : null; + } + + @Override + public void releaseOutputBuffer() { + buffer.clear(); + hasPendingBuffer = false; + } + + @Override + public boolean isEnded() { + return buffer.isEndOfStream(); + } + + @Override + public void release() {} +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java index 18b3b489a9..e5bcb5022c 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java @@ -89,8 +89,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (result != C.RESULT_FORMAT_READ) { return false; } - samplePipeline = - new AudioSamplePipeline(checkNotNull(formatHolder.format), transformation, getIndex()); + Format decoderInputFormat = checkNotNull(formatHolder.format); + if ((transformation.audioMimeType != null + && !transformation.audioMimeType.equals(decoderInputFormat.sampleMimeType)) + || transformation.flattenForSlowMotion) { + samplePipeline = new AudioSamplePipeline(decoderInputFormat, transformation, getIndex()); + } else { + samplePipeline = new PassthroughSamplePipeline(decoderInputFormat); + } return true; } diff --git a/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump b/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump index 18836cbc5d..1b6fd750ad 100644 --- a/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump +++ b/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump @@ -1,9 +1,9 @@ containerMimeType = video/mp4 format 0: sampleMimeType = audio/3gpp + maxInputSize = 61 channelCount = 1 sampleRate = 8000 - pcmEncoding = 2 sample: trackIndex = 0 dataHashCode = 924517484 @@ -15,1301 +15,1301 @@ sample: dataHashCode = -835666085 size = 13 isKeyFrame = true - presentationTimeUs = 750 + presentationTimeUs = 20000 sample: trackIndex = 0 dataHashCode = 430283125 size = 13 isKeyFrame = true - presentationTimeUs = 1500 + presentationTimeUs = 40000 sample: trackIndex = 0 dataHashCode = 1215919932 size = 13 isKeyFrame = true - presentationTimeUs = 2250 + presentationTimeUs = 60000 sample: trackIndex = 0 dataHashCode = -386387943 size = 13 isKeyFrame = true - presentationTimeUs = 3000 + presentationTimeUs = 80000 sample: trackIndex = 0 dataHashCode = -765080119 size = 13 isKeyFrame = true - presentationTimeUs = 3750 + presentationTimeUs = 100000 sample: trackIndex = 0 dataHashCode = -1855636054 size = 13 isKeyFrame = true - presentationTimeUs = 4500 + presentationTimeUs = 120000 sample: trackIndex = 0 dataHashCode = -946579722 size = 13 isKeyFrame = true - presentationTimeUs = 5250 + presentationTimeUs = 140000 sample: trackIndex = 0 dataHashCode = -841202654 size = 13 isKeyFrame = true - presentationTimeUs = 6000 + presentationTimeUs = 160000 sample: trackIndex = 0 dataHashCode = -638764303 size = 13 isKeyFrame = true - presentationTimeUs = 6750 + presentationTimeUs = 180000 sample: trackIndex = 0 dataHashCode = -1162388941 size = 13 isKeyFrame = true - presentationTimeUs = 7500 + presentationTimeUs = 200000 sample: trackIndex = 0 dataHashCode = 572634367 size = 13 isKeyFrame = true - presentationTimeUs = 8250 + presentationTimeUs = 220000 sample: trackIndex = 0 dataHashCode = -1774188021 size = 13 isKeyFrame = true - presentationTimeUs = 9000 + presentationTimeUs = 240000 sample: trackIndex = 0 dataHashCode = 92464891 size = 13 isKeyFrame = true - presentationTimeUs = 9750 + presentationTimeUs = 260000 sample: trackIndex = 0 dataHashCode = -991397659 size = 13 isKeyFrame = true - presentationTimeUs = 10500 + presentationTimeUs = 280000 sample: trackIndex = 0 dataHashCode = -934698563 size = 13 isKeyFrame = true - presentationTimeUs = 11250 + presentationTimeUs = 300000 sample: trackIndex = 0 dataHashCode = -811030035 size = 13 isKeyFrame = true - presentationTimeUs = 12000 + presentationTimeUs = 320000 sample: trackIndex = 0 dataHashCode = 1892305159 size = 13 isKeyFrame = true - presentationTimeUs = 12750 + presentationTimeUs = 340000 sample: trackIndex = 0 dataHashCode = -1266858924 size = 13 isKeyFrame = true - presentationTimeUs = 13500 + presentationTimeUs = 360000 sample: trackIndex = 0 dataHashCode = 673814721 size = 13 isKeyFrame = true - presentationTimeUs = 14250 + presentationTimeUs = 380000 sample: trackIndex = 0 dataHashCode = 1061124709 size = 13 isKeyFrame = true - presentationTimeUs = 15000 + presentationTimeUs = 400000 sample: trackIndex = 0 dataHashCode = -869356712 size = 13 isKeyFrame = true - presentationTimeUs = 15750 + presentationTimeUs = 420000 sample: trackIndex = 0 dataHashCode = 664729362 size = 13 isKeyFrame = true - presentationTimeUs = 16500 + presentationTimeUs = 440000 sample: trackIndex = 0 dataHashCode = -1439741143 size = 13 isKeyFrame = true - presentationTimeUs = 17250 + presentationTimeUs = 460000 sample: trackIndex = 0 dataHashCode = -151627580 size = 13 isKeyFrame = true - presentationTimeUs = 18000 + presentationTimeUs = 480000 sample: trackIndex = 0 dataHashCode = -673268457 size = 13 isKeyFrame = true - presentationTimeUs = 18750 + presentationTimeUs = 500000 sample: trackIndex = 0 dataHashCode = 1839962647 size = 13 isKeyFrame = true - presentationTimeUs = 19500 + presentationTimeUs = 520000 sample: trackIndex = 0 dataHashCode = 1858999665 size = 13 isKeyFrame = true - presentationTimeUs = 20250 + presentationTimeUs = 540000 sample: trackIndex = 0 dataHashCode = -1278193537 size = 13 isKeyFrame = true - presentationTimeUs = 21000 + presentationTimeUs = 560000 sample: trackIndex = 0 dataHashCode = 568547001 size = 13 isKeyFrame = true - presentationTimeUs = 21750 + presentationTimeUs = 580000 sample: trackIndex = 0 dataHashCode = 68217362 size = 13 isKeyFrame = true - presentationTimeUs = 22500 + presentationTimeUs = 600000 sample: trackIndex = 0 dataHashCode = 1396217256 size = 13 isKeyFrame = true - presentationTimeUs = 23250 + presentationTimeUs = 620000 sample: trackIndex = 0 dataHashCode = -971293094 size = 13 isKeyFrame = true - presentationTimeUs = 24000 + presentationTimeUs = 640000 sample: trackIndex = 0 dataHashCode = -1742638874 size = 13 isKeyFrame = true - presentationTimeUs = 24750 + presentationTimeUs = 660000 sample: trackIndex = 0 dataHashCode = 2047109317 size = 13 isKeyFrame = true - presentationTimeUs = 25500 + presentationTimeUs = 680000 sample: trackIndex = 0 dataHashCode = -1668945241 size = 13 isKeyFrame = true - presentationTimeUs = 26250 + presentationTimeUs = 700000 sample: trackIndex = 0 dataHashCode = -1229766218 size = 13 isKeyFrame = true - presentationTimeUs = 27000 + presentationTimeUs = 720000 sample: trackIndex = 0 dataHashCode = 1765233454 size = 13 isKeyFrame = true - presentationTimeUs = 27750 + presentationTimeUs = 740000 sample: trackIndex = 0 dataHashCode = -1930255456 size = 13 isKeyFrame = true - presentationTimeUs = 28500 + presentationTimeUs = 760000 sample: trackIndex = 0 dataHashCode = -764925242 size = 13 isKeyFrame = true - presentationTimeUs = 29250 + presentationTimeUs = 780000 sample: trackIndex = 0 dataHashCode = -1144688369 size = 13 isKeyFrame = true - presentationTimeUs = 30000 + presentationTimeUs = 800000 sample: trackIndex = 0 dataHashCode = 1493699436 size = 13 isKeyFrame = true - presentationTimeUs = 30750 + presentationTimeUs = 820000 sample: trackIndex = 0 dataHashCode = -468614511 size = 13 isKeyFrame = true - presentationTimeUs = 31500 + presentationTimeUs = 840000 sample: trackIndex = 0 dataHashCode = -1578782058 size = 13 isKeyFrame = true - presentationTimeUs = 32250 + presentationTimeUs = 860000 sample: trackIndex = 0 dataHashCode = -675743397 size = 13 isKeyFrame = true - presentationTimeUs = 33000 + presentationTimeUs = 880000 sample: trackIndex = 0 dataHashCode = -863790111 size = 13 isKeyFrame = true - presentationTimeUs = 33750 + presentationTimeUs = 900000 sample: trackIndex = 0 dataHashCode = -732307506 size = 13 isKeyFrame = true - presentationTimeUs = 34500 + presentationTimeUs = 920000 sample: trackIndex = 0 dataHashCode = -693298708 size = 13 isKeyFrame = true - presentationTimeUs = 35250 + presentationTimeUs = 940000 sample: trackIndex = 0 dataHashCode = -799131843 size = 13 isKeyFrame = true - presentationTimeUs = 36000 + presentationTimeUs = 960000 sample: trackIndex = 0 dataHashCode = 1782866119 size = 13 isKeyFrame = true - presentationTimeUs = 36750 + presentationTimeUs = 980000 sample: trackIndex = 0 dataHashCode = -912205505 size = 13 isKeyFrame = true - presentationTimeUs = 37500 + presentationTimeUs = 1000000 sample: trackIndex = 0 dataHashCode = 1067981287 size = 13 isKeyFrame = true - presentationTimeUs = 38250 + presentationTimeUs = 1020000 sample: trackIndex = 0 dataHashCode = 490520060 size = 13 isKeyFrame = true - presentationTimeUs = 39000 + presentationTimeUs = 1040000 sample: trackIndex = 0 dataHashCode = -1950632957 size = 13 isKeyFrame = true - presentationTimeUs = 39750 + presentationTimeUs = 1060000 sample: trackIndex = 0 dataHashCode = 565485817 size = 13 isKeyFrame = true - presentationTimeUs = 40500 + presentationTimeUs = 1080000 sample: trackIndex = 0 dataHashCode = -1057414703 size = 13 isKeyFrame = true - presentationTimeUs = 41250 + presentationTimeUs = 1100000 sample: trackIndex = 0 dataHashCode = 1568746155 size = 13 isKeyFrame = true - presentationTimeUs = 42000 + presentationTimeUs = 1120000 sample: trackIndex = 0 dataHashCode = 1355412472 size = 13 isKeyFrame = true - presentationTimeUs = 42750 + presentationTimeUs = 1140000 sample: trackIndex = 0 dataHashCode = 1546368465 size = 13 isKeyFrame = true - presentationTimeUs = 43500 + presentationTimeUs = 1160000 sample: trackIndex = 0 dataHashCode = 1811529381 size = 13 isKeyFrame = true - presentationTimeUs = 44250 + presentationTimeUs = 1180000 sample: trackIndex = 0 dataHashCode = 658031078 size = 13 isKeyFrame = true - presentationTimeUs = 45000 + presentationTimeUs = 1200000 sample: trackIndex = 0 dataHashCode = 1606584486 size = 13 isKeyFrame = true - presentationTimeUs = 45750 + presentationTimeUs = 1220000 sample: trackIndex = 0 dataHashCode = 2123252778 size = 13 isKeyFrame = true - presentationTimeUs = 46500 + presentationTimeUs = 1240000 sample: trackIndex = 0 dataHashCode = -1364579398 size = 13 isKeyFrame = true - presentationTimeUs = 47250 + presentationTimeUs = 1260000 sample: trackIndex = 0 dataHashCode = 1311427887 size = 13 isKeyFrame = true - presentationTimeUs = 48000 + presentationTimeUs = 1280000 sample: trackIndex = 0 dataHashCode = -691467569 size = 13 isKeyFrame = true - presentationTimeUs = 48750 + presentationTimeUs = 1300000 sample: trackIndex = 0 dataHashCode = 1876470084 size = 13 isKeyFrame = true - presentationTimeUs = 49500 + presentationTimeUs = 1320000 sample: trackIndex = 0 dataHashCode = -1472873479 size = 13 isKeyFrame = true - presentationTimeUs = 50250 + presentationTimeUs = 1340000 sample: trackIndex = 0 dataHashCode = -143574992 size = 13 isKeyFrame = true - presentationTimeUs = 51000 + presentationTimeUs = 1360000 sample: trackIndex = 0 dataHashCode = 984180453 size = 13 isKeyFrame = true - presentationTimeUs = 51750 + presentationTimeUs = 1380000 sample: trackIndex = 0 dataHashCode = -113645527 size = 13 isKeyFrame = true - presentationTimeUs = 52500 + presentationTimeUs = 1400000 sample: trackIndex = 0 dataHashCode = 1987501641 size = 13 isKeyFrame = true - presentationTimeUs = 53250 + presentationTimeUs = 1420000 sample: trackIndex = 0 dataHashCode = -1816426230 size = 13 isKeyFrame = true - presentationTimeUs = 54000 + presentationTimeUs = 1440000 sample: trackIndex = 0 dataHashCode = -1250050360 size = 13 isKeyFrame = true - presentationTimeUs = 54750 + presentationTimeUs = 1460000 sample: trackIndex = 0 dataHashCode = 1722852790 size = 13 isKeyFrame = true - presentationTimeUs = 55500 + presentationTimeUs = 1480000 sample: trackIndex = 0 dataHashCode = 225656333 size = 13 isKeyFrame = true - presentationTimeUs = 56250 + presentationTimeUs = 1500000 sample: trackIndex = 0 dataHashCode = -2137778394 size = 13 isKeyFrame = true - presentationTimeUs = 57000 + presentationTimeUs = 1520000 sample: trackIndex = 0 dataHashCode = 1433327155 size = 13 isKeyFrame = true - presentationTimeUs = 57750 + presentationTimeUs = 1540000 sample: trackIndex = 0 dataHashCode = -974261023 size = 13 isKeyFrame = true - presentationTimeUs = 58500 + presentationTimeUs = 1560000 sample: trackIndex = 0 dataHashCode = 1797813317 size = 13 isKeyFrame = true - presentationTimeUs = 59250 + presentationTimeUs = 1580000 sample: trackIndex = 0 dataHashCode = -594033497 size = 13 isKeyFrame = true - presentationTimeUs = 60000 + presentationTimeUs = 1600000 sample: trackIndex = 0 dataHashCode = -628310540 size = 13 isKeyFrame = true - presentationTimeUs = 60750 + presentationTimeUs = 1620000 sample: trackIndex = 0 dataHashCode = 1868627831 size = 13 isKeyFrame = true - presentationTimeUs = 61500 + presentationTimeUs = 1640000 sample: trackIndex = 0 dataHashCode = 1051863958 size = 13 isKeyFrame = true - presentationTimeUs = 62250 + presentationTimeUs = 1660000 sample: trackIndex = 0 dataHashCode = -1279059211 size = 13 isKeyFrame = true - presentationTimeUs = 63000 + presentationTimeUs = 1680000 sample: trackIndex = 0 dataHashCode = 408201874 size = 13 isKeyFrame = true - presentationTimeUs = 63750 + presentationTimeUs = 1700000 sample: trackIndex = 0 dataHashCode = 1686644299 size = 13 isKeyFrame = true - presentationTimeUs = 64500 + presentationTimeUs = 1720000 sample: trackIndex = 0 dataHashCode = 1288226241 size = 13 isKeyFrame = true - presentationTimeUs = 65250 + presentationTimeUs = 1740000 sample: trackIndex = 0 dataHashCode = 432829731 size = 13 isKeyFrame = true - presentationTimeUs = 66000 + presentationTimeUs = 1760000 sample: trackIndex = 0 dataHashCode = -1679312600 size = 13 isKeyFrame = true - presentationTimeUs = 66750 + presentationTimeUs = 1780000 sample: trackIndex = 0 dataHashCode = 1206680829 size = 13 isKeyFrame = true - presentationTimeUs = 67500 + presentationTimeUs = 1800000 sample: trackIndex = 0 dataHashCode = -325844704 size = 13 isKeyFrame = true - presentationTimeUs = 68250 + presentationTimeUs = 1820000 sample: trackIndex = 0 dataHashCode = 1941808848 size = 13 isKeyFrame = true - presentationTimeUs = 69000 + presentationTimeUs = 1840000 sample: trackIndex = 0 dataHashCode = -87346412 size = 13 isKeyFrame = true - presentationTimeUs = 69750 + presentationTimeUs = 1860000 sample: trackIndex = 0 dataHashCode = -329133765 size = 13 isKeyFrame = true - presentationTimeUs = 70500 + presentationTimeUs = 1880000 sample: trackIndex = 0 dataHashCode = -1299416212 size = 13 isKeyFrame = true - presentationTimeUs = 71250 + presentationTimeUs = 1900000 sample: trackIndex = 0 dataHashCode = -1314599219 size = 13 isKeyFrame = true - presentationTimeUs = 72000 + presentationTimeUs = 1920000 sample: trackIndex = 0 dataHashCode = 1456741286 size = 13 isKeyFrame = true - presentationTimeUs = 72750 + presentationTimeUs = 1940000 sample: trackIndex = 0 dataHashCode = 151296500 size = 13 isKeyFrame = true - presentationTimeUs = 73500 + presentationTimeUs = 1960000 sample: trackIndex = 0 dataHashCode = 1708763603 size = 13 isKeyFrame = true - presentationTimeUs = 74250 + presentationTimeUs = 1980000 sample: trackIndex = 0 dataHashCode = 227542220 size = 13 isKeyFrame = true - presentationTimeUs = 75000 + presentationTimeUs = 2000000 sample: trackIndex = 0 dataHashCode = 1094305517 size = 13 isKeyFrame = true - presentationTimeUs = 75750 + presentationTimeUs = 2020000 sample: trackIndex = 0 dataHashCode = -990377604 size = 13 isKeyFrame = true - presentationTimeUs = 76500 + presentationTimeUs = 2040000 sample: trackIndex = 0 dataHashCode = -1798036230 size = 13 isKeyFrame = true - presentationTimeUs = 77250 + presentationTimeUs = 2060000 sample: trackIndex = 0 dataHashCode = -1027148291 size = 13 isKeyFrame = true - presentationTimeUs = 78000 + presentationTimeUs = 2080000 sample: trackIndex = 0 dataHashCode = 359763976 size = 13 isKeyFrame = true - presentationTimeUs = 78750 + presentationTimeUs = 2100000 sample: trackIndex = 0 dataHashCode = 1332016420 size = 13 isKeyFrame = true - presentationTimeUs = 79500 + presentationTimeUs = 2120000 sample: trackIndex = 0 dataHashCode = -102753250 size = 13 isKeyFrame = true - presentationTimeUs = 80250 + presentationTimeUs = 2140000 sample: trackIndex = 0 dataHashCode = 1959063156 size = 13 isKeyFrame = true - presentationTimeUs = 81000 + presentationTimeUs = 2160000 sample: trackIndex = 0 dataHashCode = 2129089853 size = 13 isKeyFrame = true - presentationTimeUs = 81750 + presentationTimeUs = 2180000 sample: trackIndex = 0 dataHashCode = 1658742073 size = 13 isKeyFrame = true - presentationTimeUs = 82500 + presentationTimeUs = 2200000 sample: trackIndex = 0 dataHashCode = 2136916514 size = 13 isKeyFrame = true - presentationTimeUs = 83250 + presentationTimeUs = 2220000 sample: trackIndex = 0 dataHashCode = 105121407 size = 13 isKeyFrame = true - presentationTimeUs = 84000 + presentationTimeUs = 2240000 sample: trackIndex = 0 dataHashCode = -839464484 size = 13 isKeyFrame = true - presentationTimeUs = 84750 + presentationTimeUs = 2260000 sample: trackIndex = 0 dataHashCode = -1956791168 size = 13 isKeyFrame = true - presentationTimeUs = 85500 + presentationTimeUs = 2280000 sample: trackIndex = 0 dataHashCode = -1387546109 size = 13 isKeyFrame = true - presentationTimeUs = 86250 + presentationTimeUs = 2300000 sample: trackIndex = 0 dataHashCode = 128410432 size = 13 isKeyFrame = true - presentationTimeUs = 87000 + presentationTimeUs = 2320000 sample: trackIndex = 0 dataHashCode = 907081136 size = 13 isKeyFrame = true - presentationTimeUs = 87750 + presentationTimeUs = 2340000 sample: trackIndex = 0 dataHashCode = 1124845067 size = 13 isKeyFrame = true - presentationTimeUs = 88500 + presentationTimeUs = 2360000 sample: trackIndex = 0 dataHashCode = -1714479962 size = 13 isKeyFrame = true - presentationTimeUs = 89250 + presentationTimeUs = 2380000 sample: trackIndex = 0 dataHashCode = 322029323 size = 13 isKeyFrame = true - presentationTimeUs = 90000 + presentationTimeUs = 2400000 sample: trackIndex = 0 dataHashCode = -1116281187 size = 13 isKeyFrame = true - presentationTimeUs = 90750 + presentationTimeUs = 2420000 sample: trackIndex = 0 dataHashCode = 1571181228 size = 13 isKeyFrame = true - presentationTimeUs = 91500 + presentationTimeUs = 2440000 sample: trackIndex = 0 dataHashCode = 997979854 size = 13 isKeyFrame = true - presentationTimeUs = 92250 + presentationTimeUs = 2460000 sample: trackIndex = 0 dataHashCode = -1413492413 size = 13 isKeyFrame = true - presentationTimeUs = 93000 + presentationTimeUs = 2480000 sample: trackIndex = 0 dataHashCode = -381390490 size = 13 isKeyFrame = true - presentationTimeUs = 93750 + presentationTimeUs = 2500000 sample: trackIndex = 0 dataHashCode = -331348340 size = 13 isKeyFrame = true - presentationTimeUs = 94500 + presentationTimeUs = 2520000 sample: trackIndex = 0 dataHashCode = -1568238592 size = 13 isKeyFrame = true - presentationTimeUs = 95250 + presentationTimeUs = 2540000 sample: trackIndex = 0 dataHashCode = -941591445 size = 13 isKeyFrame = true - presentationTimeUs = 96000 + presentationTimeUs = 2560000 sample: trackIndex = 0 dataHashCode = 1616911281 size = 13 isKeyFrame = true - presentationTimeUs = 96750 + presentationTimeUs = 2580000 sample: trackIndex = 0 dataHashCode = -1755664741 size = 13 isKeyFrame = true - presentationTimeUs = 97500 + presentationTimeUs = 2600000 sample: trackIndex = 0 dataHashCode = -1950609742 size = 13 isKeyFrame = true - presentationTimeUs = 98250 + presentationTimeUs = 2620000 sample: trackIndex = 0 dataHashCode = 1476082149 size = 13 isKeyFrame = true - presentationTimeUs = 99000 + presentationTimeUs = 2640000 sample: trackIndex = 0 dataHashCode = 1289547483 size = 13 isKeyFrame = true - presentationTimeUs = 99750 + presentationTimeUs = 2660000 sample: trackIndex = 0 dataHashCode = -367599018 size = 13 isKeyFrame = true - presentationTimeUs = 100500 + presentationTimeUs = 2680000 sample: trackIndex = 0 dataHashCode = 679378334 size = 13 isKeyFrame = true - presentationTimeUs = 101250 + presentationTimeUs = 2700000 sample: trackIndex = 0 dataHashCode = 1437306809 size = 13 isKeyFrame = true - presentationTimeUs = 102000 + presentationTimeUs = 2720000 sample: trackIndex = 0 dataHashCode = 311988463 size = 13 isKeyFrame = true - presentationTimeUs = 102750 + presentationTimeUs = 2740000 sample: trackIndex = 0 dataHashCode = -1870442665 size = 13 isKeyFrame = true - presentationTimeUs = 103500 + presentationTimeUs = 2760000 sample: trackIndex = 0 dataHashCode = 1530013920 size = 13 isKeyFrame = true - presentationTimeUs = 104250 + presentationTimeUs = 2780000 sample: trackIndex = 0 dataHashCode = -585506443 size = 13 isKeyFrame = true - presentationTimeUs = 105000 + presentationTimeUs = 2800000 sample: trackIndex = 0 dataHashCode = -293690558 size = 13 isKeyFrame = true - presentationTimeUs = 105750 + presentationTimeUs = 2820000 sample: trackIndex = 0 dataHashCode = -616893325 size = 13 isKeyFrame = true - presentationTimeUs = 106500 + presentationTimeUs = 2840000 sample: trackIndex = 0 dataHashCode = 632210495 size = 13 isKeyFrame = true - presentationTimeUs = 107250 + presentationTimeUs = 2860000 sample: trackIndex = 0 dataHashCode = -291767937 size = 13 isKeyFrame = true - presentationTimeUs = 108000 + presentationTimeUs = 2880000 sample: trackIndex = 0 dataHashCode = -270265 size = 13 isKeyFrame = true - presentationTimeUs = 108750 + presentationTimeUs = 2900000 sample: trackIndex = 0 dataHashCode = -1095959376 size = 13 isKeyFrame = true - presentationTimeUs = 109500 + presentationTimeUs = 2920000 sample: trackIndex = 0 dataHashCode = -1363867284 size = 13 isKeyFrame = true - presentationTimeUs = 110250 + presentationTimeUs = 2940000 sample: trackIndex = 0 dataHashCode = 185415707 size = 13 isKeyFrame = true - presentationTimeUs = 111000 + presentationTimeUs = 2960000 sample: trackIndex = 0 dataHashCode = 1033720098 size = 13 isKeyFrame = true - presentationTimeUs = 111750 + presentationTimeUs = 2980000 sample: trackIndex = 0 dataHashCode = 1813896085 size = 13 isKeyFrame = true - presentationTimeUs = 112500 + presentationTimeUs = 3000000 sample: trackIndex = 0 dataHashCode = -1381192241 size = 13 isKeyFrame = true - presentationTimeUs = 113250 + presentationTimeUs = 3020000 sample: trackIndex = 0 dataHashCode = 362689054 size = 13 isKeyFrame = true - presentationTimeUs = 114000 + presentationTimeUs = 3040000 sample: trackIndex = 0 dataHashCode = -1320787356 size = 13 isKeyFrame = true - presentationTimeUs = 114750 + presentationTimeUs = 3060000 sample: trackIndex = 0 dataHashCode = 1306489379 size = 13 isKeyFrame = true - presentationTimeUs = 115500 + presentationTimeUs = 3080000 sample: trackIndex = 0 dataHashCode = -910313430 size = 13 isKeyFrame = true - presentationTimeUs = 116250 + presentationTimeUs = 3100000 sample: trackIndex = 0 dataHashCode = -1533334115 size = 13 isKeyFrame = true - presentationTimeUs = 117000 + presentationTimeUs = 3120000 sample: trackIndex = 0 dataHashCode = -700061723 size = 13 isKeyFrame = true - presentationTimeUs = 117750 + presentationTimeUs = 3140000 sample: trackIndex = 0 dataHashCode = 474100444 size = 13 isKeyFrame = true - presentationTimeUs = 118500 + presentationTimeUs = 3160000 sample: trackIndex = 0 dataHashCode = -2096659943 size = 13 isKeyFrame = true - presentationTimeUs = 119250 + presentationTimeUs = 3180000 sample: trackIndex = 0 dataHashCode = -690442126 size = 13 isKeyFrame = true - presentationTimeUs = 120000 + presentationTimeUs = 3200000 sample: trackIndex = 0 dataHashCode = 158718784 size = 13 isKeyFrame = true - presentationTimeUs = 120750 + presentationTimeUs = 3220000 sample: trackIndex = 0 dataHashCode = -1587553019 size = 13 isKeyFrame = true - presentationTimeUs = 121500 + presentationTimeUs = 3240000 sample: trackIndex = 0 dataHashCode = 1266916929 size = 13 isKeyFrame = true - presentationTimeUs = 122250 + presentationTimeUs = 3260000 sample: trackIndex = 0 dataHashCode = 1947792537 size = 13 isKeyFrame = true - presentationTimeUs = 123000 + presentationTimeUs = 3280000 sample: trackIndex = 0 dataHashCode = 2051622372 size = 13 isKeyFrame = true - presentationTimeUs = 123750 + presentationTimeUs = 3300000 sample: trackIndex = 0 dataHashCode = 1648973196 size = 13 isKeyFrame = true - presentationTimeUs = 124500 + presentationTimeUs = 3320000 sample: trackIndex = 0 dataHashCode = -1119069213 size = 13 isKeyFrame = true - presentationTimeUs = 125250 + presentationTimeUs = 3340000 sample: trackIndex = 0 dataHashCode = -1162670307 size = 13 isKeyFrame = true - presentationTimeUs = 126000 + presentationTimeUs = 3360000 sample: trackIndex = 0 dataHashCode = 505180178 size = 13 isKeyFrame = true - presentationTimeUs = 126750 + presentationTimeUs = 3380000 sample: trackIndex = 0 dataHashCode = -1707111799 size = 13 isKeyFrame = true - presentationTimeUs = 127500 + presentationTimeUs = 3400000 sample: trackIndex = 0 dataHashCode = 549350779 size = 13 isKeyFrame = true - presentationTimeUs = 128250 + presentationTimeUs = 3420000 sample: trackIndex = 0 dataHashCode = -895461091 size = 13 isKeyFrame = true - presentationTimeUs = 129000 + presentationTimeUs = 3440000 sample: trackIndex = 0 dataHashCode = 1834306839 size = 13 isKeyFrame = true - presentationTimeUs = 129750 + presentationTimeUs = 3460000 sample: trackIndex = 0 dataHashCode = -646169807 size = 13 isKeyFrame = true - presentationTimeUs = 130500 + presentationTimeUs = 3480000 sample: trackIndex = 0 dataHashCode = 123454915 size = 13 isKeyFrame = true - presentationTimeUs = 131250 + presentationTimeUs = 3500000 sample: trackIndex = 0 dataHashCode = 2074179659 size = 13 isKeyFrame = true - presentationTimeUs = 132000 + presentationTimeUs = 3520000 sample: trackIndex = 0 dataHashCode = 488070546 size = 13 isKeyFrame = true - presentationTimeUs = 132750 + presentationTimeUs = 3540000 sample: trackIndex = 0 dataHashCode = -1379245827 size = 13 isKeyFrame = true - presentationTimeUs = 133500 + presentationTimeUs = 3560000 sample: trackIndex = 0 dataHashCode = 922846867 size = 13 isKeyFrame = true - presentationTimeUs = 134250 + presentationTimeUs = 3580000 sample: trackIndex = 0 dataHashCode = 1163092079 size = 13 isKeyFrame = true - presentationTimeUs = 135000 + presentationTimeUs = 3600000 sample: trackIndex = 0 dataHashCode = -817674907 size = 13 isKeyFrame = true - presentationTimeUs = 135750 + presentationTimeUs = 3620000 sample: trackIndex = 0 dataHashCode = -765143209 size = 13 isKeyFrame = true - presentationTimeUs = 136500 + presentationTimeUs = 3640000 sample: trackIndex = 0 dataHashCode = 1337234415 size = 13 isKeyFrame = true - presentationTimeUs = 137250 + presentationTimeUs = 3660000 sample: trackIndex = 0 dataHashCode = 152696122 size = 13 isKeyFrame = true - presentationTimeUs = 138000 + presentationTimeUs = 3680000 sample: trackIndex = 0 dataHashCode = -1037369189 size = 13 isKeyFrame = true - presentationTimeUs = 138750 + presentationTimeUs = 3700000 sample: trackIndex = 0 dataHashCode = 93852784 size = 13 isKeyFrame = true - presentationTimeUs = 139500 + presentationTimeUs = 3720000 sample: trackIndex = 0 dataHashCode = -1512860804 size = 13 isKeyFrame = true - presentationTimeUs = 140250 + presentationTimeUs = 3740000 sample: trackIndex = 0 dataHashCode = -1571797975 size = 13 isKeyFrame = true - presentationTimeUs = 141000 + presentationTimeUs = 3760000 sample: trackIndex = 0 dataHashCode = -1390710594 size = 13 isKeyFrame = true - presentationTimeUs = 141750 + presentationTimeUs = 3780000 sample: trackIndex = 0 dataHashCode = 775548254 size = 13 isKeyFrame = true - presentationTimeUs = 142500 + presentationTimeUs = 3800000 sample: trackIndex = 0 dataHashCode = 329825934 size = 13 isKeyFrame = true - presentationTimeUs = 143250 + presentationTimeUs = 3820000 sample: trackIndex = 0 dataHashCode = 449672203 size = 13 isKeyFrame = true - presentationTimeUs = 144000 + presentationTimeUs = 3840000 sample: trackIndex = 0 dataHashCode = 135215283 size = 13 isKeyFrame = true - presentationTimeUs = 144750 + presentationTimeUs = 3860000 sample: trackIndex = 0 dataHashCode = -627202145 size = 13 isKeyFrame = true - presentationTimeUs = 145500 + presentationTimeUs = 3880000 sample: trackIndex = 0 dataHashCode = 565795710 size = 13 isKeyFrame = true - presentationTimeUs = 146250 + presentationTimeUs = 3900000 sample: trackIndex = 0 dataHashCode = -853390981 size = 13 isKeyFrame = true - presentationTimeUs = 147000 + presentationTimeUs = 3920000 sample: trackIndex = 0 dataHashCode = 1904980829 size = 13 isKeyFrame = true - presentationTimeUs = 147750 + presentationTimeUs = 3940000 sample: trackIndex = 0 dataHashCode = 1772857005 size = 13 isKeyFrame = true - presentationTimeUs = 148500 + presentationTimeUs = 3960000 sample: trackIndex = 0 dataHashCode = -1159621303 size = 13 isKeyFrame = true - presentationTimeUs = 149250 + presentationTimeUs = 3980000 sample: trackIndex = 0 dataHashCode = 712585139 size = 13 isKeyFrame = true - presentationTimeUs = 150000 + presentationTimeUs = 4000000 sample: trackIndex = 0 dataHashCode = 7470296 size = 13 isKeyFrame = true - presentationTimeUs = 150750 + presentationTimeUs = 4020000 sample: trackIndex = 0 dataHashCode = 1154659763 size = 13 isKeyFrame = true - presentationTimeUs = 151500 + presentationTimeUs = 4040000 sample: trackIndex = 0 dataHashCode = 512209179 size = 13 isKeyFrame = true - presentationTimeUs = 152250 + presentationTimeUs = 4060000 sample: trackIndex = 0 dataHashCode = 2026712081 size = 13 isKeyFrame = true - presentationTimeUs = 153000 + presentationTimeUs = 4080000 sample: trackIndex = 0 dataHashCode = -1625715216 size = 13 isKeyFrame = true - presentationTimeUs = 153750 + presentationTimeUs = 4100000 sample: trackIndex = 0 dataHashCode = -1299058326 size = 13 isKeyFrame = true - presentationTimeUs = 154500 + presentationTimeUs = 4120000 sample: trackIndex = 0 dataHashCode = -813560096 size = 13 isKeyFrame = true - presentationTimeUs = 155250 + presentationTimeUs = 4140000 sample: trackIndex = 0 dataHashCode = 1311045251 size = 13 isKeyFrame = true - presentationTimeUs = 156000 + presentationTimeUs = 4160000 sample: trackIndex = 0 dataHashCode = 1388107407 size = 13 isKeyFrame = true - presentationTimeUs = 156750 + presentationTimeUs = 4180000 sample: trackIndex = 0 dataHashCode = 1113099440 size = 13 isKeyFrame = true - presentationTimeUs = 157500 + presentationTimeUs = 4200000 sample: trackIndex = 0 dataHashCode = -339743582 size = 13 isKeyFrame = true - presentationTimeUs = 158250 + presentationTimeUs = 4220000 sample: trackIndex = 0 dataHashCode = -1055895345 size = 13 isKeyFrame = true - presentationTimeUs = 159000 + presentationTimeUs = 4240000 sample: trackIndex = 0 dataHashCode = 1869841923 size = 13 isKeyFrame = true - presentationTimeUs = 159750 + presentationTimeUs = 4260000 sample: trackIndex = 0 dataHashCode = 229443301 size = 13 isKeyFrame = true - presentationTimeUs = 160500 + presentationTimeUs = 4280000 sample: trackIndex = 0 dataHashCode = 1526951012 size = 13 isKeyFrame = true - presentationTimeUs = 161250 + presentationTimeUs = 4300000 sample: trackIndex = 0 dataHashCode = -1517436626 size = 13 isKeyFrame = true - presentationTimeUs = 162000 + presentationTimeUs = 4320000 sample: trackIndex = 0 dataHashCode = -1403405700 size = 13 isKeyFrame = true - presentationTimeUs = 162750 + presentationTimeUs = 4340000 released = true diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump index c5991e7a4b..7b6604be43 100644 --- a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump +++ b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump @@ -1,9 +1,15 @@ containerMimeType = video/mp4 format 0: + id = 2 sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 channelCount = 1 sampleRate = 44100 - pcmEncoding = 2 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 format 1: id = 1 sampleMimeType = video/avc @@ -110,271 +116,127 @@ sample: dataHashCode = 1205768497 size = 23 isKeyFrame = true - presentationTimeUs = 0 + presentationTimeUs = 44000 sample: trackIndex = 0 dataHashCode = 837571078 size = 6 isKeyFrame = true - presentationTimeUs = 249 + presentationTimeUs = 67219 sample: trackIndex = 0 dataHashCode = -1991633045 size = 148 isKeyFrame = true - presentationTimeUs = 317 + presentationTimeUs = 90439 sample: trackIndex = 0 dataHashCode = -822987359 size = 189 isKeyFrame = true - presentationTimeUs = 1995 + presentationTimeUs = 113659 sample: trackIndex = 0 dataHashCode = -1141508176 size = 205 isKeyFrame = true - presentationTimeUs = 4126 + presentationTimeUs = 136879 sample: trackIndex = 0 dataHashCode = -226971245 size = 210 isKeyFrame = true - presentationTimeUs = 6438 + presentationTimeUs = 160099 sample: trackIndex = 0 dataHashCode = -2099636855 size = 210 isKeyFrame = true - presentationTimeUs = 8818 + presentationTimeUs = 183319 sample: trackIndex = 0 dataHashCode = 1541550559 size = 207 isKeyFrame = true - presentationTimeUs = 11198 + presentationTimeUs = 206539 sample: trackIndex = 0 dataHashCode = 411148001 size = 225 isKeyFrame = true - presentationTimeUs = 13533 + presentationTimeUs = 229759 sample: trackIndex = 0 dataHashCode = -897603973 size = 215 isKeyFrame = true - presentationTimeUs = 16072 + presentationTimeUs = 252979 sample: trackIndex = 0 dataHashCode = 1478106136 size = 211 isKeyFrame = true - presentationTimeUs = 18498 + presentationTimeUs = 276199 sample: trackIndex = 0 dataHashCode = -1380417145 size = 216 isKeyFrame = true - presentationTimeUs = 20878 + presentationTimeUs = 299419 sample: trackIndex = 0 dataHashCode = 780903644 size = 229 isKeyFrame = true - presentationTimeUs = 23326 + presentationTimeUs = 322639 sample: trackIndex = 0 dataHashCode = 586204432 size = 232 isKeyFrame = true - presentationTimeUs = 25911 + presentationTimeUs = 345859 sample: trackIndex = 0 dataHashCode = -2038771492 size = 235 isKeyFrame = true - presentationTimeUs = 28541 + presentationTimeUs = 369079 sample: trackIndex = 0 dataHashCode = -2065161304 size = 231 isKeyFrame = true - presentationTimeUs = 31194 + presentationTimeUs = 392299 sample: trackIndex = 0 dataHashCode = 468662933 size = 226 isKeyFrame = true - presentationTimeUs = 33801 + presentationTimeUs = 415519 sample: trackIndex = 0 dataHashCode = -358398546 size = 216 isKeyFrame = true - presentationTimeUs = 36363 + presentationTimeUs = 438739 sample: trackIndex = 0 dataHashCode = 1767325983 size = 229 isKeyFrame = true - presentationTimeUs = 38811 + presentationTimeUs = 461959 sample: trackIndex = 0 dataHashCode = 1093095458 size = 219 isKeyFrame = true - presentationTimeUs = 41396 + presentationTimeUs = 485179 sample: trackIndex = 0 dataHashCode = 1687543702 size = 241 isKeyFrame = true - presentationTimeUs = 43867 -sample: - trackIndex = 0 - dataHashCode = 1675188486 - size = 228 - isKeyFrame = true - presentationTimeUs = 46588 -sample: - trackIndex = 0 - dataHashCode = 888567545 - size = 238 - isKeyFrame = true - presentationTimeUs = 49173 -sample: - trackIndex = 0 - dataHashCode = -439631803 - size = 234 - isKeyFrame = true - presentationTimeUs = 51871 -sample: - trackIndex = 0 - dataHashCode = 1606694497 - size = 231 - isKeyFrame = true - presentationTimeUs = 54524 -sample: - trackIndex = 0 - dataHashCode = 1747388653 - size = 217 - isKeyFrame = true - presentationTimeUs = 57131 -sample: - trackIndex = 0 - dataHashCode = -734560004 - size = 239 - isKeyFrame = true - presentationTimeUs = 59579 -sample: - trackIndex = 0 - dataHashCode = -975079040 - size = 243 - isKeyFrame = true - presentationTimeUs = 62277 -sample: - trackIndex = 0 - dataHashCode = -1403504710 - size = 231 - isKeyFrame = true - presentationTimeUs = 65020 -sample: - trackIndex = 0 - dataHashCode = 379512981 - size = 230 - isKeyFrame = true - presentationTimeUs = 67627 -sample: - trackIndex = 0 - dataHashCode = -997198863 - size = 238 - isKeyFrame = true - presentationTimeUs = 70234 -sample: - trackIndex = 0 - dataHashCode = 1394492825 - size = 225 - isKeyFrame = true - presentationTimeUs = 72932 -sample: - trackIndex = 0 - dataHashCode = -885232755 - size = 232 - isKeyFrame = true - presentationTimeUs = 75471 -sample: - trackIndex = 0 - dataHashCode = 260871367 - size = 243 - isKeyFrame = true - presentationTimeUs = 78101 -sample: - trackIndex = 0 - dataHashCode = -1505318960 - size = 232 - isKeyFrame = true - presentationTimeUs = 80844 -sample: - trackIndex = 0 - dataHashCode = -390625371 - size = 237 - isKeyFrame = true - presentationTimeUs = 83474 -sample: - trackIndex = 0 - dataHashCode = 1067950751 - size = 228 - isKeyFrame = true - presentationTimeUs = 86149 -sample: - trackIndex = 0 - dataHashCode = -1179436278 - size = 235 - isKeyFrame = true - presentationTimeUs = 88734 -sample: - trackIndex = 0 - dataHashCode = 1906607774 - size = 264 - isKeyFrame = true - presentationTimeUs = 91387 -sample: - trackIndex = 0 - dataHashCode = -800475828 - size = 257 - isKeyFrame = true - presentationTimeUs = 94380 -sample: - trackIndex = 0 - dataHashCode = 1718972977 - size = 227 - isKeyFrame = true - presentationTimeUs = 97282 -sample: - trackIndex = 0 - dataHashCode = -1120448741 - size = 227 - isKeyFrame = true - presentationTimeUs = 99844 -sample: - trackIndex = 0 - dataHashCode = -1718323210 - size = 235 - isKeyFrame = true - presentationTimeUs = 102406 -sample: - trackIndex = 0 - dataHashCode = -422416 - size = 229 - isKeyFrame = true - presentationTimeUs = 105059 -sample: - trackIndex = 0 - dataHashCode = 833757830 - size = 6 - isKeyFrame = true - presentationTimeUs = 107644 + presentationTimeUs = 508399 sample: trackIndex = 1 dataHashCode = -1830836678 @@ -465,4 +327,148 @@ sample: size = 568 isKeyFrame = false presentationTimeUs = 934266 +sample: + trackIndex = 0 + dataHashCode = 1675188486 + size = 228 + isKeyFrame = true + presentationTimeUs = 531619 +sample: + trackIndex = 0 + dataHashCode = 888567545 + size = 238 + isKeyFrame = true + presentationTimeUs = 554839 +sample: + trackIndex = 0 + dataHashCode = -439631803 + size = 234 + isKeyFrame = true + presentationTimeUs = 578058 +sample: + trackIndex = 0 + dataHashCode = 1606694497 + size = 231 + isKeyFrame = true + presentationTimeUs = 601278 +sample: + trackIndex = 0 + dataHashCode = 1747388653 + size = 217 + isKeyFrame = true + presentationTimeUs = 624498 +sample: + trackIndex = 0 + dataHashCode = -734560004 + size = 239 + isKeyFrame = true + presentationTimeUs = 647718 +sample: + trackIndex = 0 + dataHashCode = -975079040 + size = 243 + isKeyFrame = true + presentationTimeUs = 670938 +sample: + trackIndex = 0 + dataHashCode = -1403504710 + size = 231 + isKeyFrame = true + presentationTimeUs = 694158 +sample: + trackIndex = 0 + dataHashCode = 379512981 + size = 230 + isKeyFrame = true + presentationTimeUs = 717378 +sample: + trackIndex = 0 + dataHashCode = -997198863 + size = 238 + isKeyFrame = true + presentationTimeUs = 740598 +sample: + trackIndex = 0 + dataHashCode = 1394492825 + size = 225 + isKeyFrame = true + presentationTimeUs = 763818 +sample: + trackIndex = 0 + dataHashCode = -885232755 + size = 232 + isKeyFrame = true + presentationTimeUs = 787038 +sample: + trackIndex = 0 + dataHashCode = 260871367 + size = 243 + isKeyFrame = true + presentationTimeUs = 810258 +sample: + trackIndex = 0 + dataHashCode = -1505318960 + size = 232 + isKeyFrame = true + presentationTimeUs = 833478 +sample: + trackIndex = 0 + dataHashCode = -390625371 + size = 237 + isKeyFrame = true + presentationTimeUs = 856698 +sample: + trackIndex = 0 + dataHashCode = 1067950751 + size = 228 + isKeyFrame = true + presentationTimeUs = 879918 +sample: + trackIndex = 0 + dataHashCode = -1179436278 + size = 235 + isKeyFrame = true + presentationTimeUs = 903138 +sample: + trackIndex = 0 + dataHashCode = 1906607774 + size = 264 + isKeyFrame = true + presentationTimeUs = 926358 +sample: + trackIndex = 0 + dataHashCode = -800475828 + size = 257 + isKeyFrame = true + presentationTimeUs = 949578 +sample: + trackIndex = 0 + dataHashCode = 1718972977 + size = 227 + isKeyFrame = true + presentationTimeUs = 972798 +sample: + trackIndex = 0 + dataHashCode = -1120448741 + size = 227 + isKeyFrame = true + presentationTimeUs = 996018 +sample: + trackIndex = 0 + dataHashCode = -1718323210 + size = 235 + isKeyFrame = true + presentationTimeUs = 1019238 +sample: + trackIndex = 0 + dataHashCode = -422416 + size = 229 + isKeyFrame = true + presentationTimeUs = 1042458 +sample: + trackIndex = 0 + dataHashCode = 833757830 + size = 6 + isKeyFrame = true + presentationTimeUs = 1065678 released = true diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump index e94ff8bb7f..adc14a43a1 100644 --- a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump +++ b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump @@ -1,277 +1,283 @@ containerMimeType = video/mp4 format 0: + id = 2 sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 channelCount = 1 sampleRate = 44100 - pcmEncoding = 2 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 sample: trackIndex = 0 dataHashCode = 1205768497 size = 23 isKeyFrame = true - presentationTimeUs = 0 + presentationTimeUs = 44000 sample: trackIndex = 0 dataHashCode = 837571078 size = 6 isKeyFrame = true - presentationTimeUs = 249 + presentationTimeUs = 67219 sample: trackIndex = 0 dataHashCode = -1991633045 size = 148 isKeyFrame = true - presentationTimeUs = 317 + presentationTimeUs = 90439 sample: trackIndex = 0 dataHashCode = -822987359 size = 189 isKeyFrame = true - presentationTimeUs = 1995 + presentationTimeUs = 113659 sample: trackIndex = 0 dataHashCode = -1141508176 size = 205 isKeyFrame = true - presentationTimeUs = 4126 + presentationTimeUs = 136879 sample: trackIndex = 0 dataHashCode = -226971245 size = 210 isKeyFrame = true - presentationTimeUs = 6438 + presentationTimeUs = 160099 sample: trackIndex = 0 dataHashCode = -2099636855 size = 210 isKeyFrame = true - presentationTimeUs = 8818 + presentationTimeUs = 183319 sample: trackIndex = 0 dataHashCode = 1541550559 size = 207 isKeyFrame = true - presentationTimeUs = 11198 + presentationTimeUs = 206539 sample: trackIndex = 0 dataHashCode = 411148001 size = 225 isKeyFrame = true - presentationTimeUs = 13533 + presentationTimeUs = 229759 sample: trackIndex = 0 dataHashCode = -897603973 size = 215 isKeyFrame = true - presentationTimeUs = 16072 + presentationTimeUs = 252979 sample: trackIndex = 0 dataHashCode = 1478106136 size = 211 isKeyFrame = true - presentationTimeUs = 18498 + presentationTimeUs = 276199 sample: trackIndex = 0 dataHashCode = -1380417145 size = 216 isKeyFrame = true - presentationTimeUs = 20878 + presentationTimeUs = 299419 sample: trackIndex = 0 dataHashCode = 780903644 size = 229 isKeyFrame = true - presentationTimeUs = 23326 + presentationTimeUs = 322639 sample: trackIndex = 0 dataHashCode = 586204432 size = 232 isKeyFrame = true - presentationTimeUs = 25911 + presentationTimeUs = 345859 sample: trackIndex = 0 dataHashCode = -2038771492 size = 235 isKeyFrame = true - presentationTimeUs = 28541 + presentationTimeUs = 369079 sample: trackIndex = 0 dataHashCode = -2065161304 size = 231 isKeyFrame = true - presentationTimeUs = 31194 + presentationTimeUs = 392299 sample: trackIndex = 0 dataHashCode = 468662933 size = 226 isKeyFrame = true - presentationTimeUs = 33801 + presentationTimeUs = 415519 sample: trackIndex = 0 dataHashCode = -358398546 size = 216 isKeyFrame = true - presentationTimeUs = 36363 + presentationTimeUs = 438739 sample: trackIndex = 0 dataHashCode = 1767325983 size = 229 isKeyFrame = true - presentationTimeUs = 38811 + presentationTimeUs = 461959 sample: trackIndex = 0 dataHashCode = 1093095458 size = 219 isKeyFrame = true - presentationTimeUs = 41396 + presentationTimeUs = 485179 sample: trackIndex = 0 dataHashCode = 1687543702 size = 241 isKeyFrame = true - presentationTimeUs = 43867 + presentationTimeUs = 508399 sample: trackIndex = 0 dataHashCode = 1675188486 size = 228 isKeyFrame = true - presentationTimeUs = 46588 + presentationTimeUs = 531619 sample: trackIndex = 0 dataHashCode = 888567545 size = 238 isKeyFrame = true - presentationTimeUs = 49173 + presentationTimeUs = 554839 sample: trackIndex = 0 dataHashCode = -439631803 size = 234 isKeyFrame = true - presentationTimeUs = 51871 + presentationTimeUs = 578058 sample: trackIndex = 0 dataHashCode = 1606694497 size = 231 isKeyFrame = true - presentationTimeUs = 54524 + presentationTimeUs = 601278 sample: trackIndex = 0 dataHashCode = 1747388653 size = 217 isKeyFrame = true - presentationTimeUs = 57131 + presentationTimeUs = 624498 sample: trackIndex = 0 dataHashCode = -734560004 size = 239 isKeyFrame = true - presentationTimeUs = 59579 + presentationTimeUs = 647718 sample: trackIndex = 0 dataHashCode = -975079040 size = 243 isKeyFrame = true - presentationTimeUs = 62277 + presentationTimeUs = 670938 sample: trackIndex = 0 dataHashCode = -1403504710 size = 231 isKeyFrame = true - presentationTimeUs = 65020 + presentationTimeUs = 694158 sample: trackIndex = 0 dataHashCode = 379512981 size = 230 isKeyFrame = true - presentationTimeUs = 67627 + presentationTimeUs = 717378 sample: trackIndex = 0 dataHashCode = -997198863 size = 238 isKeyFrame = true - presentationTimeUs = 70234 + presentationTimeUs = 740598 sample: trackIndex = 0 dataHashCode = 1394492825 size = 225 isKeyFrame = true - presentationTimeUs = 72932 + presentationTimeUs = 763818 sample: trackIndex = 0 dataHashCode = -885232755 size = 232 isKeyFrame = true - presentationTimeUs = 75471 + presentationTimeUs = 787038 sample: trackIndex = 0 dataHashCode = 260871367 size = 243 isKeyFrame = true - presentationTimeUs = 78101 + presentationTimeUs = 810258 sample: trackIndex = 0 dataHashCode = -1505318960 size = 232 isKeyFrame = true - presentationTimeUs = 80844 + presentationTimeUs = 833478 sample: trackIndex = 0 dataHashCode = -390625371 size = 237 isKeyFrame = true - presentationTimeUs = 83474 + presentationTimeUs = 856698 sample: trackIndex = 0 dataHashCode = 1067950751 size = 228 isKeyFrame = true - presentationTimeUs = 86149 + presentationTimeUs = 879918 sample: trackIndex = 0 dataHashCode = -1179436278 size = 235 isKeyFrame = true - presentationTimeUs = 88734 + presentationTimeUs = 903138 sample: trackIndex = 0 dataHashCode = 1906607774 size = 264 isKeyFrame = true - presentationTimeUs = 91387 + presentationTimeUs = 926358 sample: trackIndex = 0 dataHashCode = -800475828 size = 257 isKeyFrame = true - presentationTimeUs = 94380 + presentationTimeUs = 949578 sample: trackIndex = 0 dataHashCode = 1718972977 size = 227 isKeyFrame = true - presentationTimeUs = 97282 + presentationTimeUs = 972798 sample: trackIndex = 0 dataHashCode = -1120448741 size = 227 isKeyFrame = true - presentationTimeUs = 99844 + presentationTimeUs = 996018 sample: trackIndex = 0 dataHashCode = -1718323210 size = 235 isKeyFrame = true - presentationTimeUs = 102406 + presentationTimeUs = 1019238 sample: trackIndex = 0 dataHashCode = -422416 size = 229 isKeyFrame = true - presentationTimeUs = 105059 + presentationTimeUs = 1042458 sample: trackIndex = 0 dataHashCode = 833757830 size = 6 isKeyFrame = true - presentationTimeUs = 107644 + presentationTimeUs = 1065678 released = true From 0b48570bb30c1ef15e930ffb358ca0ba35d6b15a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 5 Nov 2021 11:34:33 +0000 Subject: [PATCH 10/41] Fix track selection with mixed empty/non-empty overrides When we have multiple overrides for TrackGroups associated with one renderer, we need to look at all of them to find the non-empty one. Empty ones should only be used to remove previously selected tracks for this group and otherwise be ignored. Currently this is broken because the first override (no matter if it's empty or not) is used as the final selection for this renderer. Issue: google/ExoPlayer#9649 #minor-release PiperOrigin-RevId: 407792330 --- RELEASENOTES.md | 3 ++ .../trackselection/DefaultTrackSelector.java | 19 +++++++--- .../DefaultTrackSelectorTest.java | 37 +++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b53b2f6226..7c278fa114 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,9 @@ ### dev-v2 (not yet released) * Core Library: + * Fix track selection issue where a mixture of non-empty and empty track + overrides is not applied correctly + ([#9649](https://github.com/google/ExoPlayer/issues/9649). * Add protected method `DefaultRenderersFactory.getCodecAdapterFactory()` so that subclasses of `DefaultRenderersFactory` that override `buildVideoRenderers()` or `buildAudioRenderers()` can access the codec diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 3e0ed7e887..600a387796 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -1553,7 +1553,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { || params.disabledTrackTypes.contains(rendererType)) { return null; } - // Per TrackGroupArray override + // Per TrackGroupArray overrides. TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); if (params.hasSelectionOverride(rendererIndex, rendererTrackGroups)) { @Nullable @@ -1564,18 +1564,27 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new ExoTrackSelection.Definition( rendererTrackGroups.get(override.groupIndex), override.tracks, override.type); } - // Per TrackGroup override + // Per TrackGroup overrides. for (int j = 0; j < rendererTrackGroups.length; j++) { TrackGroup trackGroup = rendererTrackGroups.get(j); @Nullable TrackSelectionOverride overrideTracks = params.trackSelectionOverrides.getOverride(trackGroup); if (overrideTracks != null) { - return new ExoTrackSelection.Definition( - trackGroup, Ints.toArray(overrideTracks.trackIndexes)); + if (overrideTracks.trackIndexes.isEmpty()) { + // TrackGroup is disabled. Deselect the currentDefinition if applicable. Otherwise ignore. + if (currentDefinition != null && currentDefinition.group.equals(trackGroup)) { + currentDefinition = null; + } + } else { + // Override current definition with new selection. + currentDefinition = + new ExoTrackSelection.Definition( + trackGroup, Ints.toArray(overrideTracks.trackIndexes)); + } } } - return currentDefinition; // No override + return currentDefinition; } // Track selection prior to overrides and disabled flags being applied. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index c2eaf5ddaf..dcec44bc26 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -204,6 +204,43 @@ public final class DefaultTrackSelectorTest { .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT}); } + @Test + public void selectTrack_withMixedEmptyAndNonEmptyTrackOverrides_appliesNonEmptyOverride() + throws Exception { + TrackGroup videoGroupHighBitrate = + new TrackGroup(VIDEO_FORMAT.buildUpon().setAverageBitrate(1_000_000).build()); + TrackGroup videoGroupMidBitrate = + new TrackGroup(VIDEO_FORMAT.buildUpon().setAverageBitrate(500_000).build()); + TrackGroup videoGroupLowBitrate = + new TrackGroup(VIDEO_FORMAT.buildUpon().setAverageBitrate(100_000).build()); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .addOverride( + new TrackSelectionOverride( + videoGroupHighBitrate, /* trackIndexes= */ ImmutableList.of())) + .addOverride( + new TrackSelectionOverride( + videoGroupMidBitrate, /* trackIndexes= */ ImmutableList.of(0))) + .addOverride( + new TrackSelectionOverride( + videoGroupLowBitrate, /* trackIndexes= */ ImmutableList.of())) + .build())); + + TrackSelectorResult result = + trackSelector.selectTracks( + RENDERER_CAPABILITIES, + new TrackGroupArray(videoGroupHighBitrate, videoGroupMidBitrate, videoGroupLowBitrate), + periodId, + TIMELINE); + + assertThat(result.selections) + .asList() + .containsExactly(new FixedTrackSelection(videoGroupMidBitrate, /* track= */ 0), null); + } + /** Tests that an empty override is not applied for a different set of available track groups. */ @Test public void selectTracks_withEmptyTrackOverrideForDifferentTracks_hasNoEffect() From 9e1597a479a5ad21b66ed2da5b452252949febf9 Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 5 Nov 2021 12:42:20 +0000 Subject: [PATCH 11/41] Add experimental method to turn-off async flush When operating the MediaCodec in asynchronous mode, after a MediaCodec.flush(), we start MediaCodec in the callback thread, which might trigger errors in some platforms. This change adds an experimental flag to move the call to MediaCodec.start() back to the playback thread. PiperOrigin-RevId: 407801013 --- .../exoplayer2/DefaultRenderersFactory.java | 19 +++ .../AsynchronousMediaCodecAdapter.java | 43 +++--- .../AsynchronousMediaCodecCallback.java | 66 +++++---- .../DefaultMediaCodecAdapterFactory.java | 22 ++- .../AsynchronousMediaCodecAdapterTest.java | 3 +- .../AsynchronousMediaCodecCallbackTest.java | 127 ++++++++---------- 6 files changed, 154 insertions(+), 126 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index ca205c2901..97f9ae83c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -191,6 +191,25 @@ public class DefaultRenderersFactory implements RenderersFactory { return this; } + /** + * Enable calling {@link MediaCodec#start} immediately after {@link MediaCodec#flush} on the + * playback thread, when operating the codec in asynchronous mode. If disabled, {@link + * MediaCodec#start} will be called by the callback thread after pending callbacks are handled. + * + *

By default, this feature is disabled. + * + *

This method is experimental, and will be renamed or removed in a future release. + * + * @param enabled Whether {@link MediaCodec#start} will be called on the playback thread + * immediately after {@link MediaCodec#flush}. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory experimentalSetImmediateCodecStartAfterFlushEnabled( + boolean enabled) { + codecAdapterFactory.experimentalSetImmediateCodecStartAfterFlushEnabled(enabled); + return this; + } + /** * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails. * This may result in using a decoder that is less efficient or slower than the primary decoder. 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 63b4f65a4c..c56bf54f8c 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 @@ -50,11 +50,7 @@ import java.nio.ByteBuffer; private final Supplier callbackThreadSupplier; private final Supplier queueingThreadSupplier; private final boolean synchronizeCodecInteractionsWithQueueing; - - /** Creates a factory for codecs handling the specified {@link C.TrackType track type}. */ - public Factory(@C.TrackType int trackType) { - this(trackType, /* synchronizeCodecInteractionsWithQueueing= */ false); - } + private final boolean enableImmediateCodecStartAfterFlush; /** * Creates an factory for {@link AsynchronousMediaCodecAdapter} instances. @@ -66,23 +62,29 @@ import java.nio.ByteBuffer; * interactions will wait until all input buffers pending queueing wil be submitted to the * {@link MediaCodec}. */ - public Factory(@C.TrackType int trackType, boolean synchronizeCodecInteractionsWithQueueing) { + public Factory( + @C.TrackType int trackType, + boolean synchronizeCodecInteractionsWithQueueing, + boolean enableImmediateCodecStartAfterFlush) { this( /* callbackThreadSupplier= */ () -> new HandlerThread(createCallbackThreadLabel(trackType)), /* queueingThreadSupplier= */ () -> new HandlerThread(createQueueingThreadLabel(trackType)), - synchronizeCodecInteractionsWithQueueing); + synchronizeCodecInteractionsWithQueueing, + enableImmediateCodecStartAfterFlush); } @VisibleForTesting /* package */ Factory( Supplier callbackThreadSupplier, Supplier queueingThreadSupplier, - boolean synchronizeCodecInteractionsWithQueueing) { + boolean synchronizeCodecInteractionsWithQueueing, + boolean enableImmediateCodecStartAfterFlush) { this.callbackThreadSupplier = callbackThreadSupplier; this.queueingThreadSupplier = queueingThreadSupplier; this.synchronizeCodecInteractionsWithQueueing = synchronizeCodecInteractionsWithQueueing; + this.enableImmediateCodecStartAfterFlush = enableImmediateCodecStartAfterFlush; } @Override @@ -99,7 +101,8 @@ import java.nio.ByteBuffer; codec, callbackThreadSupplier.get(), queueingThreadSupplier.get(), - synchronizeCodecInteractionsWithQueueing); + synchronizeCodecInteractionsWithQueueing, + enableImmediateCodecStartAfterFlush); TraceUtil.endSection(); codecAdapter.initialize( configuration.mediaFormat, @@ -132,6 +135,7 @@ import java.nio.ByteBuffer; private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback; private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer; private final boolean synchronizeCodecInteractionsWithQueueing; + private final boolean enableImmediateCodecStartAfterFlush; private boolean codecReleased; @State private int state; @Nullable private Surface inputSurface; @@ -140,11 +144,13 @@ import java.nio.ByteBuffer; MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread, - boolean synchronizeCodecInteractionsWithQueueing) { + boolean synchronizeCodecInteractionsWithQueueing, + boolean enableImmediateCodecStartAfterFlush) { this.codec = codec; this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread); this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread); this.synchronizeCodecInteractionsWithQueueing = synchronizeCodecInteractionsWithQueueing; + this.enableImmediateCodecStartAfterFlush = enableImmediateCodecStartAfterFlush; this.state = STATE_CREATED; } @@ -231,13 +237,20 @@ import java.nio.ByteBuffer; @Override public void flush() { // The order of calls is important: - // First, flush the bufferEnqueuer to stop queueing input buffers. - // Second, flush the codec to stop producing available input/output buffers. - // Third, flush the callback after flushing the codec so that in-flight callbacks are discarded. + // 1. Flush the bufferEnqueuer to stop queueing input buffers. + // 2. Flush the codec to stop producing available input/output buffers. + // 3. Flush the callback after flushing the codec so that in-flight callbacks are discarded. bufferEnqueuer.flush(); codec.flush(); - // When flushAsync() is completed, start the codec again. - asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ codec::start); + if (enableImmediateCodecStartAfterFlush) { + // The asynchronous callback will drop pending callbacks but we can start the codec now. + asynchronousMediaCodecCallback.flush(/* codec= */ null); + codec.start(); + } else { + // Let the asynchronous callback start the codec in the callback thread after pending + // callbacks are handled. + asynchronousMediaCodecCallback.flush(codec); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java index b6fe773f4f..be44abb290 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java @@ -34,8 +34,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @RequiresApi(23) /* package */ final class AsynchronousMediaCodecCallback extends MediaCodec.Callback { private final Object lock; - private final HandlerThread callbackThread; + private @MonotonicNonNull Handler handler; @GuardedBy("lock") @@ -192,14 +192,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * Initiates a flush asynchronously, which will be completed on the callback thread. When the * flush is complete, it will trigger {@code onFlushCompleted} from the callback thread. * - * @param onFlushCompleted A {@link Runnable} that will be called when flush is completed. {@code - * onFlushCompleted} will be called from the scallback thread, therefore it should execute - * synchronized and thread-safe code. + * @param codec A {@link MediaCodec} to {@link MediaCodec#start start} after all pending callbacks + * are handled, or {@code null} if starting the {@link MediaCodec} is performed elsewhere. */ - public void flushAsync(Runnable onFlushCompleted) { + public void flush(@Nullable MediaCodec codec) { synchronized (lock) { ++pendingFlushCount; - Util.castNonNull(handler).post(() -> this.onFlushCompleted(onFlushCompleted)); + Util.castNonNull(handler).post(() -> this.onFlushCompleted(codec)); } } @@ -239,34 +238,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - private void onFlushCompleted(Runnable onFlushCompleted) { + private void onFlushCompleted(@Nullable MediaCodec codec) { synchronized (lock) { - onFlushCompletedSynchronized(onFlushCompleted); - } - } + if (shutDown) { + return; + } - @GuardedBy("lock") - private void onFlushCompletedSynchronized(Runnable onFlushCompleted) { - if (shutDown) { - return; - } - - --pendingFlushCount; - if (pendingFlushCount > 0) { - // Another flush() has been called. - return; - } else if (pendingFlushCount < 0) { - // This should never happen. - setInternalException(new IllegalStateException()); - return; - } - flushInternal(); - try { - onFlushCompleted.run(); - } catch (IllegalStateException e) { - setInternalException(e); - } catch (Exception e) { - setInternalException(new IllegalStateException(e)); + --pendingFlushCount; + if (pendingFlushCount > 0) { + // Another flush() has been called. + return; + } else if (pendingFlushCount < 0) { + // This should never happen. + setInternalException(new IllegalStateException()); + return; + } + flushInternal(); + if (codec != null) { + try { + codec.start(); + } catch (IllegalStateException e) { + setInternalException(e); + } catch (Exception e) { + setInternalException(new IllegalStateException(e)); + } + } } } @@ -275,10 +271,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private void flushInternal() { if (!formats.isEmpty()) { pendingOutputFormat = formats.getLast(); - } else { - // pendingOutputFormat may already be non-null following a previous flush, and remains set in - // this case. } + // else, pendingOutputFormat may already be non-null following a previous flush, and remains + // set in this case. + availableInputBuffers.clear(); availableOutputBuffers.clear(); bufferInfos.clear(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DefaultMediaCodecAdapterFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DefaultMediaCodecAdapterFactory.java index f371d92598..2f12468f9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DefaultMediaCodecAdapterFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DefaultMediaCodecAdapterFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.mediacodec; +import android.media.MediaCodec; import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; @@ -46,6 +47,7 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. @Mode private int asynchronousMode; private boolean enableSynchronizeCodecInteractionsWithQueueing; + private boolean enableImmediateCodecStartAfterFlush; public DefaultMediaCodecAdapterFactory() { asynchronousMode = MODE_DEFAULT; @@ -85,6 +87,22 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. enableSynchronizeCodecInteractionsWithQueueing = enabled; } + /** + * Enable calling {@link MediaCodec#start} immediately after {@link MediaCodec#flush} on the + * playback thread, when operating the codec in asynchronous mode. If disabled, {@link + * MediaCodec#start} will be called by the callback thread after pending callbacks are handled. + * + *

By default, this feature is disabled. + * + *

This method is experimental, and will be renamed or removed in a future release. + * + * @param enabled Whether {@link MediaCodec#start()} will be called on the playback thread + * immediately after {@link MediaCodec#flush}. + */ + public void experimentalSetImmediateCodecStartAfterFlushEnabled(boolean enabled) { + enableImmediateCodecStartAfterFlush = enabled; + } + @Override public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configuration) throws IOException { @@ -97,7 +115,9 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. + Util.getTrackTypeString(trackType)); AsynchronousMediaCodecAdapter.Factory factory = new AsynchronousMediaCodecAdapter.Factory( - trackType, enableSynchronizeCodecInteractionsWithQueueing); + trackType, + enableSynchronizeCodecInteractionsWithQueueing, + enableImmediateCodecStartAfterFlush); return factory.createAdapter(configuration); } return new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration); 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 9aa2b5f3ba..3aacf01791 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 @@ -54,7 +54,8 @@ public class AsynchronousMediaCodecAdapterTest { new AsynchronousMediaCodecAdapter.Factory( /* callbackThreadSupplier= */ () -> callbackThread, /* queueingThreadSupplier= */ () -> queueingThread, - /* synchronizeCodecInteractionsWithQueueing= */ false) + /* synchronizeCodecInteractionsWithQueueing= */ false, + /* enableImmediateCodecStartAfterFlush= */ false) .createAdapter(configuration); bufferInfo = new MediaCodec.BufferInfo(); // After starting the MediaCodec, the ShadowMediaCodec offers input buffer 0. We advance the diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java index fbb031a64b..8f538de9de 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java @@ -24,6 +24,7 @@ import static org.robolectric.Shadows.shadowOf; import android.media.MediaCodec; import android.media.MediaFormat; +import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -81,16 +82,24 @@ public class AsynchronousMediaCodecCallbackTest { @Test public void dequeInputBufferIndex_withPendingFlush_returnsTryAgain() { - Looper callbackThreadLooper = callbackThread.getLooper(); + AtomicBoolean beforeFlushCompletes = new AtomicBoolean(); AtomicBoolean flushCompleted = new AtomicBoolean(); + Looper callbackThreadLooper = callbackThread.getLooper(); + Handler callbackHandler = new Handler(callbackThreadLooper); + ShadowLooper shadowCallbackLooper = shadowOf(callbackThreadLooper); // Pause the callback thread so that flush() never completes. - shadowOf(callbackThreadLooper).pause(); + shadowCallbackLooper.pause(); // Send two input buffers to the callback and then flush(). asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + callbackHandler.post(() -> beforeFlushCompletes.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + callbackHandler.post(() -> flushCompleted.set(true)); + while (!beforeFlushCompletes.get()) { + shadowCallbackLooper.runOneTask(); + } + assertThat(flushCompleted.get()).isFalse(); assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); @@ -104,8 +113,8 @@ public class AsynchronousMediaCodecCallbackTest { // Send two input buffers to the callback and then flush(). asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback thread so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -123,10 +132,11 @@ public class AsynchronousMediaCodecCallbackTest { // another input buffer. asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); - // Progress the callback thread so that flush() completes. - shadowOf(callbackThreadLooper).idle(); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); + // Progress the callback thread to complete flush. + shadowOf(callbackThread.getLooper()).idle(); + // Send another input buffer to the callback asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 2); assertThat(flushCompleted.get()).isTrue(); @@ -152,20 +162,6 @@ public class AsynchronousMediaCodecCallbackTest { () -> asynchronousMediaCodecCallback.dequeueInputBufferIndex()); } - @Test - public void dequeueInputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception { - MediaCodec.CodecException codecException = createCodecException(); - asynchronousMediaCodecCallback.flushAsync( - () -> { - throw codecException; - }); - shadowOf(callbackThread.getLooper()).idle(); - - assertThrows( - MediaCodec.CodecException.class, - () -> asynchronousMediaCodecCallback.dequeueInputBufferIndex()); - } - @Test public void dequeOutputBufferIndex_afterCreation_returnsTryAgain() { MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); @@ -198,17 +194,24 @@ public class AsynchronousMediaCodecCallbackTest { @Test public void dequeOutputBufferIndex_withPendingFlush_returnsTryAgain() { - Looper callbackThreadLooper = callbackThread.getLooper(); + AtomicBoolean beforeFlushCompletes = new AtomicBoolean(); AtomicBoolean flushCompleted = new AtomicBoolean(); + Looper callbackThreadLooper = callbackThread.getLooper(); + Handler callbackHandler = new Handler(callbackThreadLooper); + ShadowLooper shadowCallbackLooper = shadowOf(callbackThreadLooper); // Pause the callback thread so that flush() never completes. - shadowOf(callbackThreadLooper).pause(); + shadowCallbackLooper.pause(); // Send two output buffers to the callback and then flush(). MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + callbackHandler.post(() -> beforeFlushCompletes.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + callbackHandler.post(() -> flushCompleted.set(true)); + while (beforeFlushCompletes.get()) { + shadowCallbackLooper.runOneTask(); + } assertThat(flushCompleted.get()).isFalse(); assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo())) @@ -224,8 +227,8 @@ public class AsynchronousMediaCodecCallbackTest { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -245,10 +248,11 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputFormatChanged(codec, createMediaFormat("format0")); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); + // Emulate an output buffer is available. asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 2, bufferInfo); MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); @@ -271,8 +275,8 @@ public class AsynchronousMediaCodecCallbackTest { MediaFormat pendingMediaFormat = new MediaFormat(); asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat); // flush() should not discard the last format. - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); // Right after flush(), we send an output buffer: the pending output format should be @@ -298,8 +302,8 @@ public class AsynchronousMediaCodecCallbackTest { MediaFormat pendingMediaFormat = new MediaFormat(); asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat); // flush() should not discard the last format. - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); // The first callback after flush() is a new MediaFormat, it should overwrite the pending @@ -335,20 +339,6 @@ public class AsynchronousMediaCodecCallbackTest { () -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo())); } - @Test - public void dequeueOutputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception { - MediaCodec.CodecException codecException = createCodecException(); - asynchronousMediaCodecCallback.flushAsync( - () -> { - throw codecException; - }); - shadowOf(callbackThread.getLooper()).idle(); - - assertThrows( - MediaCodec.CodecException.class, - () -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo())); - } - @Test public void getOutputFormat_onNewInstance_raisesException() { try { @@ -377,8 +367,8 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format); asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -390,7 +380,8 @@ public class AsynchronousMediaCodecCallbackTest { public void getOutputFormat_afterFlushWithPendingFormat_returnsPendingFormat() { MediaCodec.BufferInfo outInfo = new MediaCodec.BufferInfo(); AtomicBoolean flushCompleted = new AtomicBoolean(); - ShadowLooper shadowCallbackLooper = shadowOf(callbackThread.getLooper()); + Looper callbackThreadLooper = callbackThread.getLooper(); + ShadowLooper shadowCallbackLooper = shadowOf(callbackThreadLooper); shadowCallbackLooper.pause(); asynchronousMediaCodecCallback.onOutputFormatChanged(codec, createMediaFormat("format0")); @@ -399,8 +390,8 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputFormatChanged(codec, createMediaFormat("format1")); asynchronousMediaCodecCallback.onOutputBufferAvailable( codec, /* index= */ 1, new MediaCodec.BufferInfo()); - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ () -> flushCompleted.set(true)); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the looper so that flush is completed shadowCallbackLooper.idle(); // Enqueue an output buffer to make the pending format available. @@ -419,7 +410,8 @@ public class AsynchronousMediaCodecCallbackTest { public void getOutputFormat_withConsecutiveFlushAndPendingFormatFromFirstFlush_returnsPendingFormat() { MediaCodec.BufferInfo outInfo = new MediaCodec.BufferInfo(); - AtomicInteger flushesCompleted = new AtomicInteger(); + AtomicInteger flushCompleted = new AtomicInteger(); + Handler callbackThreadHandler = new Handler(callbackThread.getLooper()); ShadowLooper shadowCallbackLooper = shadowOf(callbackThread.getLooper()); shadowCallbackLooper.pause(); @@ -427,17 +419,17 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputBufferAvailable( codec, /* index= */ 0, new MediaCodec.BufferInfo()); // Flush and progress the looper so that flush is completed. - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ flushesCompleted::incrementAndGet); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + callbackThreadHandler.post(flushCompleted::incrementAndGet); shadowCallbackLooper.idle(); // Flush again, the pending format from the first flush should remain as pending. - asynchronousMediaCodecCallback.flushAsync( - /* onFlushCompleted= */ flushesCompleted::incrementAndGet); + asynchronousMediaCodecCallback.flush(/* codec= */ null); + callbackThreadHandler.post(flushCompleted::incrementAndGet); shadowCallbackLooper.idle(); asynchronousMediaCodecCallback.onOutputBufferAvailable( codec, /* index= */ 1, new MediaCodec.BufferInfo()); - assertThat(flushesCompleted.get()).isEqualTo(2); + assertThat(flushCompleted.get()).isEqualTo(2); assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outInfo)) .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); assertThat(asynchronousMediaCodecCallback.getOutputFormat().getString("name")) @@ -445,19 +437,6 @@ public class AsynchronousMediaCodecCallbackTest { assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outInfo)).isEqualTo(1); } - @Test - public void flush_withPendingFlush_onlyLastFlushCompletes() { - ShadowLooper callbackLooperShadow = shadowOf(callbackThread.getLooper()); - callbackLooperShadow.pause(); - AtomicInteger flushCompleted = new AtomicInteger(); - - asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(1)); - asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(2)); - callbackLooperShadow.idle(); - - assertThat(flushCompleted.get()).isEqualTo(2); - } - /** Reflectively create a {@link MediaCodec.CodecException}. */ private static MediaCodec.CodecException createCodecException() throws Exception { Constructor constructor = From 028bb2341adaf150af933b532cdd151d9f16e1c4 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 5 Nov 2021 14:39:51 +0000 Subject: [PATCH 12/41] Update the demo app to use the stable ExoPlayer.Builder constructor The ExoPlayer.Builder constructor overloads are only needed for apps trying to ensure certain classes are removed by R8/proguard, which isn't relevant for the demo app. PiperOrigin-RevId: 407819694 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index ca9fd45d42..302138d947 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -269,7 +269,8 @@ public class PlayerActivity extends AppCompatActivity trackSelector = new DefaultTrackSelector(/* context= */ this); lastSeenTracksInfo = TracksInfo.EMPTY; player = - new ExoPlayer.Builder(/* context= */ this, renderersFactory) + new ExoPlayer.Builder(/* context= */ this) + .setRenderersFactory(renderersFactory) .setMediaSourceFactory(mediaSourceFactory) .setTrackSelector(trackSelector) .build(); From ed63fee21c780fd51cfcf567e635da8d03797c0a Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 5 Nov 2021 16:46:46 +0000 Subject: [PATCH 13/41] Migrate the demo app to use non-deprecated MediaItem Builders #minor-release PiperOrigin-RevId: 407843859 --- .../android/exoplayer2/demo/IntentUtil.java | 42 ++++++++++++------- .../demo/SampleChooserActivity.java | 27 +++++++----- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java index d3579c8c35..afc155bfc3 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java @@ -23,11 +23,13 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaItem.ClippingConfiguration; +import com.google.android.exoplayer2.MediaItem.SubtitleConfiguration; import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,36 +120,46 @@ public class IntentUtil { @Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix); @Nullable String title = intent.getStringExtra(TITLE_EXTRA + extrasKeySuffix); @Nullable String adTagUri = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix); + @Nullable + SubtitleConfiguration subtitleConfiguration = + createSubtitleConfiguration(intent, extrasKeySuffix); MediaItem.Builder builder = new MediaItem.Builder() .setUri(uri) .setMimeType(mimeType) .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) - .setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix)) - .setClipStartPositionMs( - intent.getLongExtra(CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, 0)) - .setClipEndPositionMs( - intent.getLongExtra( - CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE)); + .setClippingConfiguration( + new ClippingConfiguration.Builder() + .setStartPositionMs( + intent.getLongExtra(CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, 0)) + .setEndPositionMs( + intent.getLongExtra( + CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE)) + .build()); if (adTagUri != null) { builder.setAdsConfiguration( new MediaItem.AdsConfiguration.Builder(Uri.parse(adTagUri)).build()); } + if (subtitleConfiguration != null) { + builder.setSubtitleConfigurations(ImmutableList.of(subtitleConfiguration)); + } return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build(); } - private static List createSubtitlesFromIntent( + @Nullable + private static MediaItem.SubtitleConfiguration createSubtitleConfiguration( Intent intent, String extrasKeySuffix) { if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { - return Collections.emptyList(); + return null; } - return Collections.singletonList( - new MediaItem.Subtitle( - Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)), - checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix)), - intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix), - C.SELECTION_FLAG_DEFAULT)); + return new MediaItem.SubtitleConfiguration.Builder( + Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix))) + .setMimeType( + checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix))) + .setLanguage(intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix)) + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build(); } private static MediaItem.Builder populateDrmPropertiesFromIntent( diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 6cfe215a78..2f51451fb3 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -43,6 +43,7 @@ import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaItem.ClippingConfiguration; import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.RenderersFactory; @@ -53,6 +54,7 @@ import com.google.android.exoplayer2.upstream.DataSourceUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.io.InputStream; @@ -351,6 +353,8 @@ public class SampleChooserActivity extends AppCompatActivity boolean drmSessionForClearContent = false; boolean drmMultiSession = false; boolean drmForceDefaultLicenseUri = false; + MediaItem.ClippingConfiguration.Builder clippingConfiguration = + new ClippingConfiguration.Builder(); MediaItem.Builder mediaItem = new MediaItem.Builder(); reader.beginObject(); @@ -367,10 +371,10 @@ public class SampleChooserActivity extends AppCompatActivity extension = reader.nextString(); break; case "clip_start_position_ms": - mediaItem.setClipStartPositionMs(reader.nextLong()); + clippingConfiguration.setStartPositionMs(reader.nextLong()); break; case "clip_end_position_ms": - mediaItem.setClipEndPositionMs(reader.nextLong()); + clippingConfiguration.setEndPositionMs(reader.nextLong()); break; case "ad_tag_uri": mediaItem.setAdsConfiguration( @@ -439,7 +443,8 @@ public class SampleChooserActivity extends AppCompatActivity mediaItem .setUri(uri) .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) - .setMimeType(adaptiveMimeType); + .setMimeType(adaptiveMimeType) + .setClippingConfiguration(clippingConfiguration.build()); if (drmUuid != null) { mediaItem.setDrmConfiguration( new MediaItem.DrmConfiguration.Builder(drmUuid) @@ -463,13 +468,15 @@ public class SampleChooserActivity extends AppCompatActivity "drm_uuid is required if drm_force_default_license_uri is set."); } if (subtitleUri != null) { - MediaItem.Subtitle subtitle = - new MediaItem.Subtitle( - subtitleUri, - checkNotNull( - subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."), - subtitleLanguage); - mediaItem.setSubtitles(Collections.singletonList(subtitle)); + MediaItem.SubtitleConfiguration subtitleConfiguration = + new MediaItem.SubtitleConfiguration.Builder(subtitleUri) + .setMimeType( + checkNotNull( + subtitleMimeType, + "subtitle_mime_type is required if subtitle_uri is set.")) + .setLanguage(subtitleLanguage) + .build(); + mediaItem.setSubtitleConfigurations(ImmutableList.of(subtitleConfiguration)); } return new PlaylistHolder(title, Collections.singletonList(mediaItem.build())); } From 08d827f2ed237e27df0c7af5f58a1d4024297343 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 5 Nov 2021 17:04:13 +0000 Subject: [PATCH 14/41] Migrate usages of deprecated MediaItem symbols #minor-release PiperOrigin-RevId: 407847729 --- .../ext/media2/DefaultMediaItemConverter.java | 7 +- .../android/exoplayer2/TimelineTest.java | 13 +- .../exoplayer2/ClippedPlaybackTest.java | 35 +- .../DefaultLivePlaybackSpeedControlTest.java | 455 ++++++++++-------- .../source/DefaultMediaSourceFactoryTest.java | 45 +- .../source/dash/DashMediaSource.java | 9 +- 6 files changed, 320 insertions(+), 244 deletions(-) diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java index 12ac0fb4e8..502c3b7ba2 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java @@ -88,8 +88,11 @@ public class DefaultMediaItemConverter implements MediaItemConverter { .setMediaId(mediaId != null ? mediaId : MediaItem.DEFAULT_MEDIA_ID) .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) .setTag(media2MediaItem) - .setClipStartPositionMs(startPositionMs) - .setClipEndPositionMs(endPositionMs) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(startPositionMs) + .setEndPositionMs(endPositionMs) + .build()) .build(); } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java index 4bf3f94bad..fda6c6e888 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -301,12 +301,13 @@ public class TimelineTest { window.isSeekable = true; window.isDynamic = true; window.liveConfiguration = - new LiveConfiguration( - /* targetOffsetMs= */ 1, - /* minOffsetMs= */ 2, - /* maxOffsetMs= */ 3, - /* minPlaybackSpeed= */ 0.5f, - /* maxPlaybackSpeed= */ 1.5f); + new LiveConfiguration.Builder() + .setTargetOffsetMs(1) + .setMinOffsetMs(2) + .setMaxOffsetMs(3) + .setMinPlaybackSpeed(0.5f) + .setMaxPlaybackSpeed(1.5f) + .build(); window.isPlaceholder = true; window.defaultPositionUs = 444; window.durationUs = 555; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ClippedPlaybackTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ClippedPlaybackTest.java index 8cbfd9d552..7d541cd8fa 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ClippedPlaybackTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ClippedPlaybackTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.MediaItem.SubtitleConfiguration; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.ConditionVariable; @@ -44,15 +45,16 @@ public final class ClippedPlaybackTest { MediaItem mediaItem = new MediaItem.Builder() .setUri("asset:///media/mp4/sample.mp4") - .setSubtitles( + .setSubtitleConfigurations( ImmutableList.of( - new MediaItem.Subtitle( - Uri.parse("asset:///media/webvtt/typical"), - MimeTypes.TEXT_VTT, - "en", - C.SELECTION_FLAG_DEFAULT))) + new SubtitleConfiguration.Builder(Uri.parse("asset:///media/webvtt/typical")) + .setMimeType(MimeTypes.TEXT_VTT) + .setLanguage("en") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build())) // Expect the clipping to affect both subtitles and video. - .setClipEndPositionMs(1000) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(1000).build()) .build(); AtomicReference player = new AtomicReference<>(); TextCapturingPlaybackListener textCapturer = new TextCapturingPlaybackListener(); @@ -80,21 +82,24 @@ public final class ClippedPlaybackTest { ImmutableList.of( new MediaItem.Builder() .setUri("asset:///media/mp4/sample.mp4") - .setSubtitles( + .setSubtitleConfigurations( ImmutableList.of( - new MediaItem.Subtitle( - Uri.parse("asset:///media/webvtt/typical"), - MimeTypes.TEXT_VTT, - "en", - C.SELECTION_FLAG_DEFAULT))) + new SubtitleConfiguration.Builder( + Uri.parse("asset:///media/webvtt/typical")) + .setMimeType(MimeTypes.TEXT_VTT) + .setLanguage("en") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build())) // Expect the clipping to affect both subtitles and video. - .setClipEndPositionMs(1000) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(1000).build()) .build(), new MediaItem.Builder() .setUri("asset:///media/mp4/sample.mp4") // Not needed for correctness, just makes test run faster. Must be longer than the // subtitle content (3.5s). - .setClipEndPositionMs(4_000) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(4_000).build()) .build()); AtomicReference player = new AtomicReference<>(); TextCapturingPlaybackListener textCapturer = new TextCapturingPlaybackListener(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java index 58f6522b1d..cc05a87558 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java @@ -44,12 +44,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(42_000); } @@ -60,12 +61,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 4321, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(4321) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(400_000); } @@ -76,12 +78,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 3, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(3) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(5_000); } @@ -93,12 +96,13 @@ public class DefaultLivePlaybackSpeedControlTest { defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(321_000); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -113,12 +117,13 @@ public class DefaultLivePlaybackSpeedControlTest { defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(123_456_789); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -133,12 +138,13 @@ public class DefaultLivePlaybackSpeedControlTest { defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(3_141); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -164,12 +170,13 @@ public class DefaultLivePlaybackSpeedControlTest { new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(123_456_789); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(C.TIME_UNSET); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -184,12 +191,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); defaultLivePlaybackSpeedControl.notifyRebuffer(); @@ -206,12 +214,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); List targetOffsetsUs = new ArrayList<>(); for (int i = 0; i < 500; i++) { @@ -231,12 +240,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(0) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -252,12 +262,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(321_000); @@ -274,22 +285,24 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); defaultLivePlaybackSpeedControl.notifyRebuffer(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 3, - /* maxLiveOffsetMs= */ 450, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(3) + .setMaxOffsetMs(450) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetAfterUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); assertThat(targetLiveOffsetAfterUs).isGreaterThan(targetLiveOffsetBeforeUs); @@ -304,21 +317,23 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetLiveOffsetIncrementOnRebufferMs(3) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42, - /* minLiveOffsetMs= */ 5, - /* maxLiveOffsetMs= */ 400, - /* minPlaybackSpeed= */ 1f, - /* maxPlaybackSpeed= */ 1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinOffsetMs(5) + .setMaxOffsetMs(400) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 39, - /* minLiveOffsetMs= */ 3, - /* maxLiveOffsetMs= */ 450, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(39) + .setMinOffsetMs(3) + .setMaxOffsetMs(450) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); assertThat(targetLiveOffsetUs).isEqualTo(39_000); @@ -333,12 +348,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMinUpdateIntervalMs(100) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); long targetLiveOffsetAfterRebufferUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -371,12 +387,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMinUpdateIntervalMs(100) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); long targetLiveOffsetAfterRebufferUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -408,12 +425,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); // Pretend to have a buffered duration at around the target duration with some artificial noise. @@ -440,12 +458,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMinPossibleLiveOffsetSmoothingFactor(0f) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); // Pretend to have a buffered duration at around the target duration with some artificial noise. @@ -474,12 +493,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMinUpdateIntervalMs(100) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 42_000, - /* minLiveOffsetMs= */ 5_000, - /* maxLiveOffsetMs= */ 400_000, - /* minPlaybackSpeed= */ 0.9f, - /* maxPlaybackSpeed= */ 1.1f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(42_000) + .setMinOffsetMs(5_000) + .setMaxOffsetMs(400_000) + .setMinPlaybackSpeed(0.9f) + .setMaxPlaybackSpeed(1.1f) + .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -495,12 +515,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -516,12 +537,13 @@ public class DefaultLivePlaybackSpeedControlTest { .setMaxLiveOffsetErrorMsForUnitSpeed(5) .build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeedJustAboveLowerErrorMargin = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -539,12 +561,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setProportionalControlFactor(0.01f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -561,12 +584,13 @@ public class DefaultLivePlaybackSpeedControlTest { new DefaultLivePlaybackSpeedControl.Builder().setProportionalControlFactor(0.01f).build(); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(2_000_000); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -583,12 +607,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setFallbackMaxPlaybackSpeed(1.5f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -603,12 +628,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setFallbackMinPlaybackSpeed(0.5f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -623,12 +649,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setFallbackMaxPlaybackSpeed(1.5f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ 2f)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(2f) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -643,12 +670,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setFallbackMinPlaybackSpeed(0.5f).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ 0.2f, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(0.2f) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -662,12 +690,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -691,23 +720,25 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( /* liveOffsetUs= */ 1_500_000, /* bufferedDurationUs= */ 1_000_000); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed2 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( /* liveOffsetUs= */ 2_500_000, /* bufferedDurationUs= */ 1_000_000); @@ -721,23 +752,25 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( /* liveOffsetUs= */ 1_500_000, /* bufferedDurationUs= */ 1_000_000); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 1_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(1_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed2 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( /* liveOffsetUs= */ 2_500_000, /* bufferedDurationUs= */ 1_000_000); @@ -751,12 +784,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( @@ -774,12 +808,13 @@ public class DefaultLivePlaybackSpeedControlTest { DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); defaultLivePlaybackSpeedControl.setLiveConfiguration( - new LiveConfiguration( - /* targetLiveOffsetMs= */ 2_000, - /* minLiveOffsetMs= */ C.TIME_UNSET, - /* maxLiveOffsetMs= */ C.TIME_UNSET, - /* minPlaybackSpeed= */ C.RATE_UNSET, - /* maxPlaybackSpeed= */ C.RATE_UNSET)); + new LiveConfiguration.Builder() + .setTargetOffsetMs(2_000) + .setMinOffsetMs(C.TIME_UNSET) + .setMaxOffsetMs(C.TIME_UNSET) + .setMinPlaybackSpeed(C.RATE_UNSET) + .setMaxPlaybackSpeed(C.RATE_UNSET) + .build()); float adjustedSpeed1 = defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java index ed1960b708..e580d757be 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java @@ -93,12 +93,22 @@ public final class DefaultMediaSourceFactoryTest { public void createMediaSource_withSubtitle_isMergingMediaSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); - List subtitles = + List subtitleConfigurations = Arrays.asList( - new MediaItem.Subtitle(Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "en"), - new MediaItem.Subtitle( - Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "de", C.SELECTION_FLAG_DEFAULT)); - MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setSubtitles(subtitles).build(); + new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_TEXT)) + .setMimeType(MimeTypes.APPLICATION_TTML) + .setLanguage("en") + .build(), + new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_TEXT)) + .setMimeType(MimeTypes.APPLICATION_TTML) + .setLanguage("de") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build()); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setSubtitleConfigurations(subtitleConfigurations) + .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -110,7 +120,11 @@ public final class DefaultMediaSourceFactoryTest { DefaultMediaSourceFactory defaultMediaSourceFactory = new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = - new MediaItem.Builder().setUri(URI_MEDIA).setClipStartPositionMs(1000L).build(); + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setStartPositionMs(1000L).build()) + .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -122,7 +136,11 @@ public final class DefaultMediaSourceFactoryTest { DefaultMediaSourceFactory defaultMediaSourceFactory = new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = - new MediaItem.Builder().setUri(URI_MEDIA).setClipEndPositionMs(1000L).build(); + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(1000L).build()) + .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -134,7 +152,13 @@ public final class DefaultMediaSourceFactoryTest { DefaultMediaSourceFactory defaultMediaSourceFactory = new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = - new MediaItem.Builder().setUri(URI_MEDIA).setClipRelativeToDefaultPosition(true).build(); + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setRelativeToDefaultPosition(true) + .build()) + .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -148,7 +172,10 @@ public final class DefaultMediaSourceFactoryTest { MediaItem mediaItem = new MediaItem.Builder() .setUri(URI_MEDIA) - .setClipEndPositionMs(C.TIME_END_OF_SOURCE) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setEndPositionMs(C.TIME_END_OF_SOURCE) + .build()) .build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 327cbb308f..fac44d9a19 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -1053,8 +1053,13 @@ public final class DashMediaSource extends BaseMediaSource { maxPlaybackSpeed = manifest.serviceDescription.maxPlaybackSpeed; } liveConfiguration = - new MediaItem.LiveConfiguration( - targetOffsetMs, minLiveOffsetMs, maxLiveOffsetMs, minPlaybackSpeed, maxPlaybackSpeed); + new MediaItem.LiveConfiguration.Builder() + .setTargetOffsetMs(targetOffsetMs) + .setMinOffsetMs(minLiveOffsetMs) + .setMaxOffsetMs(maxLiveOffsetMs) + .setMinPlaybackSpeed(minPlaybackSpeed) + .setMaxPlaybackSpeed(maxPlaybackSpeed) + .build(); } private void scheduleManifestRefresh(long delayUntilNextLoadMs) { From 8fb0af83f52b6c6539c9ec4ee4fa334917481089 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 5 Nov 2021 17:12:32 +0000 Subject: [PATCH 15/41] Use stop(boolean) in MediaSessionConnector --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index cfda09ff0a..751a850fbe 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -1208,8 +1208,7 @@ public final class MediaSessionConnector { @Override public void onStop() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_STOP)) { - player.stop(); - player.clearMediaItems(); + player.stop(/* reset= */ true); } } From 1a81662dad222d9994f4aa956d7d4467931b8887 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Nov 2021 08:44:32 +0000 Subject: [PATCH 16/41] Fix deprecated Javadoc link. #minor-release PiperOrigin-RevId: 408269341 --- .../src/main/java/com/google/android/exoplayer2/Player.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java index b6222596a7..406398e39f 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java @@ -2119,7 +2119,7 @@ public interface Player { /** Returns the index of the period currently being played. */ int getCurrentPeriodIndex(); - /** @deprecated Use {@link #getCurrentMediaItem()} instead. */ + /** @deprecated Use {@link #getCurrentMediaItemIndex()} instead. */ @Deprecated int getCurrentWindowIndex(); From 3e934e537900e7b796f1cd20e5edf150ddbdbaaa Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Nov 2021 09:12:03 +0000 Subject: [PATCH 17/41] Rename indexes to indices in TrackSelectionOverrides And in a couple of related places. This is for consistency with the rest of the codebase where we exclusively use indices. #minor-release PiperOrigin-RevId: 408273372 --- .../TrackSelectionOverrides.java | 28 ++++++++-------- .../TrackSelectionOverridesTest.java | 10 +++--- .../TrackSelectionParametersTest.java | 2 +- .../trackselection/DefaultTrackSelector.java | 32 +++++++++---------- .../upstream/DefaultBandwidthMeter.java | 2 +- .../DefaultTrackSelectorTest.java | 8 ++--- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java index 239dd76b60..27f644071a 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java @@ -113,7 +113,7 @@ public final class TrackSelectionOverrides implements Bundleable { } /** - * Forces the selection of {@link #trackIndexes} for a {@link TrackGroup}. + * Forces the selection of {@link #trackIndices} for a {@link TrackGroup}. * *

If multiple {link #tracks} are overridden, as many as possible will be selected depending on * the player capabilities. @@ -126,10 +126,10 @@ public final class TrackSelectionOverrides implements Bundleable { */ public static final class TrackSelectionOverride implements Bundleable { - /** The {@link TrackGroup} whose {@link #trackIndexes} are forced to be selected. */ + /** The {@link TrackGroup} whose {@link #trackIndices} are forced to be selected. */ public final TrackGroup trackGroup; - /** The index of tracks in a {@link TrackGroup} to be selected. */ - public final ImmutableList trackIndexes; + /** The indices of tracks in a {@link TrackGroup} to be selected. */ + public final ImmutableList trackIndices; /** Constructs an instance to force all tracks in {@code trackGroup} to be selected. */ public TrackSelectionOverride(TrackGroup trackGroup) { @@ -138,23 +138,23 @@ public final class TrackSelectionOverrides implements Bundleable { for (int i = 0; i < trackGroup.length; i++) { builder.add(i); } - this.trackIndexes = builder.build(); + this.trackIndices = builder.build(); } /** - * Constructs an instance to force {@code trackIndexes} in {@code trackGroup} to be selected. + * Constructs an instance to force {@code trackIndices} in {@code trackGroup} to be selected. * * @param trackGroup The {@link TrackGroup} for which to override the track selection. - * @param trackIndexes The indexes of the tracks in the {@link TrackGroup} to select. + * @param trackIndices The indices of the tracks in the {@link TrackGroup} to select. */ - public TrackSelectionOverride(TrackGroup trackGroup, List trackIndexes) { - if (!trackIndexes.isEmpty()) { - if (min(trackIndexes) < 0 || max(trackIndexes) >= trackGroup.length) { + public TrackSelectionOverride(TrackGroup trackGroup, List trackIndices) { + if (!trackIndices.isEmpty()) { + if (min(trackIndices) < 0 || max(trackIndices) >= trackGroup.length) { throw new IndexOutOfBoundsException(); } } this.trackGroup = trackGroup; - this.trackIndexes = ImmutableList.copyOf(trackIndexes); + this.trackIndices = ImmutableList.copyOf(trackIndices); } @Override @@ -166,12 +166,12 @@ public final class TrackSelectionOverrides implements Bundleable { return false; } TrackSelectionOverride that = (TrackSelectionOverride) obj; - return trackGroup.equals(that.trackGroup) && trackIndexes.equals(that.trackIndexes); + return trackGroup.equals(that.trackGroup) && trackIndices.equals(that.trackIndices); } @Override public int hashCode() { - return trackGroup.hashCode() + 31 * trackIndexes.hashCode(); + return trackGroup.hashCode() + 31 * trackIndices.hashCode(); } private @C.TrackType int getTrackType() { @@ -195,7 +195,7 @@ public final class TrackSelectionOverrides implements Bundleable { public Bundle toBundle() { Bundle bundle = new Bundle(); bundle.putBundle(keyForField(FIELD_TRACK_GROUP), trackGroup.toBundle()); - bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndexes)); + bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndices)); return bundle; } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverridesTest.java b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverridesTest.java index d96e497b1f..5fe642ac48 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverridesTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverridesTest.java @@ -49,7 +49,7 @@ public final class TrackSelectionOverridesTest { new TrackSelectionOverride(newTrackGroupWithIds(1, 2)); assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2)); - assertThat(trackSelectionOverride.trackIndexes).containsExactly(0, 1).inOrder(); + assertThat(trackSelectionOverride.trackIndices).containsExactly(0, 1).inOrder(); } @Test @@ -58,7 +58,7 @@ public final class TrackSelectionOverridesTest { new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of(1)); assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2)); - assertThat(trackSelectionOverride.trackIndexes).containsExactly(1); + assertThat(trackSelectionOverride.trackIndices).containsExactly(1); } @Test @@ -67,7 +67,7 @@ public final class TrackSelectionOverridesTest { new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of()); assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2)); - assertThat(trackSelectionOverride.trackIndexes).isEmpty(); + assertThat(trackSelectionOverride.trackIndices).isEmpty(); } @Test @@ -118,9 +118,9 @@ public final class TrackSelectionOverridesTest { public void addOverride_onSameGroup_replacesOverride() { TrackGroup trackGroup = newTrackGroupWithIds(1, 2, 3); TrackSelectionOverride override1 = - new TrackSelectionOverride(trackGroup, /* trackIndexes= */ ImmutableList.of(0)); + new TrackSelectionOverride(trackGroup, /* trackIndices= */ ImmutableList.of(0)); TrackSelectionOverride override2 = - new TrackSelectionOverride(trackGroup, /* trackIndexes= */ ImmutableList.of(1)); + new TrackSelectionOverride(trackGroup, /* trackIndices= */ ImmutableList.of(1)); TrackSelectionOverrides trackSelectionOverrides = new TrackSelectionOverrides.Builder().addOverride(override1).addOverride(override2).build(); diff --git a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java index f1271ecf7a..519b1d4bed 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java @@ -77,7 +77,7 @@ public final class TrackSelectionParametersTest { new TrackGroup( new Format.Builder().setId(4).build(), new Format.Builder().setId(5).build()), - /* trackIndexes= */ ImmutableList.of(1))) + /* trackIndices= */ ImmutableList.of(1))) .build(); TrackSelectionParameters parameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 600a387796..ea95d3afc7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -307,7 +307,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererDisabledFlags = makeSparseBooleanArrayFromTrueKeys( bundle.getIntArray( - Parameters.keyForField(Parameters.FIELD_RENDERER_DISABLED_INDEXES))); + Parameters.keyForField(Parameters.FIELD_RENDERER_DISABLED_INDICES))); } @Override @@ -825,9 +825,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { private void setSelectionOverridesFromBundle(Bundle bundle) { @Nullable - int[] rendererIndexes = + int[] rendererIndices = bundle.getIntArray( - Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDEXES)); + Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDICES)); List trackGroupArrays = BundleableUtil.fromBundleNullableList( TrackGroupArray.CREATOR, @@ -841,11 +841,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES)), /* defaultValue= */ new SparseArray<>()); - if (rendererIndexes == null || rendererIndexes.length != trackGroupArrays.size()) { + if (rendererIndices == null || rendererIndices.length != trackGroupArrays.size()) { return; // Incorrect format, ignore all overrides. } - for (int i = 0; i < rendererIndexes.length; i++) { - int rendererIndex = rendererIndexes[i]; + for (int i = 0; i < rendererIndices.length; i++) { + int rendererIndex = rendererIndices[i]; TrackGroupArray groups = trackGroupArrays.get(i); @Nullable SelectionOverride selectionOverride = selectionOverrides.get(i); setSelectionOverride(rendererIndex, groups, selectionOverride); @@ -1107,10 +1107,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY, FIELD_TUNNELING_ENABLED, FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS, - FIELD_SELECTION_OVERRIDES_RENDERER_INDEXES, + FIELD_SELECTION_OVERRIDES_RENDERER_INDICES, FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS, FIELD_SELECTION_OVERRIDES, - FIELD_RENDERER_DISABLED_INDEXES, + FIELD_RENDERER_DISABLED_INDICES, }) private @interface FieldNumber {} @@ -1126,10 +1126,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = 1008; private static final int FIELD_TUNNELING_ENABLED = 1009; private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = 1010; - private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDEXES = 1011; + private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = 1011; private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = 1012; private static final int FIELD_SELECTION_OVERRIDES = 1013; - private static final int FIELD_RENDERER_DISABLED_INDEXES = 1014; + private static final int FIELD_RENDERER_DISABLED_INDICES = 1014; @Override public Bundle toBundle() { @@ -1172,7 +1172,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { putSelectionOverridesToBundle(bundle, selectionOverrides); // Only true values are put into rendererDisabledFlags. bundle.putIntArray( - keyForField(FIELD_RENDERER_DISABLED_INDEXES), + keyForField(FIELD_RENDERER_DISABLED_INDICES), getKeysFromSparseBooleanArray(rendererDisabledFlags)); return bundle; @@ -1194,7 +1194,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void putSelectionOverridesToBundle( Bundle bundle, SparseArray> selectionOverrides) { - ArrayList rendererIndexes = new ArrayList<>(); + ArrayList rendererIndices = new ArrayList<>(); ArrayList trackGroupArrays = new ArrayList<>(); SparseArray selections = new SparseArray<>(); @@ -1207,10 +1207,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { selections.put(trackGroupArrays.size(), selection); } trackGroupArrays.add(override.getKey()); - rendererIndexes.add(rendererIndex); + rendererIndices.add(rendererIndex); } bundle.putIntArray( - keyForField(FIELD_SELECTION_OVERRIDES_RENDERER_INDEXES), Ints.toArray(rendererIndexes)); + keyForField(FIELD_SELECTION_OVERRIDES_RENDERER_INDICES), Ints.toArray(rendererIndices)); bundle.putParcelableArrayList( keyForField(FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS), BundleableUtil.toBundleArrayList(trackGroupArrays)); @@ -1571,7 +1571,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackSelectionOverride overrideTracks = params.trackSelectionOverrides.getOverride(trackGroup); if (overrideTracks != null) { - if (overrideTracks.trackIndexes.isEmpty()) { + if (overrideTracks.trackIndices.isEmpty()) { // TrackGroup is disabled. Deselect the currentDefinition if applicable. Otherwise ignore. if (currentDefinition != null && currentDefinition.group.equals(trackGroup)) { currentDefinition = null; @@ -1580,7 +1580,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Override current definition with new selection. currentDefinition = new ExoTrackSelection.Definition( - trackGroup, Ints.toArray(overrideTracks.trackIndexes)); + trackGroup, Ints.toArray(overrideTracks.trackIndices)); } } } 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 2db19fb6a7..e606cfe148 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 @@ -465,7 +465,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** * Returns initial bitrate group assignments for a {@code country}. The initial bitrate is a list - * of indexes for [Wifi, 2G, 3G, 4G, 5G_NSA, 5G_SA]. + * of indices for [Wifi, 2G, 3G, 4G, 5G_NSA, 5G_SA]. */ private static int[] getInitialBitrateCountryGroupAssignment(String country) { switch (country) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index dcec44bc26..b70bfd53fa 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -220,13 +220,13 @@ public final class DefaultTrackSelectorTest { new TrackSelectionOverrides.Builder() .addOverride( new TrackSelectionOverride( - videoGroupHighBitrate, /* trackIndexes= */ ImmutableList.of())) + videoGroupHighBitrate, /* trackIndices= */ ImmutableList.of())) .addOverride( new TrackSelectionOverride( - videoGroupMidBitrate, /* trackIndexes= */ ImmutableList.of(0))) + videoGroupMidBitrate, /* trackIndices= */ ImmutableList.of(0))) .addOverride( new TrackSelectionOverride( - videoGroupLowBitrate, /* trackIndexes= */ ImmutableList.of())) + videoGroupLowBitrate, /* trackIndices= */ ImmutableList.of())) .build())); TrackSelectorResult result = @@ -1933,7 +1933,7 @@ public final class DefaultTrackSelectorTest { .setOverrideForType( new TrackSelectionOverride( new TrackGroup(AUDIO_FORMAT, AUDIO_FORMAT, AUDIO_FORMAT, AUDIO_FORMAT), - /* trackIndexes= */ ImmutableList.of(0, 2, 3))) + /* trackIndices= */ ImmutableList.of(0, 2, 3))) .build()) .setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_AUDIO)) .build(); From 136ce57f1e2f4251eb63a276b50fe0aa8350fdf8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Nov 2021 11:00:55 +0000 Subject: [PATCH 18/41] Create wrapper class for LogSessionId. The platform class is only available from API 31, so we need a generic wrapper that can be used on all API levels. The wrapper essentially provides an identifier for a player instance, so naming it accordingly. PiperOrigin-RevId: 408292802 --- .../exoplayer2/analytics/PlayerId.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/analytics/PlayerId.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlayerId.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlayerId.java new file mode 100644 index 0000000000..0e72e7453a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlayerId.java @@ -0,0 +1,75 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.analytics; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import android.media.metrics.LogSessionId; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.util.Util; + +/** Identifier for a player instance. */ +public final class PlayerId { + + /** + * A player identifier with unset default values that can be used as a placeholder or for testing. + */ + public static final PlayerId UNSET = + Util.SDK_INT < 31 ? new PlayerId() : new PlayerId(LogSessionIdApi31.UNSET); + + @Nullable private final LogSessionIdApi31 logSessionIdApi31; + + /** Creates an instance for API < 31. */ + public PlayerId() { + this(/* logSessionIdApi31= */ (LogSessionIdApi31) null); + checkState(Util.SDK_INT < 31); + } + + /** + * Creates an instance for API ≥ 31. + * + * @param logSessionId The {@link LogSessionId} used for this player. + */ + @RequiresApi(31) + public PlayerId(LogSessionId logSessionId) { + this(new LogSessionIdApi31(logSessionId)); + } + + private PlayerId(@Nullable LogSessionIdApi31 logSessionIdApi31) { + this.logSessionIdApi31 = logSessionIdApi31; + } + + /** Returns the {@link LogSessionId} for this player instance. */ + @RequiresApi(31) + public LogSessionId getLogSessionId() { + return checkNotNull(logSessionIdApi31).logSessionId; + } + + @RequiresApi(31) + private static final class LogSessionIdApi31 { + + public static final LogSessionIdApi31 UNSET = + new LogSessionIdApi31(LogSessionId.LOG_SESSION_ID_NONE); + + public final LogSessionId logSessionId; + + public LogSessionIdApi31(LogSessionId logSessionId) { + this.logSessionId = logSessionId; + } + } +} From b3b4645e69a5f8ec6774c003fc0992626554b827 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 8 Nov 2021 12:08:04 +0000 Subject: [PATCH 19/41] Migrate GL demo from deprecated ExoPlayer.VideoComponent to ExoPlayer #minor-release PiperOrigin-RevId: 408304187 --- .../exoplayer2/gldemo/MainActivity.java | 5 ++- .../gldemo/VideoProcessingGLSurfaceView.java | 33 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java index 5b513e93f5..106b620ea1 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java @@ -179,8 +179,7 @@ public final class MainActivity extends Activity { player.play(); VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = Assertions.checkNotNull(this.videoProcessingGLSurfaceView); - videoProcessingGLSurfaceView.setVideoComponent( - Assertions.checkNotNull(player.getVideoComponent())); + videoProcessingGLSurfaceView.setPlayer(player); Assertions.checkNotNull(playerView).setPlayer(player); player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null)); this.player = player; @@ -188,9 +187,9 @@ public final class MainActivity extends Activity { private void releasePlayer() { Assertions.checkNotNull(playerView).setPlayer(null); + Assertions.checkNotNull(videoProcessingGLSurfaceView).setPlayer(null); if (player != null) { player.release(); - Assertions.checkNotNull(videoProcessingGLSurfaceView).setVideoComponent(null); player = null; } } diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java index 4cc0813dab..e2796b0370 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java @@ -73,7 +73,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { @Nullable private SurfaceTexture surfaceTexture; @Nullable private Surface surface; - @Nullable private ExoPlayer.VideoComponent videoComponent; + @Nullable private ExoPlayer player; /** * Creates a new instance. Pass {@code true} for {@code requireSecureContext} if the {@link @@ -147,25 +147,24 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { } /** - * Attaches or detaches (if {@code newVideoComponent} is {@code null}) this view from the video - * component of the player. + * Attaches or detaches (if {@code player} is {@code null}) this view from the player. * - * @param newVideoComponent The new video component, or {@code null} to detach this view. + * @param player The new player, or {@code null} to detach this view. */ - public void setVideoComponent(@Nullable ExoPlayer.VideoComponent newVideoComponent) { - if (newVideoComponent == videoComponent) { + public void setPlayer(@Nullable ExoPlayer player) { + if (player == this.player) { return; } - if (videoComponent != null) { + if (this.player != null) { if (surface != null) { - videoComponent.clearVideoSurface(surface); + this.player.clearVideoSurface(surface); } - videoComponent.clearVideoFrameMetadataListener(renderer); + this.player.clearVideoFrameMetadataListener(renderer); } - videoComponent = newVideoComponent; - if (videoComponent != null) { - videoComponent.setVideoFrameMetadataListener(renderer); - videoComponent.setVideoSurface(surface); + this.player = player; + if (this.player != null) { + this.player.setVideoFrameMetadataListener(renderer); + this.player.setVideoSurface(surface); } } @@ -176,8 +175,8 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { mainHandler.post( () -> { if (surface != null) { - if (videoComponent != null) { - videoComponent.setVideoSurface(null); + if (player != null) { + player.setVideoSurface(null); } releaseSurface(surfaceTexture, surface); surfaceTexture = null; @@ -194,8 +193,8 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { this.surfaceTexture = surfaceTexture; this.surface = new Surface(surfaceTexture); releaseSurface(oldSurfaceTexture, oldSurface); - if (videoComponent != null) { - videoComponent.setVideoSurface(surface); + if (player != null) { + player.setVideoSurface(surface); } }); } From 4404404795a6fb34906ab99bc89769bd82cd7793 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 8 Nov 2021 12:41:43 +0000 Subject: [PATCH 20/41] Split MediaItemTest#setSubtitles into two tests Each test exercises one of the setters. Together they assert that both setters set both fields. PiperOrigin-RevId: 408309207 --- .../android/exoplayer2/MediaItemTest.java | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java index 3cca972822..340f346a3e 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java @@ -268,8 +268,8 @@ public class MediaItemTest { } @Test - @SuppressWarnings("deprecation") // Using deprecated Subtitle type - public void builderSetSubtitles_setsSubtitles() { + @SuppressWarnings("deprecation") // Reading deprecated subtitles field + public void builderSetSubtitleConfigurations() { List subtitleConfigurations = ImmutableList.of( new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_STRING + "/es")) @@ -278,7 +278,24 @@ public class MediaItemTest { .setSelectionFlags(C.SELECTION_FLAG_FORCED) .setRoleFlags(C.ROLE_FLAG_ALTERNATE) .setLabel("label") - .build(), + .build()); + + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_STRING) + .setSubtitleConfigurations(subtitleConfigurations) + .build(); + + assertThat(mediaItem.localConfiguration.subtitleConfigurations) + .isEqualTo(subtitleConfigurations); + assertThat(mediaItem.localConfiguration.subtitles).isEqualTo(subtitleConfigurations); + } + + @Test + @SuppressWarnings("deprecation") // Using deprecated Subtitle type + public void builderSetSubtitles() { + List subtitles = + ImmutableList.of( new MediaItem.Subtitle( Uri.parse(URI_STRING + "/en"), MimeTypes.APPLICATION_TTML, /* language= */ "en"), new MediaItem.Subtitle( @@ -295,14 +312,10 @@ public class MediaItemTest { "label")); MediaItem mediaItem = - new MediaItem.Builder() - .setUri(URI_STRING) - .setSubtitleConfigurations(subtitleConfigurations) - .build(); + new MediaItem.Builder().setUri(URI_STRING).setSubtitles(subtitles).build(); - assertThat(mediaItem.localConfiguration.subtitleConfigurations) - .isEqualTo(subtitleConfigurations); - assertThat(mediaItem.localConfiguration.subtitles).isEqualTo(subtitleConfigurations); + assertThat(mediaItem.localConfiguration.subtitleConfigurations).isEqualTo(subtitles); + assertThat(mediaItem.localConfiguration.subtitles).isEqualTo(subtitles); } @Test From 364239ac8cb17bb068e935d9247ea5b5558d3253 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 8 Nov 2021 13:02:44 +0000 Subject: [PATCH 21/41] Remove usages of ParserException from the demo app PiperOrigin-RevId: 408311942 --- .../android/exoplayer2/demo/SampleChooserActivity.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 2f51451fb3..b79a7a62ca 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -45,7 +45,6 @@ import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem.ClippingConfiguration; import com.google.android.exoplayer2.MediaMetadata; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; @@ -329,8 +328,7 @@ public class SampleChooserActivity extends AppCompatActivity reader.nextString(); // Ignore. break; default: - throw ParserException.createForMalformedManifest( - "Unsupported name: " + name, /* cause= */ null); + throw new IOException("Unsupported name: " + name, /* cause= */ null); } } reader.endObject(); @@ -424,8 +422,7 @@ public class SampleChooserActivity extends AppCompatActivity reader.endArray(); break; default: - throw ParserException.createForMalformedManifest( - "Unsupported attribute name: " + name, /* cause= */ null); + throw new IOException("Unsupported attribute name: " + name, /* cause= */ null); } } reader.endObject(); From d06e8136ee6dc5ff86a30953de7a9934d56584d8 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 8 Nov 2021 13:53:17 +0000 Subject: [PATCH 22/41] Add @Deprecated to MediaSourceFactory deprecated overrides This is needed to ensure the deprecation warning appears on usages in Android Studio and in javadoc. #minor-release PiperOrigin-RevId: 408319182 --- .../android/exoplayer2/source/DefaultMediaSourceFactory.java | 3 +++ .../android/exoplayer2/source/ProgressiveMediaSource.java | 4 ++++ .../android/exoplayer2/source/dash/DashMediaSource.java | 3 +++ .../google/android/exoplayer2/source/hls/HlsMediaSource.java | 3 +++ .../exoplayer2/source/smoothstreaming/SsMediaSource.java | 3 +++ 5 files changed, 16 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index 0cbae12170..e24598032d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -294,6 +294,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { return this; } + @Deprecated @Override public DefaultMediaSourceFactory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -301,12 +302,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { return this; } + @Deprecated @Override public DefaultMediaSourceFactory setDrmUserAgent(@Nullable String userAgent) { delegateFactoryLoader.setDrmUserAgent(userAgent); return this; } + @Deprecated @Override public DefaultMediaSourceFactory setDrmSessionManager( @Nullable DrmSessionManager drmSessionManager) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 67d019ccee..1adc8e8648 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -175,6 +175,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } + @Deprecated + @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { if (drmSessionManager == null) { setDrmSessionManagerProvider(null); @@ -184,6 +186,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -194,6 +197,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmUserAgent(@Nullable String userAgent) { if (!usingCustomDrmSessionManagerProvider) { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index fac44d9a19..5b901bc092 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -178,6 +178,7 @@ public final class DashMediaSource extends BaseMediaSource { return this; } + @Deprecated @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { if (drmSessionManager == null) { @@ -188,6 +189,7 @@ public final class DashMediaSource extends BaseMediaSource { return this; } + @Deprecated @Override public Factory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -198,6 +200,7 @@ public final class DashMediaSource extends BaseMediaSource { return this; } + @Deprecated @Override public Factory setDrmUserAgent(@Nullable String userAgent) { if (!usingCustomDrmSessionManagerProvider) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 56ebfb5762..687d1bb0dd 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -297,6 +297,7 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { if (drmSessionManager == null) { @@ -307,6 +308,7 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -317,6 +319,7 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmUserAgent(@Nullable String userAgent) { if (!usingCustomDrmSessionManagerProvider) { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index fd2f3a49fe..5295bbab5b 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -205,6 +205,7 @@ public final class SsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { if (drmSessionManager == null) { @@ -215,6 +216,7 @@ public final class SsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -225,6 +227,7 @@ public final class SsMediaSource extends BaseMediaSource return this; } + @Deprecated @Override public Factory setDrmUserAgent(@Nullable String userAgent) { if (!usingCustomDrmSessionManagerProvider) { From 5de4915fe49cfc4040410efbb2f8a30716f1b46e Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Nov 2021 14:14:38 +0000 Subject: [PATCH 23/41] Add missing @Nullable for ExoPlayer.getPlayerError. #minor-release Issue: google/ExoPlayer#9660 PiperOrigin-RevId: 408323173 --- .../src/main/java/com/google/android/exoplayer2/ExoPlayer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 2143d2a1fb..39db65a02a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -982,6 +982,7 @@ public interface ExoPlayer extends Player { * {@link ExoPlaybackException}. */ @Override + @Nullable ExoPlaybackException getPlayerError(); /** From 1eca6700ae66978d9f78d9b9a841007acec334d1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 8 Nov 2021 14:40:52 +0000 Subject: [PATCH 24/41] Rollback of https://github.com/google/ExoPlayer/commit/95e6db931a047775c1aa792c452be7996167a08f *** Original commit *** Add link to annual media developer survey. This will be removed after the survey has closed in ~1 month. *** PiperOrigin-RevId: 408327757 --- docs/index.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index 4a512678e1..b28743192f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,6 @@ --- layout: article --- -The Android media team is interested in your experiences with the Android media -APIs and developer resources. Please provide your feedback by -[completing this short survey](https://goo.gle/media-survey-6). -{:.info} ExoPlayer is an application level media player for Android. It provides an alternative to Android’s MediaPlayer API for playing audio and video both From ba9ade1c8edb7d9b12687244d08d37135777f4d3 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 8 Nov 2021 15:03:42 +0000 Subject: [PATCH 25/41] Update exoplayer.dev copyright notice to 2021 #minor-release PiperOrigin-RevId: 408331834 --- docs/_data/locale.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/_data/locale.yml b/docs/_data/locale.yml index 3871545994..c3fc356594 100644 --- a/docs/_data/locale.yml +++ b/docs/_data/locale.yml @@ -18,7 +18,7 @@ en: &EN FOLLOW_US : "Follow us on [NAME]." EMAIL_ME : "Send me Email." EMAIL_US : "Send us Email." - COPYRIGHT_DATES : "2019" + COPYRIGHT_DATES : "2021" en-GB: <<: *EN @@ -49,7 +49,7 @@ zh-Hans: &ZH_HANS FOLLOW_US : "在 [NAME] 上关注我们。" EMAIL_ME : "给我发邮件。" EMAIL_US : "给我们发邮件。" - COPYRIGHT_DATES : "2019" + COPYRIGHT_DATES : "2021" zh: <<: *ZH_HANS @@ -78,7 +78,7 @@ zh-Hant: &ZH_HANT FOLLOW_US : "在 [NAME] 上關注我們。" EMAIL_ME : "給我發郵件。" EMAIL_US : "給我們發郵件。" - COPYRIGHT_DATES : "2019" + COPYRIGHT_DATES : "2021" zh-TW: <<: *ZH_HANT @@ -105,7 +105,7 @@ ko: &KO FOLLOW_US : "[NAME]에서 팔로우하기" EMAIL_ME : "이메일 보내기" EMAIL_US : "이메일 보내기" - COPYRIGHT_DATES : "2019" + COPYRIGHT_DATES : "2021" ko-KR: <<: *KO From 9efa32e49b998c1b2ffd8dcea115138930e76cbe Mon Sep 17 00:00:00 2001 From: hschlueter Date: Mon, 8 Nov 2021 15:52:01 +0000 Subject: [PATCH 26/41] Accumulate remainder in buffer duration calculations. When dropping the remainder, the decoder and encoder timestamps start diverging after a few buffers when no speed changes are supposed to occur. Tracking the remainder keeps them in sync. PiperOrigin-RevId: 408341074 --- .../transformer/AudioSamplePipeline.java | 22 +++++++-- .../mp4/sample_sef_slow_motion.mp4.dump | 48 +++++++++---------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java index 167d1725bc..d3d4a12092 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java @@ -31,7 +31,9 @@ import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; import com.google.android.exoplayer2.audio.SonicAudioProcessor; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.common.math.LongMath; import java.io.IOException; +import java.math.RoundingMode; import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -62,6 +64,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private @MonotonicNonNull AudioFormat encoderInputAudioFormat; private @MonotonicNonNull MediaCodecAdapterWrapper encoder; private long nextEncoderInputBufferTimeUs; + private long encoderBufferDurationRemainder; private ByteBuffer sonicOutputBuffer; private boolean drainingSonicForSpeedChange; @@ -82,6 +85,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sonicAudioProcessor = new SonicAudioProcessor(); sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; nextEncoderInputBufferTimeUs = 0; + encoderBufferDurationRemainder = 0; speedProvider = new SegmentSpeedProvider(decoderInputFormat); currentSpeed = speedProvider.getSpeed(0); try { @@ -269,7 +273,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; encoderInputBufferData.put(inputBuffer); encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; nextEncoderInputBufferTimeUs += - getBufferDurationUs( + getEncoderBufferDurationUs( /* bytesWritten= */ encoderInputBufferData.position(), encoderInputAudioFormat.bytesPerFrame, encoderInputAudioFormat.sampleRate); @@ -366,9 +370,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; errorCode); } - // TODO(internal b/204978301): Ensure encoder and decoder timestamps match when no speed change. - private static long getBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) { - long framesWritten = bytesWritten / bytesPerFrame; - return framesWritten * C.MICROS_PER_SECOND / sampleRate; + private long getEncoderBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) { + // The calculation below accounts for remainders and rounding. Without that it corresponds to + // the following: + // bufferDurationUs = numberOfFramesInBuffer * sampleDurationUs + // where numberOfFramesInBuffer = bytesWritten / bytesPerFrame + // and sampleDurationUs = C.MICROS_PER_SECOND / sampleRate + long framesWrittenMicrosPerSecond = + bytesWritten * C.MICROS_PER_SECOND / bytesPerFrame + encoderBufferDurationRemainder; + long bufferDurationUs = + LongMath.divide(framesWrittenMicrosPerSecond, sampleRate, RoundingMode.CEILING); + encoderBufferDurationRemainder = framesWrittenMicrosPerSecond - bufferDurationUs * sampleRate; + return bufferDurationUs; } } diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump b/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump index 707be77d53..816e26e384 100644 --- a/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump +++ b/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump @@ -135,145 +135,145 @@ sample: dataHashCode = 1000136444 size = 140 isKeyFrame = true - presentationTimeUs = 416 + presentationTimeUs = 417 sample: trackIndex = 0 dataHashCode = 217961709 size = 172 isKeyFrame = true - presentationTimeUs = 3332 + presentationTimeUs = 3334 sample: trackIndex = 0 dataHashCode = -879376936 size = 176 isKeyFrame = true - presentationTimeUs = 6915 + presentationTimeUs = 6917 sample: trackIndex = 0 dataHashCode = 1259979587 size = 192 isKeyFrame = true - presentationTimeUs = 10581 + presentationTimeUs = 10584 sample: trackIndex = 0 dataHashCode = 907407225 size = 188 isKeyFrame = true - presentationTimeUs = 14581 + presentationTimeUs = 14584 sample: trackIndex = 0 dataHashCode = -904354707 size = 176 isKeyFrame = true - presentationTimeUs = 18497 + presentationTimeUs = 18500 sample: trackIndex = 0 dataHashCode = 1001385853 size = 172 isKeyFrame = true - presentationTimeUs = 22163 + presentationTimeUs = 22167 sample: trackIndex = 0 dataHashCode = 1545716086 size = 196 isKeyFrame = true - presentationTimeUs = 25746 + presentationTimeUs = 25750 sample: trackIndex = 0 dataHashCode = 358710839 size = 180 isKeyFrame = true - presentationTimeUs = 29829 + presentationTimeUs = 29834 sample: trackIndex = 0 dataHashCode = -671124798 size = 140 isKeyFrame = true - presentationTimeUs = 33579 + presentationTimeUs = 33584 sample: trackIndex = 0 dataHashCode = -945404910 size = 120 isKeyFrame = true - presentationTimeUs = 36495 + presentationTimeUs = 36500 sample: trackIndex = 0 dataHashCode = 1881048379 size = 88 isKeyFrame = true - presentationTimeUs = 38995 + presentationTimeUs = 39000 sample: trackIndex = 0 dataHashCode = 1059579897 size = 88 isKeyFrame = true - presentationTimeUs = 40828 + presentationTimeUs = 40834 sample: trackIndex = 0 dataHashCode = 1496098648 size = 84 isKeyFrame = true - presentationTimeUs = 42661 + presentationTimeUs = 42667 sample: trackIndex = 0 dataHashCode = 250093960 size = 751 isKeyFrame = true - presentationTimeUs = 44411 + presentationTimeUs = 44417 sample: trackIndex = 0 dataHashCode = 1895536226 size = 1045 isKeyFrame = true - presentationTimeUs = 59994 + presentationTimeUs = 60063 sample: trackIndex = 0 dataHashCode = 1723596464 size = 947 isKeyFrame = true - presentationTimeUs = 81744 + presentationTimeUs = 81834 sample: trackIndex = 0 dataHashCode = -978803114 size = 946 isKeyFrame = true - presentationTimeUs = 101410 + presentationTimeUs = 101563 sample: trackIndex = 0 dataHashCode = 387377078 size = 946 isKeyFrame = true - presentationTimeUs = 121076 + presentationTimeUs = 121271 sample: trackIndex = 0 dataHashCode = -132658698 size = 901 isKeyFrame = true - presentationTimeUs = 140742 + presentationTimeUs = 140980 sample: trackIndex = 0 dataHashCode = 1495036471 size = 899 isKeyFrame = true - presentationTimeUs = 159492 + presentationTimeUs = 159750 sample: trackIndex = 0 dataHashCode = 304440590 size = 878 isKeyFrame = true - presentationTimeUs = 178158 + presentationTimeUs = 178480 sample: trackIndex = 0 dataHashCode = -1955900344 size = 112 isKeyFrame = true - presentationTimeUs = 196408 + presentationTimeUs = 196771 sample: trackIndex = 0 dataHashCode = 88896626 size = 116 isKeyFrame = true - presentationTimeUs = 198741 + presentationTimeUs = 199105 sample: trackIndex = 1 dataHashCode = 2139021989 From 4082d8a63dcf0fcbc41558de044dbb04bd173a17 Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 9 Nov 2021 09:52:45 +0000 Subject: [PATCH 27/41] WavExtractor: skip unknown chunks consistently #minor-release PiperOrigin-RevId: 408550935 --- .../extractor/wav/WavHeaderReader.java | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index 4541a305d6..f05a510191 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -71,14 +71,8 @@ import java.io.IOException; public static WavFormat readFormat(ExtractorInput input) throws IOException { // Allocate a scratch buffer large enough to store the format chunk. ParsableByteArray scratch = new ParsableByteArray(16); - // Skip chunks until we find the format chunk. - ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != WavUtil.FMT_FOURCC) { - input.skipFully(ChunkHeader.SIZE_IN_BYTES + (int) chunkHeader.size); - chunkHeader = ChunkHeader.peek(input, scratch); - } - + ChunkHeader chunkHeader = skipToChunk(/* chunkId= */ WavUtil.FMT_FOURCC, input, scratch); Assertions.checkState(chunkHeader.size >= 16); input.peekFully(scratch.getData(), 0, 16); scratch.setPosition(0); @@ -112,7 +106,8 @@ import java.io.IOException; /** * Skips to the data in the given WAV input stream, and returns its bounds. After calling, the * input stream's position will point to the start of sample data in the WAV. If an exception is - * thrown, the input position will be left pointing to a chunk header. + * thrown, the input position will be left pointing to a chunk header (that may not be the data + * chunk header). * * @param input The input stream, whose read position must be pointing to a valid chunk header. * @return The byte positions at which the data starts (inclusive) and ends (exclusive). @@ -125,17 +120,7 @@ import java.io.IOException; ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we find the data header. - ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != WavUtil.DATA_FOURCC) { - Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); - long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; - if (bytesToSkip > Integer.MAX_VALUE) { - throw ParserException.createForUnsupportedContainerFeature( - "Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id); - } - input.skipFully((int) bytesToSkip); - chunkHeader = ChunkHeader.peek(input, scratch); - } + ChunkHeader chunkHeader = skipToChunk(/* chunkId= */ WavUtil.DATA_FOURCC, input, scratch); // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); @@ -149,6 +134,35 @@ import java.io.IOException; return Pair.create(dataStartPosition, dataEndPosition); } + /** + * Skips to the chunk header corresponding to the {@code chunkId} provided. After calling, the + * input stream's position will point to the chunk header with provided {@code chunkId} and the + * peek position to the chunk body. If an exception is thrown, the input position will be left + * pointing to a chunk header (that may not be the one corresponding to the {@code chunkId}). + * + * @param chunkId The ID of the chunk to skip to. + * @param input The input stream, whose read position must be pointing to a valid chunk header. + * @param scratch A scratch buffer to read the chunk headers. + * @return The {@link ChunkHeader} corresponding to the {@code chunkId} provided. + * @throws ParserException If an error occurs parsing chunks. + * @throws IOException If reading from the input fails. + */ + private static ChunkHeader skipToChunk( + int chunkId, ExtractorInput input, ParsableByteArray scratch) throws IOException { + ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); + while (chunkHeader.id != chunkId) { + Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; + if (bytesToSkip > Integer.MAX_VALUE) { + throw ParserException.createForUnsupportedContainerFeature( + "Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id); + } + input.skipFully((int) bytesToSkip); + chunkHeader = ChunkHeader.peek(input, scratch); + } + return chunkHeader; + } + private WavHeaderReader() { // Prevent instantiation. } From 30c77e8e1cd1c9fda619cea66933b46c86816f13 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 9 Nov 2021 15:04:39 +0000 Subject: [PATCH 28/41] Refactor buffer duration calculation for clarity. Follow-up to address comments from https://github.com/google/ExoPlayer/commit/9efa32e49b998c1b2ffd8dcea115138930e76cbe. PiperOrigin-RevId: 408600470 --- .../transformer/AudioSamplePipeline.java | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java index d3d4a12092..32f88bfa5f 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java @@ -31,9 +31,7 @@ import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; import com.google.android.exoplayer2.audio.SonicAudioProcessor; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.common.math.LongMath; import java.io.IOException; -import java.math.RoundingMode; import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -84,8 +82,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); sonicAudioProcessor = new SonicAudioProcessor(); sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; - nextEncoderInputBufferTimeUs = 0; - encoderBufferDurationRemainder = 0; speedProvider = new SegmentSpeedProvider(decoderInputFormat); currentSpeed = speedProvider.getSpeed(0); try { @@ -272,11 +268,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; inputBuffer.limit(min(bufferLimit, inputBuffer.position() + encoderInputBufferData.capacity())); encoderInputBufferData.put(inputBuffer); encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs; - nextEncoderInputBufferTimeUs += - getEncoderBufferDurationUs( - /* bytesWritten= */ encoderInputBufferData.position(), - encoderInputAudioFormat.bytesPerFrame, - encoderInputAudioFormat.sampleRate); + computeNextEncoderInputBufferTimeUs( + /* bytesWritten= */ encoderInputBufferData.position(), + encoderInputAudioFormat.bytesPerFrame, + encoderInputAudioFormat.sampleRate); encoderInputBuffer.setFlags(0); encoderInputBuffer.flip(); inputBuffer.limit(bufferLimit); @@ -370,17 +365,21 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; errorCode); } - private long getEncoderBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) { + private void computeNextEncoderInputBufferTimeUs( + long bytesWritten, int bytesPerFrame, int sampleRate) { // The calculation below accounts for remainders and rounding. Without that it corresponds to // the following: // bufferDurationUs = numberOfFramesInBuffer * sampleDurationUs // where numberOfFramesInBuffer = bytesWritten / bytesPerFrame // and sampleDurationUs = C.MICROS_PER_SECOND / sampleRate - long framesWrittenMicrosPerSecond = - bytesWritten * C.MICROS_PER_SECOND / bytesPerFrame + encoderBufferDurationRemainder; - long bufferDurationUs = - LongMath.divide(framesWrittenMicrosPerSecond, sampleRate, RoundingMode.CEILING); - encoderBufferDurationRemainder = framesWrittenMicrosPerSecond - bufferDurationUs * sampleRate; - return bufferDurationUs; + long numerator = bytesWritten * C.MICROS_PER_SECOND + encoderBufferDurationRemainder; + long denominator = (long) bytesPerFrame * sampleRate; + long bufferDurationUs = numerator / denominator; + encoderBufferDurationRemainder = numerator - bufferDurationUs * denominator; + if (encoderBufferDurationRemainder > 0) { // Ceil division result. + bufferDurationUs += 1; + encoderBufferDurationRemainder -= denominator; + } + nextEncoderInputBufferTimeUs += bufferDurationUs; } } From f633e76c156d0708bf2996584c167bbf6ae97286 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 9 Nov 2021 16:23:43 +0000 Subject: [PATCH 29/41] Plumb PlayerId to renderers. We can rename the existing setIndex method to a more generic init as this method is only called by EPII and implemented by BaseRenderer anyway. PiperOrigin-RevId: 408616055 --- .../android/exoplayer2/BaseRenderer.java | 22 +++++++++++++++++-- .../android/exoplayer2/ExoPlayerImpl.java | 17 +++++++++++++- .../exoplayer2/ExoPlayerImplInternal.java | 6 +++-- .../android/exoplayer2/NoSampleRenderer.java | 3 ++- .../google/android/exoplayer2/Renderer.java | 8 ++++--- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 91330e7be7..683db1ecbc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static java.lang.Math.max; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer.InsufficientCapacityException; import com.google.android.exoplayer2.source.SampleStream; @@ -26,6 +28,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadFlags; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** An abstract base class suitable for most {@link Renderer} implementations. */ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @@ -35,6 +38,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Nullable private RendererConfiguration configuration; private int index; + private @MonotonicNonNull PlayerId playerId; private int state; @Nullable private SampleStream stream; @Nullable private Format[] streamFormats; @@ -65,8 +69,9 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override - public final void setIndex(int index) { + public final void init(int index, PlayerId playerId) { this.index = index; + this.playerId = playerId; } @Override @@ -328,11 +333,24 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return Assertions.checkNotNull(configuration); } - /** Returns the index of the renderer within the player. */ + /** + * Returns the index of the renderer within the player. + * + *

Must only be used after the renderer has been initialized by the player. + */ protected final int getIndex() { return index; } + /** + * Returns the {@link PlayerId} of the player using this renderer. + * + *

Must only be used after the renderer has been initialized by the player. + */ + protected final PlayerId getPlayerId() { + return checkNotNull(playerId); + } + /** * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for * this renderer. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 61a1d41b82..f3043f9d1a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,6 +22,7 @@ import static java.lang.Math.max; import static java.lang.Math.min; import android.annotation.SuppressLint; +import android.media.metrics.LogSessionId; import android.os.Handler; import android.os.Looper; import android.util.Pair; @@ -30,9 +31,11 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.ExoPlayer.AudioOffloadListener; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaSource; @@ -246,6 +249,7 @@ import java.util.concurrent.CopyOnWriteArraySet; addListener(analyticsCollector); bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector); } + PlayerId playerId = Util.SDK_INT < 31 ? new PlayerId() : Api31.createPlayerId(); internalPlayer = new ExoPlayerImplInternal( renderers, @@ -262,7 +266,8 @@ import java.util.concurrent.CopyOnWriteArraySet; pauseAtEndOfMediaItems, applicationLooper, clock, - playbackInfoUpdateListener); + playbackInfoUpdateListener, + playerId); } /** @@ -1856,4 +1861,14 @@ import java.util.concurrent.CopyOnWriteArraySet; return timeline; } } + + @RequiresApi(31) + private static final class Api31 { + private Api31() {} + + public static PlayerId createPlayerId() { + // TODO: Create a MediaMetricsListener and obtain LogSessionId from it. + return new PlayerId(LogSessionId.LOG_SESSION_ID_NONE); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index d6f1e1f73a..6c766045ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.BehindLiveWindowException; @@ -229,7 +230,8 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean pauseAtEndOfWindow, Looper applicationLooper, Clock clock, - PlaybackInfoUpdateListener playbackInfoUpdateListener) { + PlaybackInfoUpdateListener playbackInfoUpdateListener, + PlayerId playerId) { this.playbackInfoUpdateListener = playbackInfoUpdateListener; this.renderers = renderers; this.trackSelector = trackSelector; @@ -252,7 +254,7 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { - renderers[i].setIndex(i); + renderers[i].init(/* index= */ i, playerId); rendererCapabilities[i] = renderers[i].getCapabilities(); } mediaClock = new DefaultMediaClock(this, clock); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 0038b9ab8f..9be14c71b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; @@ -45,7 +46,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities } @Override - public final void setIndex(int index) { + public final void init(int index, PlayerId playerId) { this.index = index; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index b7b8f6bb3c..efefe4c61c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -20,6 +20,7 @@ import android.view.Surface; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.source.SampleStream; @@ -248,11 +249,12 @@ public interface Renderer extends PlayerMessage.Target { RendererCapabilities getCapabilities(); /** - * Sets the index of this renderer within the player. + * Initializes the renderer for playback with a player. * - * @param index The renderer index. + * @param index The renderer index within the player. + * @param playerId The {@link PlayerId} of the player. */ - void setIndex(int index); + void init(int index, PlayerId playerId); /** * If the renderer advances its own playback position then this method returns a corresponding From 5ae60f2be7b5154d3da45519a4da3f3a4e24a96d Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 9 Nov 2021 16:41:40 +0000 Subject: [PATCH 30/41] Split VideoSamplePipeline from TransformerTranscodingVideoRenderer. The `VideoSamplePipeline` handles all steps from decoding to re-encoding that where previously in `TransformerTranscodingVideoRenderer`. The renderer is now only responsible for reading the format, reading input, passing it to the pipeline and passing the pipeline's output to the muxer. When no transformations are needed, decoding and re-encoding is skipped using the `PassthroughPipeline`. PiperOrigin-RevId: 408619407 --- .../transformer/AudioSamplePipeline.java | 5 +- .../TransformerTranscodingVideoRenderer.java | 394 ++++-------------- .../transformer/VideoSamplePipeline.java | 335 +++++++++++++++ 3 files changed, 415 insertions(+), 319 deletions(-) create mode 100644 library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java index 32f88bfa5f..a380b8e55b 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/AudioSamplePipeline.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.transformer; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; import static java.lang.Math.min; import android.media.MediaCodec.BufferInfo; @@ -157,9 +158,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public void releaseOutputBuffer() { - if (encoder != null) { - encoder.releaseOutputBuffer(); - } + checkStateNotNull(encoder).releaseOutputBuffer(); } /** diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java index 80b5ada468..30e5125f17 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerTranscodingVideoRenderer.java @@ -16,33 +16,18 @@ package com.google.android.exoplayer2.transformer; +import static com.google.android.exoplayer2.source.SampleStream.FLAG_REQUIRE_FORMAT; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; import android.content.Context; -import android.graphics.SurfaceTexture; -import android.media.MediaCodec; -import android.opengl.EGL14; -import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLExt; -import android.opengl.EGLSurface; -import android.opengl.GLES20; -import android.view.Surface; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.util.GlUtil; -import com.google.common.collect.ImmutableMap; -import java.io.IOException; -import java.nio.ByteBuffer; -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -50,42 +35,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresApi(18) /* package */ final class TransformerTranscodingVideoRenderer extends TransformerBaseRenderer { - static { - GlUtil.glAssertionsEnabled = true; - } - private static final String TAG = "TransformerTranscodingVideoRenderer"; - // Predefined shader values. - private static final String VERTEX_SHADER_FILE_PATH = "shaders/blit_vertex_shader.glsl"; - private static final String FRAGMENT_SHADER_FILE_PATH = - "shaders/copy_external_fragment_shader.glsl"; - private static final int EXPECTED_NUMBER_OF_ATTRIBUTES = 2; - private static final int EXPECTED_NUMBER_OF_UNIFORMS = 2; - private final Context context; - private final DecoderInputBuffer decoderInputBuffer; - private final float[] decoderTextureTransformMatrix; - - private @MonotonicNonNull Format decoderInputFormat; - - @Nullable private EGLDisplay eglDisplay; - @Nullable private EGLContext eglContext; - @Nullable private EGLSurface eglSurface; - - private int decoderTextureId; - @Nullable private SurfaceTexture decoderSurfaceTexture; - @Nullable private Surface decoderSurface; - @Nullable private MediaCodecAdapterWrapper decoder; - private volatile boolean isDecoderSurfacePopulated; - private boolean waitingForPopulatedDecoderSurface; - @Nullable private GlUtil.Uniform decoderTextureTransformUniform; - - @Nullable private MediaCodecAdapterWrapper encoder; - /** Whether encoder's actual output format is obtained. */ - private boolean hasEncoderActualOutputFormat; + private @MonotonicNonNull SamplePipeline samplePipeline; + private boolean muxerWrapperTrackAdded; private boolean muxerWrapperTrackEnded; public TransformerTranscodingVideoRenderer( @@ -95,9 +51,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; Transformation transformation) { super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation); this.context = context; - decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); - decoderTextureTransformMatrix = new float[16]; - decoderTextureId = GlUtil.TEXTURE_ID_UNSET; + decoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); } @Override @@ -105,34 +60,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return TAG; } - @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (!isRendererStarted || isEnded() || !ensureInputFormatRead()) { - return; - } - ensureEncoderConfigured(); - MediaCodecAdapterWrapper encoder = this.encoder; - ensureOpenGlConfigured(); - EGLDisplay eglDisplay = this.eglDisplay; - EGLSurface eglSurface = this.eglSurface; - GlUtil.Uniform decoderTextureTransformUniform = this.decoderTextureTransformUniform; - if (!ensureDecoderConfigured()) { - return; - } - MediaCodecAdapterWrapper decoder = this.decoder; - SurfaceTexture decoderSurfaceTexture = this.decoderSurfaceTexture; - - while (feedMuxerFromEncoder(encoder)) {} - while (feedEncoderFromDecoder( - decoder, - encoder, - decoderSurfaceTexture, - eglDisplay, - eglSurface, - decoderTextureTransformUniform)) {} - while (feedDecoderFromInput(decoder)) {} - } - @Override public boolean isEnded() { return muxerWrapperTrackEnded; @@ -140,272 +67,107 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override protected void onReset() { - decoderInputBuffer.clear(); - decoderInputBuffer.data = null; - GlUtil.destroyEglContext(eglDisplay, eglContext); - eglDisplay = null; - eglContext = null; - eglSurface = null; - if (decoderTextureId != GlUtil.TEXTURE_ID_UNSET) { - GlUtil.deleteTexture(decoderTextureId); + if (samplePipeline != null) { + samplePipeline.release(); } - if (decoderSurfaceTexture != null) { - decoderSurfaceTexture.release(); - decoderSurfaceTexture = null; - } - if (decoderSurface != null) { - decoderSurface.release(); - decoderSurface = null; - } - if (decoder != null) { - decoder.release(); - decoder = null; - } - isDecoderSurfacePopulated = false; - waitingForPopulatedDecoderSurface = false; - decoderTextureTransformUniform = null; - if (encoder != null) { - encoder.release(); - encoder = null; - } - hasEncoderActualOutputFormat = false; + muxerWrapperTrackAdded = false; muxerWrapperTrackEnded = false; } - @EnsuresNonNullIf(expression = "decoderInputFormat", result = true) - private boolean ensureInputFormatRead() { - if (decoderInputFormat != null) { + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (!isRendererStarted || isEnded() || !ensureRendererConfigured()) { + return; + } + + while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {} + } + + /** Attempts to read the input format and to initialize the sample pipeline. */ + @EnsuresNonNullIf(expression = "samplePipeline", result = true) + private boolean ensureRendererConfigured() throws ExoPlaybackException { + if (samplePipeline != null) { return true; } FormatHolder formatHolder = getFormatHolder(); - @SampleStream.ReadDataResult - int result = - readSource( - formatHolder, decoderInputBuffer, /* readFlags= */ SampleStream.FLAG_REQUIRE_FORMAT); + @ReadDataResult + int result = readSource(formatHolder, decoderInputBuffer, /* readFlags= */ FLAG_REQUIRE_FORMAT); if (result != C.RESULT_FORMAT_READ) { return false; } - decoderInputFormat = checkNotNull(formatHolder.format); + Format decoderInputFormat = checkNotNull(formatHolder.format); + if (transformation.videoMimeType != null + && !transformation.videoMimeType.equals(decoderInputFormat.sampleMimeType)) { + samplePipeline = + new VideoSamplePipeline(context, decoderInputFormat, transformation, getIndex()); + } else { + samplePipeline = new PassthroughSamplePipeline(decoderInputFormat); + } return true; } - @RequiresNonNull({"decoderInputFormat"}) - @EnsuresNonNull({"encoder"}) - private void ensureEncoderConfigured() throws ExoPlaybackException { - if (encoder != null) { - return; - } - - try { - encoder = - MediaCodecAdapterWrapper.createForVideoEncoding( - new Format.Builder() - .setWidth(decoderInputFormat.width) - .setHeight(decoderInputFormat.height) - .setSampleMimeType( - transformation.videoMimeType != null - ? transformation.videoMimeType - : decoderInputFormat.sampleMimeType) - .build(), - ImmutableMap.of()); - } catch (IOException e) { - throw createRendererException( - // TODO(claincly): should be "ENCODER_INIT_FAILED" - e, decoderInputFormat, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); - } - } - - @RequiresNonNull({"encoder", "decoderInputFormat"}) - @EnsuresNonNull({"eglDisplay", "eglSurface", "decoderTextureTransformUniform"}) - private void ensureOpenGlConfigured() { - if (eglDisplay != null && eglSurface != null && decoderTextureTransformUniform != null) { - return; - } - - MediaCodecAdapterWrapper encoder = this.encoder; - EGLDisplay eglDisplay = GlUtil.createEglDisplay(); - EGLContext eglContext; - try { - eglContext = GlUtil.createEglContext(eglDisplay); - this.eglContext = eglContext; - } catch (GlUtil.UnsupportedEglVersionException e) { - throw new IllegalStateException("EGL version is unsupported", e); - } - EGLSurface eglSurface = - GlUtil.getEglSurface(eglDisplay, checkNotNull(encoder.getInputSurface())); - GlUtil.focusSurface( - eglDisplay, eglContext, eglSurface, decoderInputFormat.width, decoderInputFormat.height); - decoderTextureId = GlUtil.createExternalTexture(); - GlUtil.Program copyProgram; - try { - copyProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH); - } catch (IOException e) { - throw new IllegalStateException(e); - } - - copyProgram.use(); - GlUtil.Attribute[] copyAttributes = copyProgram.getAttributes(); - checkState( - copyAttributes.length == EXPECTED_NUMBER_OF_ATTRIBUTES, - "Expected program to have " + EXPECTED_NUMBER_OF_ATTRIBUTES + " vertex attributes."); - for (GlUtil.Attribute copyAttribute : copyAttributes) { - if (copyAttribute.name.equals("a_position")) { - copyAttribute.setBuffer( - new float[] { - -1.0f, -1.0f, 0.0f, 1.0f, - 1.0f, -1.0f, 0.0f, 1.0f, - -1.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 1.0f, 0.0f, 1.0f, - }, - /* size= */ 4); - } else if (copyAttribute.name.equals("a_texcoord")) { - copyAttribute.setBuffer( - new float[] { - 0.0f, 0.0f, 0.0f, 1.0f, - 1.0f, 0.0f, 0.0f, 1.0f, - 0.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 1.0f, 0.0f, 1.0f, - }, - /* size= */ 4); - } else { - throw new IllegalStateException("Unexpected attribute name."); - } - copyAttribute.bind(); - } - GlUtil.Uniform[] copyUniforms = copyProgram.getUniforms(); - checkState( - copyUniforms.length == EXPECTED_NUMBER_OF_UNIFORMS, - "Expected program to have " + EXPECTED_NUMBER_OF_UNIFORMS + " uniforms."); - for (GlUtil.Uniform copyUniform : copyUniforms) { - if (copyUniform.name.equals("tex_sampler")) { - copyUniform.setSamplerTexId(decoderTextureId, 0); - copyUniform.bind(); - } else if (copyUniform.name.equals("tex_transform")) { - decoderTextureTransformUniform = copyUniform; - } else { - throw new IllegalStateException("Unexpected uniform name."); - } - } - checkNotNull(decoderTextureTransformUniform); - this.eglDisplay = eglDisplay; - this.eglSurface = eglSurface; - } - - @RequiresNonNull({"decoderInputFormat"}) - @EnsuresNonNullIf( - expression = {"decoder", "decoderSurfaceTexture"}, - result = true) - private boolean ensureDecoderConfigured() throws ExoPlaybackException { - if (decoder != null && decoderSurfaceTexture != null) { - return true; - } - - checkState(decoderTextureId != GlUtil.TEXTURE_ID_UNSET); - SurfaceTexture decoderSurfaceTexture = new SurfaceTexture(decoderTextureId); - decoderSurfaceTexture.setOnFrameAvailableListener( - surfaceTexture -> isDecoderSurfacePopulated = true); - decoderSurface = new Surface(decoderSurfaceTexture); - try { - decoder = MediaCodecAdapterWrapper.createForVideoDecoding(decoderInputFormat, decoderSurface); - } catch (IOException e) { - throw createRendererException( - e, decoderInputFormat, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); - } - this.decoderSurfaceTexture = decoderSurfaceTexture; - return true; - } - - private boolean feedDecoderFromInput(MediaCodecAdapterWrapper decoder) { - if (!decoder.maybeDequeueInputBuffer(decoderInputBuffer)) { - return false; - } - - decoderInputBuffer.clear(); - @SampleStream.ReadDataResult - int result = readSource(getFormatHolder(), decoderInputBuffer, /* readFlags= */ 0); - switch (result) { - case C.RESULT_FORMAT_READ: - throw new IllegalStateException("Format changes are not supported."); - case C.RESULT_BUFFER_READ: - mediaClock.updateTimeForTrackType(getTrackType(), decoderInputBuffer.timeUs); - decoderInputBuffer.timeUs -= streamOffsetUs; - ByteBuffer data = checkNotNull(decoderInputBuffer.data); - data.flip(); - decoder.queueInputBuffer(decoderInputBuffer); - return !decoderInputBuffer.isEndOfStream(); - case C.RESULT_NOTHING_READ: - default: - return false; - } - } - - private boolean feedEncoderFromDecoder( - MediaCodecAdapterWrapper decoder, - MediaCodecAdapterWrapper encoder, - SurfaceTexture decoderSurfaceTexture, - EGLDisplay eglDisplay, - EGLSurface eglSurface, - GlUtil.Uniform decoderTextureTransformUniform) { - if (decoder.isEnded()) { - return false; - } - - if (!isDecoderSurfacePopulated) { - if (!waitingForPopulatedDecoderSurface) { - if (decoder.getOutputBufferInfo() != null) { - decoder.releaseOutputBuffer(/* render= */ true); - waitingForPopulatedDecoderSurface = true; - } - if (decoder.isEnded()) { - encoder.signalEndOfInputStream(); - } - } - return false; - } - - waitingForPopulatedDecoderSurface = false; - decoderSurfaceTexture.updateTexImage(); - decoderSurfaceTexture.getTransformMatrix(decoderTextureTransformMatrix); - decoderTextureTransformUniform.setFloats(decoderTextureTransformMatrix); - decoderTextureTransformUniform.bind(); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - long decoderSurfaceTextureTimestampNs = decoderSurfaceTexture.getTimestamp(); - EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, decoderSurfaceTextureTimestampNs); - EGL14.eglSwapBuffers(eglDisplay, eglSurface); - isDecoderSurfacePopulated = false; - return true; - } - - private boolean feedMuxerFromEncoder(MediaCodecAdapterWrapper encoder) { - if (!hasEncoderActualOutputFormat) { - @Nullable Format encoderOutputFormat = encoder.getOutputFormat(); - if (encoderOutputFormat == null) { + /** + * Attempts to write sample pipeline output data to the muxer, and returns whether it may be + * possible to write more data immediately by calling this method again. + */ + @RequiresNonNull("samplePipeline") + private boolean feedMuxerFromPipeline() { + if (!muxerWrapperTrackAdded) { + @Nullable Format samplePipelineOutputFormat = samplePipeline.getOutputFormat(); + if (samplePipelineOutputFormat == null) { return false; } - hasEncoderActualOutputFormat = true; - muxerWrapper.addTrackFormat(encoderOutputFormat); + muxerWrapperTrackAdded = true; + muxerWrapper.addTrackFormat(samplePipelineOutputFormat); } - if (encoder.isEnded()) { + if (samplePipeline.isEnded()) { muxerWrapper.endTrack(getTrackType()); muxerWrapperTrackEnded = true; return false; } - @Nullable ByteBuffer encoderOutputBuffer = encoder.getOutputBuffer(); - if (encoderOutputBuffer == null) { + @Nullable DecoderInputBuffer samplePipelineOutputBuffer = samplePipeline.getOutputBuffer(); + if (samplePipelineOutputBuffer == null) { return false; } - MediaCodec.BufferInfo encoderOutputBufferInfo = checkNotNull(encoder.getOutputBufferInfo()); if (!muxerWrapper.writeSample( getTrackType(), - encoderOutputBuffer, - /* isKeyFrame= */ (encoderOutputBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) > 0, - encoderOutputBufferInfo.presentationTimeUs)) { + samplePipelineOutputBuffer.data, + samplePipelineOutputBuffer.isKeyFrame(), + samplePipelineOutputBuffer.timeUs)) { return false; } - encoder.releaseOutputBuffer(); + samplePipeline.releaseOutputBuffer(); return true; } + + /** + * Attempts to pass input data to the sample pipeline, and returns whether it may be possible to + * pass more data immediately by calling this method again. + */ + @RequiresNonNull("samplePipeline") + private boolean feedPipelineFromInput() { + @Nullable DecoderInputBuffer samplePipelineInputBuffer = samplePipeline.dequeueInputBuffer(); + if (samplePipelineInputBuffer == null) { + return false; + } + + @ReadDataResult + int result = readSource(getFormatHolder(), samplePipelineInputBuffer, /* readFlags= */ 0); + switch (result) { + case C.RESULT_BUFFER_READ: + mediaClock.updateTimeForTrackType(getTrackType(), samplePipelineInputBuffer.timeUs); + samplePipelineInputBuffer.timeUs -= streamOffsetUs; + samplePipelineInputBuffer.flip(); + samplePipeline.queueInputBuffer(); + return !samplePipelineInputBuffer.isEndOfStream(); + case C.RESULT_FORMAT_READ: + throw new IllegalStateException("Format changes are not supported."); + case C.RESULT_NOTHING_READ: + default: + return false; + } + } } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java new file mode 100644 index 0000000000..004331404d --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/VideoSamplePipeline.java @@ -0,0 +1,335 @@ +/* + * Copyright 2021 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.transformer; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.MediaCodec; +import android.opengl.EGL14; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.opengl.GLES20; +import android.view.Surface; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * Pipeline to decode video samples, apply transformations on the raw samples, and re-encode them. + */ +@RequiresApi(18) +/* package */ final class VideoSamplePipeline implements SamplePipeline { + + static { + GlUtil.glAssertionsEnabled = true; + } + + private static final String TAG = "VideoSamplePipeline"; + + // Predefined shader values. + private static final String VERTEX_SHADER_FILE_PATH = "shaders/blit_vertex_shader.glsl"; + private static final String FRAGMENT_SHADER_FILE_PATH = + "shaders/copy_external_fragment_shader.glsl"; + private static final int EXPECTED_NUMBER_OF_ATTRIBUTES = 2; + private static final int EXPECTED_NUMBER_OF_UNIFORMS = 2; + + private final Context context; + private final int rendererIndex; + + private final MediaCodecAdapterWrapper encoder; + private final DecoderInputBuffer encoderOutputBuffer; + + private final DecoderInputBuffer decoderInputBuffer; + private final float[] decoderTextureTransformMatrix; + private final Format decoderInputFormat; + + private @MonotonicNonNull EGLDisplay eglDisplay; + private @MonotonicNonNull EGLContext eglContext; + private @MonotonicNonNull EGLSurface eglSurface; + + private int decoderTextureId; + private @MonotonicNonNull SurfaceTexture decoderSurfaceTexture; + private @MonotonicNonNull Surface decoderSurface; + private @MonotonicNonNull MediaCodecAdapterWrapper decoder; + private volatile boolean isDecoderSurfacePopulated; + private boolean waitingForPopulatedDecoderSurface; + private GlUtil.@MonotonicNonNull Uniform decoderTextureTransformUniform; + + public VideoSamplePipeline( + Context context, Format decoderInputFormat, Transformation transformation, int rendererIndex) + throws ExoPlaybackException { + this.decoderInputFormat = decoderInputFormat; + this.rendererIndex = rendererIndex; + this.context = context; + + decoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + decoderTextureTransformMatrix = new float[16]; + decoderTextureId = GlUtil.TEXTURE_ID_UNSET; + + encoderOutputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + try { + encoder = + MediaCodecAdapterWrapper.createForVideoEncoding( + new Format.Builder() + .setWidth(decoderInputFormat.width) + .setHeight(decoderInputFormat.height) + .setSampleMimeType( + transformation.videoMimeType != null + ? transformation.videoMimeType + : decoderInputFormat.sampleMimeType) + .build(), + ImmutableMap.of()); + } catch (IOException e) { + // TODO (internal b/184262323): Assign an adequate error code. + throw ExoPlaybackException.createForRenderer( + e, + TAG, + rendererIndex, + decoderInputFormat, + /* rendererFormatSupport= */ C.FORMAT_HANDLED, + /* isRecoverable= */ false, + PlaybackException.ERROR_CODE_UNSPECIFIED); + } + } + + @Override + public boolean processData() throws ExoPlaybackException { + ensureOpenGlConfigured(); + return !ensureDecoderConfigured() || feedEncoderFromDecoder(); + } + + @Override + @Nullable + public DecoderInputBuffer dequeueInputBuffer() { + return decoder != null && decoder.maybeDequeueInputBuffer(decoderInputBuffer) + ? decoderInputBuffer + : null; + } + + @Override + public void queueInputBuffer() { + checkStateNotNull(decoder).queueInputBuffer(decoderInputBuffer); + } + + @Override + @Nullable + public Format getOutputFormat() { + return encoder.getOutputFormat(); + } + + @Override + public boolean isEnded() { + return encoder.isEnded(); + } + + @Override + @Nullable + public DecoderInputBuffer getOutputBuffer() { + encoderOutputBuffer.data = encoder.getOutputBuffer(); + if (encoderOutputBuffer.data == null) { + return null; + } + MediaCodec.BufferInfo bufferInfo = checkNotNull(encoder.getOutputBufferInfo()); + encoderOutputBuffer.timeUs = bufferInfo.presentationTimeUs; + encoderOutputBuffer.setFlags(bufferInfo.flags); + return encoderOutputBuffer; + } + + @Override + public void releaseOutputBuffer() { + encoder.releaseOutputBuffer(); + } + + @Override + public void release() { + GlUtil.destroyEglContext(eglDisplay, eglContext); + if (decoderTextureId != GlUtil.TEXTURE_ID_UNSET) { + GlUtil.deleteTexture(decoderTextureId); + } + if (decoderSurfaceTexture != null) { + decoderSurfaceTexture.release(); + } + if (decoderSurface != null) { + decoderSurface.release(); + } + if (decoder != null) { + decoder.release(); + } + encoder.release(); + } + + @EnsuresNonNull({"eglDisplay", "eglContext", "eglSurface", "decoderTextureTransformUniform"}) + private void ensureOpenGlConfigured() { + if (eglDisplay != null + && eglContext != null + && eglSurface != null + && decoderTextureTransformUniform != null) { + return; + } + + eglDisplay = GlUtil.createEglDisplay(); + try { + eglContext = GlUtil.createEglContext(eglDisplay); + } catch (GlUtil.UnsupportedEglVersionException e) { + throw new IllegalStateException("EGL version is unsupported", e); + } + eglSurface = GlUtil.getEglSurface(eglDisplay, checkNotNull(encoder.getInputSurface())); + GlUtil.focusSurface( + eglDisplay, eglContext, eglSurface, decoderInputFormat.width, decoderInputFormat.height); + decoderTextureId = GlUtil.createExternalTexture(); + GlUtil.Program copyProgram; + try { + copyProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + copyProgram.use(); + GlUtil.Attribute[] copyAttributes = copyProgram.getAttributes(); + checkState( + copyAttributes.length == EXPECTED_NUMBER_OF_ATTRIBUTES, + "Expected program to have " + EXPECTED_NUMBER_OF_ATTRIBUTES + " vertex attributes."); + for (GlUtil.Attribute copyAttribute : copyAttributes) { + if (copyAttribute.name.equals("a_position")) { + copyAttribute.setBuffer( + new float[] { + -1.0f, -1.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 0.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 1.0f, + }, + /* size= */ 4); + } else if (copyAttribute.name.equals("a_texcoord")) { + copyAttribute.setBuffer( + new float[] { + 0.0f, 0.0f, 0.0f, 1.0f, + 1.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 1.0f, + }, + /* size= */ 4); + } else { + throw new IllegalStateException("Unexpected attribute name."); + } + copyAttribute.bind(); + } + GlUtil.Uniform[] copyUniforms = copyProgram.getUniforms(); + checkState( + copyUniforms.length == EXPECTED_NUMBER_OF_UNIFORMS, + "Expected program to have " + EXPECTED_NUMBER_OF_UNIFORMS + " uniforms."); + for (GlUtil.Uniform copyUniform : copyUniforms) { + if (copyUniform.name.equals("tex_sampler")) { + copyUniform.setSamplerTexId(decoderTextureId, 0); + copyUniform.bind(); + } else if (copyUniform.name.equals("tex_transform")) { + decoderTextureTransformUniform = copyUniform; + } else { + throw new IllegalStateException("Unexpected uniform name."); + } + } + checkNotNull(decoderTextureTransformUniform); + } + + @EnsuresNonNullIf( + expression = {"decoder", "decoderSurfaceTexture"}, + result = true) + private boolean ensureDecoderConfigured() throws ExoPlaybackException { + if (decoder != null && decoderSurfaceTexture != null) { + return true; + } + + checkState(decoderTextureId != GlUtil.TEXTURE_ID_UNSET); + decoderSurfaceTexture = new SurfaceTexture(decoderTextureId); + decoderSurfaceTexture.setOnFrameAvailableListener( + surfaceTexture -> isDecoderSurfacePopulated = true); + decoderSurface = new Surface(decoderSurfaceTexture); + try { + decoder = MediaCodecAdapterWrapper.createForVideoDecoding(decoderInputFormat, decoderSurface); + } catch (IOException e) { + throw createRendererException(e, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); + } + return true; + } + + @RequiresNonNull({ + "decoder", + "decoderSurfaceTexture", + "decoderTextureTransformUniform", + "eglDisplay", + "eglSurface" + }) + private boolean feedEncoderFromDecoder() { + if (decoder.isEnded()) { + return false; + } + + if (!isDecoderSurfacePopulated) { + if (!waitingForPopulatedDecoderSurface) { + if (decoder.getOutputBufferInfo() != null) { + decoder.releaseOutputBuffer(/* render= */ true); + waitingForPopulatedDecoderSurface = true; + } + if (decoder.isEnded()) { + encoder.signalEndOfInputStream(); + } + } + return false; + } + + waitingForPopulatedDecoderSurface = false; + decoderSurfaceTexture.updateTexImage(); + decoderSurfaceTexture.getTransformMatrix(decoderTextureTransformMatrix); + decoderTextureTransformUniform.setFloats(decoderTextureTransformMatrix); + decoderTextureTransformUniform.bind(); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + long decoderSurfaceTextureTimestampNs = decoderSurfaceTexture.getTimestamp(); + EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, decoderSurfaceTextureTimestampNs); + EGL14.eglSwapBuffers(eglDisplay, eglSurface); + isDecoderSurfacePopulated = false; + return true; + } + + private ExoPlaybackException createRendererException(Throwable cause, int errorCode) { + return ExoPlaybackException.createForRenderer( + cause, + TAG, + rendererIndex, + decoderInputFormat, + /* rendererFormatSupport= */ C.FORMAT_HANDLED, + /* isRecoverable= */ false, + errorCode); + } +} From ee006ff2b55dfdc8e2d01a7b15d8b9b6155f453a Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 10 Nov 2021 09:52:02 +0000 Subject: [PATCH 31/41] Add missing deprecation for old track selection override getters. The setters in the Builder are already deprecated and using the old getter is error-prone as they only return the overrides set with the deprecated setters. Issue: google/ExoPlayer#9665 PiperOrigin-RevId: 408817640 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index ea95d3afc7..5c9cdfa694 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -1009,7 +1009,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray}. * @return Whether there is an override. + * @deprecated Only works to retrieve the overrides set with the deprecated {@link + * ParametersBuilder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use + * {@link TrackSelectionParameters#trackSelectionOverrides} instead. */ + @Deprecated public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { Map overrides = selectionOverrides.get(rendererIndex); @@ -1022,7 +1026,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray}. * @return The override, or null if no override exists. + * @deprecated Only works to retrieve the overrides set with the deprecated {@link + * ParametersBuilder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use + * {@link TrackSelectionParameters#trackSelectionOverrides} instead. */ + @Deprecated @Nullable public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { Map overrides = From 0e36bdb2e64b68859bfdaa6729f82a73c7c8ae0a Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 10 Nov 2021 10:35:45 +0000 Subject: [PATCH 32/41] Fix TrackSelectionOverrides javadoc #minor-release PiperOrigin-RevId: 408825328 --- .../trackselection/TrackSelectionOverrides.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java index 27f644071a..88c3a7483f 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java @@ -115,14 +115,14 @@ public final class TrackSelectionOverrides implements Bundleable { /** * Forces the selection of {@link #trackIndices} for a {@link TrackGroup}. * - *

If multiple {link #tracks} are overridden, as many as possible will be selected depending on - * the player capabilities. + *

If multiple tracks in {@link #trackGroup} are overridden, as many as possible will be + * selected depending on the player capabilities. * - *

If a {@link TrackSelectionOverride} has no tracks ({@code tracks.isEmpty()}), no tracks will - * be played. This is similar to {@link TrackSelectionParameters#disabledTrackTypes}, except it - * will only affect the playback of the associated {@link TrackGroup}. For example, if the only - * {@link C#TRACK_TYPE_VIDEO} {@link TrackGroup} is associated with no tracks, no video will play - * until the next video starts. + *

If {@link #trackIndices} is empty, no tracks from {@link #trackGroup} will be played. This + * is similar to {@link TrackSelectionParameters#disabledTrackTypes}, except it will only affect + * the playback of the associated {@link TrackGroup}. For example, if the only {@link + * C#TRACK_TYPE_VIDEO} {@link TrackGroup} is associated with no tracks, no video will play until + * the next video starts. */ public static final class TrackSelectionOverride implements Bundleable { @@ -232,7 +232,7 @@ public final class TrackSelectionOverrides implements Bundleable { return new Builder(overrides); } - /** Returns all {@link TrackSelectionOverride} contained. */ + /** Returns a list of the {@link TrackSelectionOverride overrides}. */ public ImmutableList asList() { return ImmutableList.copyOf(overrides.values()); } From e4695abc180b983e9c0d69082b96b5e066039407 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 10 Nov 2021 12:00:57 +0000 Subject: [PATCH 33/41] Make DefaultHttpDataSourceContractTest an instrumentation test Robolectric uses the JRE HttpURLConnection [1], while real Android devices and emulators use OkHttp to implement HttpURLConnection. This can lead to important differences in behaviour, so it's better to use instrumentation tests when specific HTTP behaviour is important. [1] https://github.com/robolectric/robolectric/issues/6769#issuecomment-943556156 PiperOrigin-RevId: 408840295 --- library/datasource/src/androidTest/AndroidManifest.xml | 2 ++ .../exoplayer2/upstream/DefaultHttpDataSourceContractTest.java | 0 2 files changed, 2 insertions(+) rename library/datasource/src/{test => androidTest}/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java (100%) diff --git a/library/datasource/src/androidTest/AndroidManifest.xml b/library/datasource/src/androidTest/AndroidManifest.xml index 5edae1db96..82fda9bf67 100644 --- a/library/datasource/src/androidTest/AndroidManifest.xml +++ b/library/datasource/src/androidTest/AndroidManifest.xml @@ -18,10 +18,12 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.upstream.test"> + Date: Wed, 10 Nov 2021 12:01:41 +0000 Subject: [PATCH 34/41] Add contract tests for DataSource#getResponseHeaders PiperOrigin-RevId: 408840409 --- .../DefaultHttpDataSourceContractTest.java | 5 ++ .../testutil/DataSourceContractTest.java | 81 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/library/datasource/src/androidTest/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java b/library/datasource/src/androidTest/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java index 0fb570f8b9..0976234a63 100644 --- a/library/datasource/src/androidTest/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java +++ b/library/datasource/src/androidTest/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.DataSourceContractTest; import com.google.android.exoplayer2.testutil.HttpDataSourceTestEnv; import com.google.common.collect.ImmutableList; +import org.junit.Ignore; import org.junit.Rule; import org.junit.runner.RunWith; @@ -43,4 +44,8 @@ public class DefaultHttpDataSourceContractTest extends DataSourceContractTest { protected Uri getNotFoundUri() { return Uri.parse(httpDataSourceTestEnv.getNonexistentUrl()); } + + @Override + @Ignore("internal b/205811776") + public void getResponseHeaders_noNullKeysOrValues() {} } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java index b119512840..07faf15a6d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java @@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -39,10 +40,12 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Map; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.Ignore; import org.junit.Rule; @@ -503,6 +506,62 @@ public abstract class DataSourceContractTest { assertThat(dataSource.getUri()).isNull(); } + @Test + public void getResponseHeaders_noNullKeysOrValues() throws Exception { + ImmutableList resources = getTestResources(); + Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + + for (int i = 0; i < resources.size(); i++) { + additionalFailureInfo.setInfo(getFailureLabel(resources, i)); + TestResource resource = resources.get(i); + DataSource dataSource = createDataSource(); + try { + dataSource.open(new DataSpec(resource.getUri())); + + Map> responseHeaders = dataSource.getResponseHeaders(); + assertThat(responseHeaders).doesNotContainKey(null); + assertThat(responseHeaders.values()).doesNotContain(null); + for (List value : responseHeaders.values()) { + assertThat(value).doesNotContain(null); + } + } finally { + dataSource.close(); + } + additionalFailureInfo.setInfo(null); + } + } + + @Test + public void getResponseHeaders_caseInsensitive() throws Exception { + ImmutableList resources = getTestResources(); + Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + + for (int i = 0; i < resources.size(); i++) { + additionalFailureInfo.setInfo(getFailureLabel(resources, i)); + TestResource resource = resources.get(i); + DataSource dataSource = createDataSource(); + try { + dataSource.open(new DataSpec(resource.getUri())); + + Map> responseHeaders = dataSource.getResponseHeaders(); + for (String key : responseHeaders.keySet()) { + // TODO(internal b/205811776): Remove this when DefaultHttpDataSource is fixed to not + // return a null key. + if (key == null) { + continue; + } + String caseFlippedKey = invertAsciiCaseOfEveryOtherCharacter(key); + assertWithMessage("key='%s', caseFlippedKey='%s'", key, caseFlippedKey) + .that(responseHeaders.get(caseFlippedKey)) + .isEqualTo(responseHeaders.get(key)); + } + } finally { + dataSource.close(); + } + additionalFailureInfo.setInfo(null); + } + } + @Test public void getResponseHeaders_isEmptyWhileNotOpen() throws Exception { ImmutableList resources = getTestResources(); @@ -548,6 +607,28 @@ public abstract class DataSourceContractTest { } } + private static String invertAsciiCaseOfEveryOtherCharacter(String input) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + result.append(i % 2 == 0 ? invertAsciiCase(input.charAt(i)) : input.charAt(i)); + } + return result.toString(); + } + + /** + * Returns {@code c} in the opposite case if it's an ASCII character, otherwise returns {@code c} + * unchanged. + */ + private static char invertAsciiCase(char c) { + if (Ascii.isUpperCase(c)) { + return Ascii.toLowerCase(c); + } else if (Ascii.isLowerCase(c)) { + return Ascii.toUpperCase(c); + } else { + return c; + } + } + /** Information about a resource that can be used to test the {@link DataSource} instance. */ public static final class TestResource { From bf1cf13cab82d98f9683a771bb6b62019eaf069b Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 10 Nov 2021 12:02:23 +0000 Subject: [PATCH 35/41] Fix header name in WebServerDispatcher used for testing HTTP header names are case-insensitive, but all the others in this file are 'correctly' cased, so we might as well be consistent. PiperOrigin-RevId: 408840566 --- .../google/android/exoplayer2/testutil/WebServerDispatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java index ee21cea874..3b9a6fe0f0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java @@ -261,7 +261,7 @@ public class WebServerDispatcher extends Dispatcher { Resource resource = checkNotNull(resourcesByPath.get(requestPath)); byte[] resourceData = resource.getData(); if (resource.supportsRangeRequests()) { - response.setHeader("Accept-ranges", "bytes"); + response.setHeader("Accept-Ranges", "bytes"); } @Nullable ImmutableMap acceptEncodingHeader = getAcceptEncodingHeader(request); @Nullable String preferredContentCoding; From 658def413668947fba27fd92ffe6b1cd1a3a2ee7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 10 Nov 2021 15:55:34 +0000 Subject: [PATCH 36/41] Plumb PlayerId to MediaSource. MediaSource can be reused with other Player instances after they have been released, so we need to set the PlayerId when preparing the source. Access can mostly be handled by the implementation in BaseMediaSource. PiperOrigin-RevId: 408878824 --- .../exoplayer2/ExoPlayerImplInternal.java | 3 +- .../android/exoplayer2/MediaSourceList.java | 9 +++-- .../android/exoplayer2/MetadataRetriever.java | 4 ++- .../exoplayer2/offline/DownloadHelper.java | 4 ++- .../exoplayer2/source/BaseMediaSource.java | 22 ++++++++++-- .../source/CompositeMediaSource.java | 2 +- .../exoplayer2/source/MediaSource.java | 25 +++++++++++--- .../ads/ServerSideInsertedAdsMediaSource.java | 2 +- .../exoplayer2/MediaPeriodQueueTest.java | 8 +++-- .../exoplayer2/MediaSourceListTest.java | 34 ++++++++++--------- .../source/ConcatenatingMediaSourceTest.java | 3 +- .../source/ads/AdsMediaSourceTest.java | 4 ++- .../ServerSideInsertedAdMediaSourceTest.java | 5 ++- .../source/dash/DashMediaSourceTest.java | 3 +- .../source/hls/HlsMediaSourceTest.java | 7 ++-- .../testutil/MediaSourceTestRunner.java | 4 ++- .../testutil/FakeMediaSourceFactoryTest.java | 4 ++- 17 files changed, 104 insertions(+), 39 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 6c766045ea..ca1ae359f5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -268,7 +268,8 @@ import java.util.concurrent.atomic.AtomicBoolean; Handler eventHandler = new Handler(applicationLooper); queue = new MediaPeriodQueue(analyticsCollector, eventHandler); - mediaSourceList = new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler); + mediaSourceList = + new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId); // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // not normally change to this priority" is incorrect. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java index 6a7d298955..95da840f95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java @@ -21,6 +21,7 @@ import static java.lang.Math.min; import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.source.LoadEventInfo; @@ -70,6 +71,7 @@ import java.util.Set; private static final String TAG = "MediaSourceList"; + private final PlayerId playerId; private final List mediaSourceHolders; private final IdentityHashMap mediaSourceByMediaPeriod; private final Map mediaSourceByUid; @@ -93,11 +95,14 @@ import java.util.Set; * source events. * @param analyticsCollectorHandler The {@link Handler} to call {@link AnalyticsCollector} methods * on. + * @param playerId The {@link PlayerId} of the player using this list. */ public MediaSourceList( MediaSourceListInfoRefreshListener listener, @Nullable AnalyticsCollector analyticsCollector, - Handler analyticsCollectorHandler) { + Handler analyticsCollectorHandler, + PlayerId playerId) { + this.playerId = playerId; mediaSourceListInfoListener = listener; shuffleOrder = new DefaultShuffleOrder(0); mediaSourceByMediaPeriod = new IdentityHashMap<>(); @@ -440,7 +445,7 @@ import java.util.Set; childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener)); mediaSource.addEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener); mediaSource.addDrmEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener); - mediaSource.prepareSource(caller, mediaTransferListener); + mediaSource.prepareSource(caller, mediaTransferListener, playerId); } private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java index 4c48cd3141..a3e8081c14 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java @@ -23,6 +23,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; @@ -140,7 +141,8 @@ public final class MetadataRetriever { case MESSAGE_PREPARE_SOURCE: MediaItem mediaItem = (MediaItem) msg.obj; mediaSource = mediaSourceFactory.createMediaSource(mediaItem); - mediaSource.prepareSource(mediaSourceCaller, /* mediaTransferListener= */ null); + mediaSource.prepareSource( + mediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); return true; case MESSAGE_CHECK_FOR_FAILURE: 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 e9d6c5829a..575b71fa2f 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 @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.ExtractorsFactory; @@ -956,7 +957,8 @@ public final class DownloadHelper { public boolean handleMessage(Message msg) { switch (msg.what) { case MESSAGE_PREPARE_SOURCE: - mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null); + mediaSource.prepareSource( + /* caller= */ this, /* mediaTransferListener= */ null, PlayerId.UNSET); mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); return true; case MESSAGE_CHECK_FOR_FAILURE: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java index 8b4cddd8f7..91b0aff28d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -15,10 +15,13 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; + import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -41,6 +44,7 @@ public abstract class BaseMediaSource implements MediaSource { @Nullable private Looper looper; @Nullable private Timeline timeline; + @Nullable private PlayerId playerId; public BaseMediaSource() { mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1); @@ -51,7 +55,7 @@ public abstract class BaseMediaSource implements MediaSource { /** * Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller, - * TransferListener)}. This method is called at most once until the next call to {@link + * TransferListener, PlayerId)}. This method is called at most once until the next call to {@link * #releaseSourceInternal()}. * * @param mediaTransferListener The transfer listener which should be informed of any media data @@ -160,6 +164,16 @@ public abstract class BaseMediaSource implements MediaSource { return !enabledMediaSourceCallers.isEmpty(); } + /** + * Returns the {@link PlayerId} of the player using this media source. + * + *

Must only be used when the media source is {@link #prepareSourceInternal(TransferListener) + * prepared}. + */ + protected final PlayerId getPlayerId() { + return checkStateNotNull(playerId); + } + @Override public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) { Assertions.checkNotNull(handler); @@ -186,9 +200,12 @@ public abstract class BaseMediaSource implements MediaSource { @Override public final void prepareSource( - MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { + MediaSourceCaller caller, + @Nullable TransferListener mediaTransferListener, + PlayerId playerId) { Looper looper = Looper.myLooper(); Assertions.checkArgument(this.looper == null || this.looper == looper); + this.playerId = playerId; @Nullable Timeline timeline = this.timeline; mediaSourceCallers.add(caller); if (this.looper == null) { @@ -226,6 +243,7 @@ public abstract class BaseMediaSource implements MediaSource { if (mediaSourceCallers.isEmpty()) { looper = null; timeline = null; + playerId = null; enabledMediaSourceCallers.clear(); releaseSourceInternal(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index a19504ed7d..b38a0832d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -117,7 +117,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { childSources.put(id, new MediaSourceAndListener<>(mediaSource, caller, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); mediaSource.addDrmEventListener(Assertions.checkNotNull(eventHandler), eventListener); - mediaSource.prepareSource(caller, mediaTransferListener); + mediaSource.prepareSource(caller, mediaTransferListener, getPlayerId()); if (!isEnabled()) { mediaSource.disable(caller); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index fe039f9d16..fc9f38a530 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; @@ -34,7 +35,7 @@ import java.io.IOException; * provide a new timeline whenever the structure of the media changes. The MediaSource * provides these timelines by calling {@link MediaSourceCaller#onSourceInfoRefreshed} on the * {@link MediaSourceCaller}s passed to {@link #prepareSource(MediaSourceCaller, - * TransferListener)}. + * TransferListener, PlayerId)}. *

  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a * way for the player to load and read the media. @@ -183,6 +184,16 @@ public interface MediaSource { /** Returns the {@link MediaItem} whose media is provided by the source. */ MediaItem getMediaItem(); + /** + * @deprecated Implement {@link #prepareSource(MediaSourceCaller, TransferListener, PlayerId)} + * instead. + */ + @Deprecated + default void prepareSource( + MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { + prepareSource(caller, mediaTransferListener, PlayerId.UNSET); + } + /** * Registers a {@link MediaSourceCaller}. Starts source preparation if needed and enables the * source for the creation of {@link MediaPeriod MediaPerods}. @@ -200,15 +211,20 @@ public interface MediaSource { * transfers. May be null if no listener is available. Note that this listener should be only * informed of transfers related to the media loads and not of auxiliary loads for manifests * and other data. + * @param playerId The {@link PlayerId} of the player using this media source. */ - void prepareSource(MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener); + void prepareSource( + MediaSourceCaller caller, + @Nullable TransferListener mediaTransferListener, + PlayerId playerId); /** * Throws any pending error encountered while loading or refreshing source information. * *

    Should not be called directly from application code. * - *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. + *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener, + * PlayerId)}. */ void maybeThrowSourceInfoRefreshError() throws IOException; @@ -217,7 +233,8 @@ public interface MediaSource { * *

    Should not be called directly from application code. * - *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. + *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener, + * PlayerId)}. * * @param caller The {@link MediaSourceCaller} enabling the source. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdsMediaSource.java index 1a5d94ad1f..3a14510b8b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdsMediaSource.java @@ -168,7 +168,7 @@ public final class ServerSideInsertedAdsMediaSource extends BaseMediaSource } mediaSource.addEventListener(handler, /* eventListener= */ this); mediaSource.addDrmEventListener(handler, /* eventListener= */ this); - mediaSource.prepareSource(/* caller= */ this, mediaTransferListener); + mediaSource.prepareSource(/* caller= */ this, mediaTransferListener, getPlayerId()); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 53bc87e5e6..00e99f8b3a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -23,7 +23,9 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; @@ -78,7 +80,8 @@ public final class MediaPeriodQueueTest { new MediaSourceList( mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), /* analyticsCollector= */ null, - new Handler(Looper.getMainLooper())); + new Handler(Looper.getMainLooper()), + PlayerId.UNSET); rendererCapabilities = new RendererCapabilities[0]; trackSelector = mock(TrackSelector.class); allocator = mock(Allocator.class); @@ -738,7 +741,8 @@ public final class MediaPeriodQueueTest { new MediaSourceList.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ false); mediaSourceList.setMediaSources( ImmutableList.of(mediaSourceHolder), new FakeShuffleOrder(/* length= */ 1)); - mediaSourceHolder.mediaSource.prepareSourceInternal(/* mediaTransferListener */ null); + mediaSourceHolder.mediaSource.prepareSource( + mock(MediaSourceCaller.class), /* mediaTransferListener */ null, PlayerId.UNSET); Timeline playlistTimeline = mediaSourceList.createTimeline(); firstPeriodUid = playlistTimeline.getUidOfPeriod(/* periodIndex= */ 0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java index ea40519a3c..209fb83547 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -54,7 +55,8 @@ public class MediaSourceListTest { new MediaSourceList( mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), /* analyticsCollector= */ null, - Util.createHandlerForCurrentOrMainLooper()); + Util.createHandlerForCurrentOrMainLooper(), + PlayerId.UNSET); } @Test @@ -92,30 +94,30 @@ public class MediaSourceListTest { // Verify prepare is called once on prepare. verify(mockMediaSource1, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); mediaSourceList.prepare(/* mediaTransferListener= */ null); assertThat(mediaSourceList.isPrepared()).isTrue(); // Verify prepare is called once on prepare. verify(mockMediaSource1, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); mediaSourceList.release(); mediaSourceList.prepare(/* mediaTransferListener= */ null); // Verify prepare is called a second time on re-prepare. verify(mockMediaSource1, times(2)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(2)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); } @Test @@ -182,10 +184,10 @@ public class MediaSourceListTest { // Verify sources are prepared. verify(mockMediaSource1, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); // Set media items again. The second holder is re-used. MediaSource mockMediaSource3 = mock(MediaSource.class); @@ -203,7 +205,7 @@ public class MediaSourceListTest { assertThat(mediaSources.get(1).isRemoved).isFalse(); verify(mockMediaSource2, times(2)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); } @Test @@ -222,10 +224,10 @@ public class MediaSourceListTest { // Verify lazy initialization does not call prepare on sources. verify(mockMediaSource1, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); for (int i = 0; i < mediaSources.size(); i++) { assertThat(mediaSources.get(i).firstWindowIndexInChild).isEqualTo(i); @@ -259,10 +261,10 @@ public class MediaSourceListTest { // Verify prepare is called on sources when added. verify(mockMediaSource1, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); verify(mockMediaSource2, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); } @Test @@ -387,7 +389,7 @@ public class MediaSourceListTest { new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); verify(mockMediaSource, times(0)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); mediaSourceList.release(); verify(mockMediaSource, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); assertThat(mediaSourceHolder.isRemoved).isFalse(); @@ -406,7 +408,7 @@ public class MediaSourceListTest { new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); verify(mockMediaSource, times(1)) .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull(), any()); mediaSourceList.release(); verify(mockMediaSource, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); assertThat(mediaSourceHolder.isRemoved).isFalse(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index f34ab81e0f..b78a7c4e0f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; @@ -644,7 +645,7 @@ public final class ConcatenatingMediaSourceTest { () -> { MediaSourceCaller caller = mock(MediaSourceCaller.class); mediaSource.addMediaSources(Arrays.asList(createMediaSources(2))); - mediaSource.prepareSource(caller, /* mediaTransferListener= */ null); + mediaSource.prepareSource(caller, /* mediaTransferListener= */ null, PlayerId.UNSET); mediaSource.moveMediaSource( /* currentIndex= */ 0, /* newIndex= */ 1, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java index 1580a39f17..5e69dfc755 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java @@ -29,6 +29,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; @@ -117,7 +118,8 @@ public final class AdsMediaSourceTest { adMediaSourceFactory, mockAdsLoader, mockAdViewProvider); - adsMediaSource.prepareSource(mockMediaSourceCaller, /* mediaTransferListener= */ null); + adsMediaSource.prepareSource( + mockMediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); shadowOf(Looper.getMainLooper()).idle(); verify(mockAdsLoader) .start( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdMediaSourceTest.java index 6b1f858f96..3c0dba79b6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/ServerSideInsertedAdMediaSourceTest.java @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.robolectric.PlaybackOutput; import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; @@ -104,7 +105,9 @@ public final class ServerSideInsertedAdMediaSourceTest { mediaSource.setAdPlaybackState(adPlaybackState); mediaSource.prepareSource( - (source, timeline) -> timelineReference.set(timeline), /* mediaTransferListener= */ null); + (source, timeline) -> timelineReference.set(timeline), + /* mediaTransferListener= */ null, + PlayerId.UNSET); runMainLooperUntil(() -> timelineReference.get() != null); Timeline timeline = timelineReference.get(); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java index 004b7e22c7..de6045f8e6 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; @@ -484,7 +485,7 @@ public final class DashMediaSourceTest { countDownLatch.countDown(); } }; - mediaSource.prepareSource(caller, /* mediaTransferListener= */ null); + mediaSource.prepareSource(caller, /* mediaTransferListener= */ null, PlayerId.UNSET); while (!countDownLatch.await(/* timeout= */ 10, MILLISECONDS)) { ShadowLooper.idleMainLooper(); } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java index 556bb83bb9..6e1e298184 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java @@ -25,6 +25,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; @@ -752,7 +753,7 @@ public class HlsMediaSourceTest { List timelines = new ArrayList<>(); MediaSource.MediaSourceCaller mediaSourceCaller = (source, timeline) -> timelines.add(timeline); - mediaSource.prepareSource(mediaSourceCaller, null); + mediaSource.prepareSource(mediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); runMainLooperUntil(() -> timelines.size() == 1); mediaSource.onPrimaryPlaylistRefreshed(secondPlaylist); runMainLooperUntil(() -> timelines.size() == 2); @@ -785,7 +786,9 @@ public class HlsMediaSourceTest { throws TimeoutException { AtomicReference receivedTimeline = new AtomicReference<>(); mediaSource.prepareSource( - (source, timeline) -> receivedTimeline.set(timeline), /* mediaTransferListener= */ null); + (source, timeline) -> receivedTimeline.set(timeline), + /* mediaTransferListener= */ null, + PlayerId.UNSET); runMainLooperUntil(() -> receivedTimeline.get() != null); return receivedTimeline.get(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 3bb4e0562d..747957feb1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -26,6 +26,7 @@ import android.os.Looper; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaPeriod; @@ -115,7 +116,8 @@ public class MediaSourceTestRunner { final IOException[] prepareError = new IOException[1]; runOnPlaybackThread( () -> { - mediaSource.prepareSource(mediaSourceListener, /* mediaTransferListener= */ null); + mediaSource.prepareSource( + mediaSourceListener, /* mediaTransferListener= */ null, PlayerId.UNSET); try { // TODO: This only catches errors that are set synchronously in prepareSource. To // capture async errors we'll need to poll maybeThrowSourceInfoRefreshError until the diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactoryTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactoryTest.java index c9f9a5a7b6..ac2c41ccf7 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactoryTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeMediaSourceFactoryTest.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -42,7 +43,8 @@ public class FakeMediaSourceFactoryTest { int firstWindowIndex = timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false); reportedMediaItem.set(timeline.getWindow(firstWindowIndex, new Window()).mediaItem); }, - /* mediaTransferListener= */ null); + /* mediaTransferListener= */ null, + PlayerId.UNSET); assertThat(reportedMediaItem.get()).isSameInstanceAs(mediaItem); assertThat(mediaSource.getMediaItem()).isSameInstanceAs(mediaItem); From 13806507b05724640d25188686e33a67d0deafd6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 11 Nov 2021 09:40:35 +0000 Subject: [PATCH 37/41] Update track selection documentation. The Javadoc of DefaultTrackSelector can be shortened as it's not the right place to document detailed options of the Player track selection parameters. The documentation page about track selection is updated to the new APIs and extended with most relevant options and information needed to work with ExoPlayer's track selection API. #minor-release PiperOrigin-RevId: 409088989 --- docs/track-selection.md | 197 ++++++++++++++++-- .../trackselection/DefaultTrackSelector.java | 103 ++------- 2 files changed, 192 insertions(+), 108 deletions(-) diff --git a/docs/track-selection.md b/docs/track-selection.md index 4dd71b5046..abcf2864f8 100644 --- a/docs/track-selection.md +++ b/docs/track-selection.md @@ -3,8 +3,175 @@ title: Track selection --- Track selection determines which of the available media tracks are played by the -player. Track selection is the responsibility of a `TrackSelector`, an instance -of which can be provided whenever an `ExoPlayer` is built. +player. This process is configured by [`TrackSelectionParameters`][], which +support many different options to specify constraints and overrides. + +## Information about existing tracks + +The player needs to prepare the media to know which tracks are available for +selection. You can listen to `Player.Listener.onTracksInfoChanged` to get +notified about changes, which may happen + * When preparation completes + * When the available or selected tracks change + * When the playlist item changes + +~~~ +player.addListener(new Player.Listener() { + @Override + public void onTracksInfoChanged(TracksInfo tracksInfo) { + // Update UI using current TracksInfo. + } +}); +~~~ +{: .language-java} + +You can also retrieve the current `TracksInfo` by calling +`player.getCurrentTracksInfo()`. + +`TracksInfo` contains a list of `TrackGroupInfo`s with information about the +track type, format details, player support and selection status of each +available track. Tracks are grouped together into one `TrackGroup` if they +represent the same content that can be used interchangeably by the player (for +example, all audio tracks of a single language, but with different bitrates). + +~~~ +for (TrackGroupInfo groupInfo : tracksInfo.getTrackGroupInfos()) { + // Group level information. + @C.TrackType int trackType = groupInfo.getTrackType(); + boolean trackInGroupIsSelected = groupInfo.isSelected(); + boolean trackInGroupIsSupported = groupInfo.isSupported(); + TrackGroup group = groupInfo.getTrackGroup(); + for (int i = 0; i < group.length; i++) { + // Individual track information. + boolean isSupported = groupInfo.isTrackSupported(i); + boolean isSelected = groupInfo.isTrackSelected(i); + Format trackFormat = group.getFormat(i); + } +} +~~~ +{: .language-java} + +* A track is 'supported' if the `Player` is able to decode and render its + samples. Note that even if multiple track groups of the same type (for example + multiple audio track groups) are supported, it only means that they are + supported individually and the player is not necessarily able to play them at + the same time. +* A track is 'selected' if the track selector chose this track for playback + using the current `TrackSelectionParameters`. If multiple tracks within one + track group are selected, the player uses these tracks for adaptive playback + (for example, multiple video tracks with different bitrates). Note that only + one of these tracks will be played at any one time. If you want to be notified + of in-playback changes to the adaptive video track you can listen to + `Player.Listener.onVideoSizeChanged`. + +## Modifying track selection parameters + +The selection process can be configured by setting `TrackSelectionParameters` on +the `Player` with `Player.setTrackSelectionParameters`. These updates can be +done before and during playback. In most cases, it's advisable to obtain the +current parameters and only modify the required aspects with the +`TrackSelectionParameters.Builder`. The builder class also allows chaining to +specify multiple options with one command: + +~~~ +player.setTrackSelectionParameters( + player.getTrackSelectionParameters() + .buildUpon() + .setMaxVideoSizeSd() + .setPreferredAudioLanguage("hu") + .build()); +~~~ +{: .language-java} + +### Constraint based track selection + +Most options in `TrackSelectionParameters` allow you to specify constraints, +which are independent of the tracks that are actually available. Typical +constraints are: + + * Maximum or minimum video width, height, frame rate, or bitrate. + * Maximum audio channel count or bitrate. + * Preferred MIME types for video or audio. + * Preferred audio languages or role flags. + * Preferred text languages or role flags. + +Note that ExoPlayer already applies sensible defaults for most of these values, +for example restricting video resolution to the display size or preferring the +audio language that matches the user's system Locale setting. + +There are several benefits to using constraint based track selection instead of +specifying specific tracks directly: + +* You can specify constraints before knowing what tracks the media provides. + This allows to immediately select the appropriate tracks for faster startup + time and also simplifies track selection code as you don't have to listen for + changes in the available tracks. +* Constraints can be applied consistently across all items in a playlist. For + example, selecting an audio language based on user preference will + automatically apply to the next playlist item too, whereas overriding a + specific track will only apply to the current playlist item for which the + track exists. + +### Selecting specific tracks + +It's possible to specify specific tracks in `TrackSelectionParameters` that +should be selected for the current set of tracks. Note that a change in the +available tracks, for example when changing items in a playlist, will also +invalidate such a track override. + +The simplest way to specify track overrides is to specify the `TrackGroup` that +should be selected for its track type. For example, you can specify an audio +track group to select this audio group and prevent any other audio track groups +from being selected: + +~~~ +TrackSelectionOverrides overrides = + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(audioTrackGroup)) + .build(); +player.setTrackSelectionParameters( + player.getTrackSelectionParameters() + .buildUpon().setTrackSelectionOverrides(overrides).build()); +~~~ +{: .language-java} + +### Disabling track types or groups + +Track types, like video, audio or text, can be disabled completely by using +`TrackSelectionParameters.Builder.setDisabledTrackTypes`. This will apply +unconditionally and will also affect other playlist items. + +~~~ +player.setTrackSelectionParameters( + player.getTrackSelectionParameters() + .buildUpon() + .setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_VIDEO)) + .build()); +~~~ +{: .language-java} + +Alternatively, it's possible to prevent the selection of track groups for the +current playlist item only by specifying empty overrides for these groups: + +~~~ +TrackSelectionOverrides overrides = + new TrackSelectionOverrides.Builder() + .addOverride( + new TrackSelectionOverride( + disabledTrackGroup, + /* select no tracks for this group */ ImmutableList.of())) + .build(); +player.setTrackSelectionParameters( + player.getTrackSelectionParameters() + .buildUpon().setTrackSelectionOverrides(overrides).build()); +~~~ +{: .language-java} + +## Customizing the track selector + +Track selection is the responsibility of a `TrackSelector`, an instance +of which can be provided whenever an `ExoPlayer` is built and later obtained +with `ExoPlayer.getTrackSelector()`. ~~~ DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); @@ -16,28 +183,22 @@ ExoPlayer player = {: .language-java} `DefaultTrackSelector` is a flexible `TrackSelector` suitable for most use -cases. When using a `DefaultTrackSelector`, it's possible to control which -tracks it selects by modifying its `Parameters`. This can be done before or -during playback. For example the following code tells the selector to restrict -video track selections to SD, and to select a German audio track if there is -one: +cases. It uses the `TrackSelectionParameters` set in the `Player`, but also +provides some advanced customization options that can be specified in the +`DefaultTrackSelector.ParametersBuilder`: ~~~ trackSelector.setParameters( trackSelector .buildUponParameters() - .setMaxVideoSizeSd() - .setPreferredAudioLanguage("deu")); + .setAllowVideoMixedMimeTypeAdaptiveness(true)); ~~~ {: .language-java} -This is an example of constraint based track selection, in which constraints are -specified without knowledge of the tracks that are actually available. Many -different types of constraint can be specified using `Parameters`. `Parameters` -can also be used to select specific tracks from those that are available. See -the [`DefaultTrackSelector`][], [`Parameters`][] and [`ParametersBuilder`][] -documentation for more details. +### Tunneling -[`Parameters`]: {{ site.exo_sdk }}/trackselection/DefaultTrackSelector.Parameters.html -[`ParametersBuilder`]: {{ site.exo_sdk }}/trackselection/DefaultTrackSelector.ParametersBuilder.html -[`DefaultTrackSelector`]: {{ site.exo_sdk }}/trackselection/DefaultTrackSelector.html +Tunneled playback can be enabled in cases where the combination of renderers and +selected tracks supports it. This can be done by using +`DefaultTrackSelector.ParametersBuilder.setTunnelingEnabled(true)`. + +[`TrackSelectionParameters`]: {{ site.exo_sdk }}/trackselection/TrackSelectionParameters.html diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 5c9cdfa694..abce75b7d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.FormatSupport; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; @@ -61,112 +60,36 @@ import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.compatqual.NullableType; /** - * A default {@link TrackSelector} suitable for most use cases. Track selections are made according - * to configurable {@link Parameters}, which can be set by calling {@link - * Player#setTrackSelectionParameters}. + * A default {@link TrackSelector} suitable for most use cases. * *

    Modifying parameters

    * - * To modify only some aspects of the parameters currently used by a selector, it's possible to - * obtain a {@link ParametersBuilder} initialized with the current {@link Parameters}. The desired - * modifications can be made on the builder, and the resulting {@link Parameters} can then be built - * and set on the selector. For example the following code modifies the parameters to restrict video - * track selections to SD, and to select a German audio track if there is one: - * - *
    {@code
    - * // Build on the current parameters.
    - * TrackSelectionParameters currentParameters = player.getTrackSelectionParameters();
    - * // Build the resulting parameters.
    - * TrackSelectionParameters newParameters = currentParameters
    - *     .buildUpon()
    - *     .setMaxVideoSizeSd()
    - *     .setPreferredAudioLanguage("deu")
    - *     .build();
    - * // Set the new parameters.
    - * player.setTrackSelectionParameters(newParameters);
    - * }
    - * - * Convenience methods and chaining allow this to be written more concisely as: + * Track selection parameters should be modified by obtaining a {@link + * TrackSelectionParameters.Builder} initialized with the current {@link TrackSelectionParameters} + * from the player. The desired modifications can be made on the builder, and the resulting {@link + * TrackSelectionParameters} can then be built and set on the player: * *
    {@code
      * player.setTrackSelectionParameters(
      *     player.getTrackSelectionParameters()
      *         .buildUpon()
      *         .setMaxVideoSizeSd()
    - *         .setPreferredAudioLanguage("deu")
    + *         .setPreferredAudioLanguage("de")
      *         .build());
    + *
      * }
    * - * Selection {@link Parameters} support many different options, some of which are described below. - * - *

    Selecting specific tracks

    - * - * Track selection overrides can be used to select specific tracks. To specify an override for a - * renderer, it's first necessary to obtain the tracks that have been mapped to it: + * Some specialized parameters are only available in the extended {@link Parameters} class, which + * can be retrieved and modified in a similar way in this track selector: * *
    {@code
    - * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
    - * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
    - *     : mappedTrackInfo.getTrackGroups(rendererIndex);
    - * }
    - * - * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so - * setting an override isn't possible. Note that a {@link Player.Listener} registered on the player - * can be used to determine when the current tracks (and therefore the mapping) changes. If {@code - * rendererTrackGroups} is non-null then an override can be set. The next step is to query the - * properties of the available tracks to determine the {@code groupIndex} and the {@code - * trackIndices} within the group it that should be selected. The override can then be specified - * using {@link ParametersBuilder#setSelectionOverride}: - * - *
    {@code
    - * SelectionOverride selectionOverride = new SelectionOverride(groupIndex, trackIndices);
    - * player.setTrackSelectionParameters(
    - *     ((Parameters)player.getTrackSelectionParameters())
    + * defaultTrackSelector.setParameters(
    + *     defaultTrackSelector.getParameters()
      *         .buildUpon()
    - *         .setSelectionOverride(rendererIndex, rendererTrackGroups, selectionOverride)
    + *         .setTunnelingEnabled(true)
      *         .build());
    + *
      * }
    - * - *

    Constraint based track selection

    - * - * Whilst track selection overrides make it possible to select specific tracks, the recommended way - * of controlling which tracks are selected is by specifying constraints. For example consider the - * case of wanting to restrict video track selections to SD, and preferring German audio tracks. - * Track selection overrides could be used to select specific tracks meeting these criteria, however - * a simpler and more flexible approach is to specify these constraints directly: - * - *
    {@code
    - * player.setTrackSelectionParameters(
    - *     player.getTrackSelectionParameters()
    - *         .buildUpon()
    - *         .setMaxVideoSizeSd()
    - *         .setPreferredAudioLanguage("deu")
    - *         .build());
    - * }
    - * - * There are several benefits to using constraint based track selection instead of specific track - * overrides: - * - *
      - *
    • You can specify constraints before knowing what tracks the media provides. This can - * simplify track selection code (e.g. you don't have to listen for changes in the available - * tracks before configuring the selector). - *
    • Constraints can be applied consistently across all periods in a complex piece of media, - * even if those periods contain different tracks. In contrast, a specific track override is - * only applied to periods whose tracks match those for which the override was set. - *
    - * - *

    Disabling renderers

    - * - * Renderers can be disabled using {@link ParametersBuilder#setRendererDisabled}. Disabling a - * renderer differs from setting a {@code null} override because the renderer is disabled - * unconditionally, whereas a {@code null} override is applied only when the track groups available - * to the renderer match the {@link TrackGroupArray} for which it was specified. - * - *

    Tunneling

    - * - * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks - * supports it. This can be done by using {@link ParametersBuilder#setTunnelingEnabled(boolean)}. */ public class DefaultTrackSelector extends MappingTrackSelector { From 86f109c42f7425b7b309a269ae375875a8027fe0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 11 Nov 2021 09:44:32 +0000 Subject: [PATCH 38/41] Add repeat/shuffle mode documentation to the playlist page. We only had some documentation for a custom shuffle mode, but none for generic repeat or shuffle modes. #minor-release Issue: google/ExoPlayer#9611 PiperOrigin-RevId: 409089623 --- docs/playlists.md | 77 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/docs/playlists.md b/docs/playlists.md index e17e6ef452..d897e14e26 100644 --- a/docs/playlists.md +++ b/docs/playlists.md @@ -27,7 +27,7 @@ H264 and VP9 videos). They may even be of different types (e.g., it’s fine for playlist to contain both videos and audio only streams). It's allowed to use the same `MediaItem` multiple times within a playlist. -## Modifying the playlist ## +## Modifying the playlist It's possible to dynamically modify a playlist by adding, moving and removing media items. This can be done both before and during playback by calling the @@ -63,13 +63,60 @@ currently playing `MediaItem` is removed, the player will automatically move to playing the first remaining successor, or transition to the ended state if no such successor exists. -## Querying the playlist ## +## Querying the playlist The playlist can be queried using `Player.getMediaItemCount` and `Player.getMediaItemAt`. The currently playing media item can be queried -by calling `Player.getCurrentMediaItem`. +by calling `Player.getCurrentMediaItem`. There are also other convenience +methods like `Player.hasNextMediaItem` or `Player.getNextMediaItemIndex` to +simplify navigation in the playlist. -## Identifying playlist items ## +## Repeat modes + +The player supports 3 repeat modes that can be set at any time with +`Player.setRepeatMode`: + +* `Player.REPEAT_MODE_OFF`: The playlist isn't repeated and the player will + transition to `Player.STATE_ENDED` once the last item in the playlist has + been played. +* `Player.REPEAT_MODE_ONE`: The current item is repeated in an endless loop. + Methods like `Player.seekToNextMediaItem` will ignore this and seek to the + next item in the list, which will then be repeated in an endless loop. +* `Player.REPEAT_MODE_ALL`: The entire playlist is repeated in an endless loop. + +## Shuffle mode + +Shuffle mode can be enabled or disabled at any time with +`Player.setShuffleModeEnabled`. When in shuffle mode, the player will play the +playlist in a precomputed, randomized order. All items will be played once and +the shuffle mode can also be combined with `Player.REPEAT_MODE_ALL` to repeat +the same randomized order in an endless loop. When shuffle mode is turned off, +playback continues from the current item at its original position in the +playlist. + +Note that the indices as returned by methods like +`Player.getCurrentMediaItemIndex` always refer to the original, unshuffled +order. Similarly, `Player.seekToNextMediaItem` will not play the item at +`player.getCurrentMediaItemIndex() + 1`, but the next item according to the +shuffle order. Inserting new items in the playlist or removing items will keep +the existing shuffled order unchanged as far as possible. + +### Setting a custom shuffle order + +By default the player supports shuffling by using the `DefaultShuffleOrder`. +This can be customized by providing a custom shuffle order implementation, or by +setting a custom order in the `DefaultShuffleOrder` constructor: + +~~~ +// Set a custom shuffle order for the 5 items currently in the playlist: +exoPlayer.setShuffleOrder( + new DefaultShuffleOrder(new int[] {3, 1, 0, 4, 2}, randomSeed)); +// Enable shuffle mode. +exoPlayer.setShuffleModeEnabled(/* shuffleModeEnabled= */ true); +~~~ +{: .language-java} + +## Identifying playlist items To identify playlist items, `MediaItem.mediaId` can be set when building the item: @@ -84,7 +131,7 @@ MediaItem mediaItem = If an app does not explicitly define a media ID for a media item, the string representation of the URI is used. -## Associating app data with playlist items ## +## Associating app data with playlist items In addition to an ID, each media item can also be configured with a custom tag, which can be any app provided object. One use of custom tags is to attach @@ -98,7 +145,7 @@ MediaItem mediaItem = {: .language-java} -## Detecting when playback transitions to another media item ## +## Detecting when playback transitions to another media item When playback transitions to another media item, or starts repeating the same media item, `Listener.onMediaItemTransition(MediaItem, @@ -132,7 +179,7 @@ public void onMediaItemTransition( ~~~ {: .language-java} -## Detecting when the playlist changes ## +## Detecting when the playlist changes When a media item is added, removed or moved, `Listener.onTimelineChanged(Timeline, @TimelineChangeReason)` is called @@ -158,19 +205,3 @@ timeline update include: * A manifest becoming available after preparing an adaptive media item. * A manifest being updated periodically during playback of a live stream. - -## Setting a custom shuffle order ## - -By default the playlist supports shuffling by using the `DefaultShuffleOrder`. -This can be customized by providing a custom shuffle order implementation: - -~~~ -// Set the custom shuffle order. -exoPlayer.setShuffleOrder(shuffleOrder); -// Enable shuffle mode. -exoPlayer.setShuffleModeEnabled(/* shuffleModeEnabled= */ true); -~~~ -{: .language-java} - -If the repeat mode of the player is set to `REPEAT_MODE_ALL`, the custom shuffle -order is played in an endless loop. From 954531bd2838379aa5376c3668f2d3bdf7390195 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 11 Nov 2021 11:45:58 +0000 Subject: [PATCH 39/41] Make TrackSelectionOverride.getTrackType public This method is helpful when iterating the list of track overrides to figure out which type the override applies to. Issue: google/ExoPlayer#9665 PiperOrigin-RevId: 409108977 --- .../exoplayer2/trackselection/TrackSelectionOverrides.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java index 88c3a7483f..c45e0c6981 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionOverrides.java @@ -174,7 +174,8 @@ public final class TrackSelectionOverrides implements Bundleable { return trackGroup.hashCode() + 31 * trackIndices.hashCode(); } - private @C.TrackType int getTrackType() { + /** Returns the {@link C.TrackType} of the overriden track group. */ + public @C.TrackType int getTrackType() { return MimeTypes.getTrackType(trackGroup.getFormat(0).sampleMimeType); } From f43d6326bf4336d3216342130ea5caba806cf851 Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 11 Nov 2021 12:10:50 +0000 Subject: [PATCH 40/41] Async buffer queueing: do not throw from flush/shutdown The asynchronous MediaCodec adapter queues input buffers in a background thread. If a codec queueuing operation throws an exception, the buffer enqueuer will store it as a pending exception and re-throw it the next time the adapter will attempt to queue another input buffer. The buffer enqueuer's flush() and shutdown() may throw an exception if the pending error is set. This is subject to a race-condition in which the pending error can be set while the adapter is flushing/shutting down the enqueuer, e.g., if an input buffer is still being queued and the codec throws an exception. As a result, the adapter cannot flush or shutdown gracefully. This change makes the buffer enqueuer to ignore any pending error when flushing/shuttinf down so that the adapter can flush/release gracefully even if a queueing error was detected. PiperOrigin-RevId: 409113054 --- .../AsynchronousMediaCodecBufferEnqueuer.java | 23 +++++++-------- ...nchronousMediaCodecBufferEnqueuerTest.java | 29 +++++++++++++++++++ .../AsynchronousMediaCodecCallbackTest.java | 18 ++++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java index 1605c669e7..3e95c2a500 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.mediacodec; +import static androidx.annotation.VisibleForTesting.NONE; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Util.castNonNull; @@ -147,7 +148,7 @@ class AsynchronousMediaCodecBufferEnqueuer { } } - /** Shut down the instance. Make sure to call this method to release its internal resources. */ + /** Shuts down the instance. Make sure to call this method to release its internal resources. */ public void shutdown() { if (started) { flush(); @@ -173,26 +174,23 @@ class AsynchronousMediaCodecBufferEnqueuer { * blocks until the {@link #handlerThread} is idle. */ private void flushHandlerThread() throws InterruptedException { - Handler handler = castNonNull(this.handler); - handler.removeCallbacksAndMessages(null); + checkNotNull(this.handler).removeCallbacksAndMessages(null); blockUntilHandlerThreadIsIdle(); - // Check if any exceptions happened during the last queueing action. - maybeThrowException(); } private void blockUntilHandlerThreadIsIdle() throws InterruptedException { conditionVariable.close(); - castNonNull(handler).obtainMessage(MSG_OPEN_CV).sendToTarget(); + checkNotNull(handler).obtainMessage(MSG_OPEN_CV).sendToTarget(); conditionVariable.block(); } - // Called from the handler thread - - @VisibleForTesting + @VisibleForTesting(otherwise = NONE) /* package */ void setPendingRuntimeException(RuntimeException exception) { pendingRuntimeException.set(exception); } + // Called from the handler thread + private void doHandleMessage(Message msg) { @Nullable MessageParams params = null; switch (msg.what) { @@ -214,7 +212,8 @@ class AsynchronousMediaCodecBufferEnqueuer { conditionVariable.open(); break; default: - setPendingRuntimeException(new IllegalStateException(String.valueOf(msg.what))); + pendingRuntimeException.compareAndSet( + null, new IllegalStateException(String.valueOf(msg.what))); } if (params != null) { recycleMessageParams(params); @@ -226,7 +225,7 @@ class AsynchronousMediaCodecBufferEnqueuer { try { codec.queueInputBuffer(index, offset, size, presentationTimeUs, flag); } catch (RuntimeException e) { - setPendingRuntimeException(e); + pendingRuntimeException.compareAndSet(null, e); } } @@ -240,7 +239,7 @@ class AsynchronousMediaCodecBufferEnqueuer { codec.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); } } catch (RuntimeException e) { - setPendingRuntimeException(e); + pendingRuntimeException.compareAndSet(null, e); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java index f3a08df819..4575ad1132 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java @@ -190,6 +190,25 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { enqueuer.flush(); } + @Test + public void flush_withPendingError_doesNotResetError() { + enqueuer.start(); + enqueuer.setPendingRuntimeException( + new MediaCodec.CryptoException(/* errorCode= */ 0, /* detailMessage= */ null)); + + enqueuer.flush(); + + assertThrows( + MediaCodec.CryptoException.class, + () -> + enqueuer.queueInputBuffer( + /* index= */ 0, + /* offset= */ 0, + /* size= */ 0, + /* presentationTimeUs= */ 0, + /* flags= */ 0)); + } + @Test public void shutdown_withoutStart_works() { enqueuer.shutdown(); @@ -219,6 +238,16 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { assertThrows(IllegalStateException.class, () -> enqueuer.shutdown()); } + @Test + public void shutdown_withPendingError_doesNotThrow() { + enqueuer.start(); + enqueuer.setPendingRuntimeException( + new MediaCodec.CryptoException(/* errorCode= */ 0, /* detailMessage= */ null)); + + // Shutting down with a pending error set should not throw . + enqueuer.shutdown(); + } + private static CryptoInfo createCryptoInfo() { CryptoInfo info = new CryptoInfo(); int numSubSamples = 5; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java index 8f538de9de..39d01844d2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java @@ -437,6 +437,24 @@ public class AsynchronousMediaCodecCallbackTest { assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outInfo)).isEqualTo(1); } + @Test + public void flush_withPendingError_resetsError() throws Exception { + asynchronousMediaCodecCallback.onError(codec, createCodecException()); + // Calling flush should clear any pending error. + asynchronousMediaCodecCallback.flush(/* codec= */ null); + + assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void shutdown_withPendingError_doesNotThrow() throws Exception { + asynchronousMediaCodecCallback.onError(codec, createCodecException()); + + // Calling shutdown() should not throw. + asynchronousMediaCodecCallback.shutdown(); + } + /** Reflectively create a {@link MediaCodec.CodecException}. */ private static MediaCodec.CodecException createCodecException() throws Exception { Constructor constructor = From cb60425aabe12904531c26bd24a88ed83764a20e Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 11 Nov 2021 13:16:49 +0000 Subject: [PATCH 41/41] Fully support per-track-type selection overrides. Currently, TrackSelectionOverrides are documented as being applied per track type, meaning that one override for a type disables all other selections for the same track type. However, the actual implementation only applies it per track group, relying on the track selector to never select another renderer of the same type. This change fixes DefaultTrackSelector to fully adhere to the TrackSelectionsOverride definition. This solves problems when overriding tracks for extension renderers (see Issue: google/ExoPlayer#9675) and also simplifies a workaround added to StyledPlayerView. #minor-release PiperOrigin-RevId: 409121711 --- .../trackselection/DefaultTrackSelector.java | 152 +++++++++++++----- .../DefaultTrackSelectorTest.java | 96 ++++++++++- .../ui/StyledPlayerControlView.java | 49 +----- 3 files changed, 208 insertions(+), 89 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index abce75b7d7..d02940ff71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -1435,9 +1435,32 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererMixedMimeTypeAdaptationSupports, params); - // Apply track disabling and overriding. + // Apply per track type overrides. + SparseArray> applicableOverridesByTrackType = + getApplicableOverrides(mappedTrackInfo, params); + for (int i = 0; i < applicableOverridesByTrackType.size(); i++) { + Pair overrideAndRendererIndex = + applicableOverridesByTrackType.valueAt(i); + applyTrackTypeOverride( + mappedTrackInfo, + definitions, + /* trackType= */ applicableOverridesByTrackType.keyAt(i), + /* override= */ overrideAndRendererIndex.first, + /* overrideRendererIndex= */ overrideAndRendererIndex.second); + } + + // Apply legacy per renderer overrides. for (int i = 0; i < rendererCount; i++) { - definitions[i] = maybeApplyOverride(mappedTrackInfo, params, i, definitions[i]); + if (hasLegacyRendererOverride(mappedTrackInfo, params, /* rendererIndex= */ i)) { + definitions[i] = getLegacyRendererOverride(mappedTrackInfo, params, /* rendererIndex= */ i); + } + } + + // Disable renderers if needed. + for (int i = 0; i < rendererCount; i++) { + if (isRendererDisabled(mappedTrackInfo, params, /* rendererIndex= */ i)) { + definitions[i] = null; + } } @NullableType @@ -1469,53 +1492,94 @@ public class DefaultTrackSelector extends MappingTrackSelector { return Pair.create(rendererConfigurations, rendererTrackSelections); } - /** - * Returns the {@link ExoTrackSelection.Definition} of a renderer after applying selection - * overriding and renderer disabling. - */ - protected ExoTrackSelection.@NullableType Definition maybeApplyOverride( - MappedTrackInfo mappedTrackInfo, - Parameters params, - int rendererIndex, - ExoTrackSelection.@NullableType Definition currentDefinition) { - // Per renderer and per track type disabling + private boolean isRendererDisabled( + MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { @C.TrackType int rendererType = mappedTrackInfo.getRendererType(rendererIndex); - if (params.getRendererDisabled(rendererIndex) - || params.disabledTrackTypes.contains(rendererType)) { + return params.getRendererDisabled(rendererIndex) + || params.disabledTrackTypes.contains(rendererType); + } + + @SuppressWarnings("deprecation") // Calling deprecated hasSelectionOverride. + private boolean hasLegacyRendererOverride( + MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + return params.hasSelectionOverride(rendererIndex, rendererTrackGroups); + } + + @SuppressWarnings("deprecation") // Calling deprecated getSelectionOverride. + private ExoTrackSelection.@NullableType Definition getLegacyRendererOverride( + MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + @Nullable + SelectionOverride override = params.getSelectionOverride(rendererIndex, rendererTrackGroups); + if (override == null) { return null; } - // Per TrackGroupArray overrides. - TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - if (params.hasSelectionOverride(rendererIndex, rendererTrackGroups)) { - @Nullable - SelectionOverride override = params.getSelectionOverride(rendererIndex, rendererTrackGroups); - if (override == null) { - return null; - } - return new ExoTrackSelection.Definition( - rendererTrackGroups.get(override.groupIndex), override.tracks, override.type); - } - // Per TrackGroup overrides. - for (int j = 0; j < rendererTrackGroups.length; j++) { - TrackGroup trackGroup = rendererTrackGroups.get(j); - @Nullable - TrackSelectionOverride overrideTracks = - params.trackSelectionOverrides.getOverride(trackGroup); - if (overrideTracks != null) { - if (overrideTracks.trackIndices.isEmpty()) { - // TrackGroup is disabled. Deselect the currentDefinition if applicable. Otherwise ignore. - if (currentDefinition != null && currentDefinition.group.equals(trackGroup)) { - currentDefinition = null; - } - } else { - // Override current definition with new selection. - currentDefinition = - new ExoTrackSelection.Definition( - trackGroup, Ints.toArray(overrideTracks.trackIndices)); - } + return new ExoTrackSelection.Definition( + rendererTrackGroups.get(override.groupIndex), override.tracks, override.type); + } + + /** + * Returns applicable overrides. Mapping from track type to a pair of override and renderer index + * for this override. + */ + private SparseArray> getApplicableOverrides( + MappedTrackInfo mappedTrackInfo, Parameters params) { + SparseArray> applicableOverrides = new SparseArray<>(); + // Iterate through all existing track groups to ensure only overrides for those groups are used. + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + for (int j = 0; j < rendererTrackGroups.length; j++) { + maybeUpdateApplicableOverrides( + applicableOverrides, + params.trackSelectionOverrides.getOverride(rendererTrackGroups.get(j)), + rendererIndex); + } + } + // Also iterate unmapped groups to see if they have overrides. + TrackGroupArray unmappedGroups = mappedTrackInfo.getUnmappedTrackGroups(); + for (int i = 0; i < unmappedGroups.length; i++) { + maybeUpdateApplicableOverrides( + applicableOverrides, + params.trackSelectionOverrides.getOverride(unmappedGroups.get(i)), + /* rendererIndex= */ C.INDEX_UNSET); + } + return applicableOverrides; + } + + private void maybeUpdateApplicableOverrides( + SparseArray> applicableOverrides, + @Nullable TrackSelectionOverride override, + int rendererIndex) { + if (override == null) { + return; + } + @C.TrackType int trackType = override.getTrackType(); + @Nullable + Pair existingOverride = applicableOverrides.get(trackType); + if (existingOverride == null || existingOverride.first.trackIndices.isEmpty()) { + // We only need to choose one non-empty override per type. + applicableOverrides.put(trackType, Pair.create(override, rendererIndex)); + } + } + + private void applyTrackTypeOverride( + MappedTrackInfo mappedTrackInfo, + ExoTrackSelection.@NullableType Definition[] definitions, + @C.TrackType int trackType, + TrackSelectionOverride override, + int overrideRendererIndex) { + for (int i = 0; i < definitions.length; i++) { + if (overrideRendererIndex == i) { + definitions[i] = + new ExoTrackSelection.Definition( + override.trackGroup, Ints.toArray(override.trackIndices)); + } else if (mappedTrackInfo.getRendererType(i) == trackType) { + // Disable other renderers of the same type. + definitions[i] = null; } } - return currentDefinition; } // Track selection prior to overrides and disabled flags being applied. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index b70bfd53fa..5209adb8dc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.trackselection; import static com.google.android.exoplayer2.C.FORMAT_EXCEEDS_CAPABILITIES; import static com.google.android.exoplayer2.C.FORMAT_HANDLED; import static com.google.android.exoplayer2.C.FORMAT_UNSUPPORTED_SUBTYPE; +import static com.google.android.exoplayer2.C.FORMAT_UNSUPPORTED_TYPE; import static com.google.android.exoplayer2.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_NOT_SUPPORTED; import static com.google.android.exoplayer2.RendererConfiguration.DEFAULT; @@ -52,6 +53,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.HashMap; import java.util.Map; @@ -238,7 +240,8 @@ public final class DefaultTrackSelectorTest { assertThat(result.selections) .asList() - .containsExactly(new FixedTrackSelection(videoGroupMidBitrate, /* track= */ 0), null); + .containsExactly(new FixedTrackSelection(videoGroupMidBitrate, /* track= */ 0), null) + .inOrder(); } /** Tests that an empty override is not applied for a different set of available track groups. */ @@ -267,6 +270,97 @@ public final class DefaultTrackSelectorTest { .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT}); } + @Test + public void selectTrack_withOverrideForDifferentRenderer_clearsDefaultSelectionOfSameType() + throws Exception { + Format videoFormatH264 = + VIDEO_FORMAT.buildUpon().setId("H264").setSampleMimeType(MimeTypes.VIDEO_H264).build(); + Format videoFormatAv1 = + VIDEO_FORMAT.buildUpon().setId("AV1").setSampleMimeType(MimeTypes.VIDEO_AV1).build(); + TrackGroup videoGroupH264 = new TrackGroup(videoFormatH264); + TrackGroup videoGroupAv1 = new TrackGroup(videoFormatAv1); + Map rendererCapabilitiesMap = + ImmutableMap.of( + videoFormatH264.id, FORMAT_HANDLED, videoFormatAv1.id, FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities rendererCapabilitiesH264 = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_VIDEO, rendererCapabilitiesMap); + rendererCapabilitiesMap = + ImmutableMap.of( + videoFormatH264.id, FORMAT_UNSUPPORTED_TYPE, videoFormatAv1.id, FORMAT_HANDLED); + RendererCapabilities rendererCapabilitiesAv1 = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_VIDEO, rendererCapabilitiesMap); + + // Try to force selection of one TrackGroup in both directions to ensure the default gets + // overridden without having to know what the default is. + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(videoGroupH264)) + .build())); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilitiesH264, rendererCapabilitiesAv1}, + new TrackGroupArray(videoGroupH264, videoGroupAv1), + periodId, + TIMELINE); + + assertThat(result.selections) + .asList() + .containsExactly(new FixedTrackSelection(videoGroupH264, /* track= */ 0), null) + .inOrder(); + + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(videoGroupAv1)) + .build())); + result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilitiesH264, rendererCapabilitiesAv1}, + new TrackGroupArray(videoGroupH264, videoGroupAv1), + periodId, + TIMELINE); + + assertThat(result.selections) + .asList() + .containsExactly(null, new FixedTrackSelection(videoGroupAv1, /* track= */ 0)) + .inOrder(); + } + + @Test + public void selectTracks_withOverrideForUnmappedGroup_disablesAllRenderersOfSameType() + throws Exception { + Format audioSupported = AUDIO_FORMAT.buildUpon().setId("supported").build(); + Format audioUnsupported = AUDIO_FORMAT.buildUpon().setId("unsupported").build(); + TrackGroup audioGroupSupported = new TrackGroup(audioSupported); + TrackGroup audioGroupUnsupported = new TrackGroup(audioUnsupported); + Map audioRendererCapabilitiesMap = + ImmutableMap.of( + audioSupported.id, FORMAT_HANDLED, audioUnsupported.id, FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities audioRendererCapabilties = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, audioRendererCapabilitiesMap); + + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(audioGroupUnsupported)) + .build())); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {VIDEO_CAPABILITIES, audioRendererCapabilties}, + new TrackGroupArray(VIDEO_TRACK_GROUP, audioGroupSupported, audioGroupUnsupported), + periodId, + TIMELINE); + + assertThat(result.selections).asList().containsExactly(VIDEO_TRACK_SELECTION, null).inOrder(); + } + /** Tests that an override is not applied for a different set of available track groups. */ @Test public void selectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index c6e0aadad0..cd33ecc9d9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -82,7 +82,6 @@ import java.util.Formatter; import java.util.List; import java.util.Locale; import java.util.concurrent.CopyOnWriteArrayList; -import org.checkerframework.dataflow.qual.Pure; /** * A view for controlling {@link Player} instances. @@ -2166,12 +2165,11 @@ public class StyledPlayerControlView extends FrameLayout { TrackSelectionParameters trackSelectionParameters = player.getTrackSelectionParameters(); TrackSelectionOverrides overrides = - forceTrackSelection( - trackSelectionParameters.trackSelectionOverrides, - track.tracksInfo, - track.trackGroupIndex, - new TrackSelectionOverride( - track.trackGroup, ImmutableList.of(track.trackIndex))); + new TrackSelectionOverrides.Builder() + .setOverrideForType( + new TrackSelectionOverride( + track.trackGroup, ImmutableList.of(track.trackIndex))) + .build(); checkNotNull(player) .setTrackSelectionParameters( trackSelectionParameters @@ -2209,41 +2207,4 @@ public class StyledPlayerControlView extends FrameLayout { checkView = itemView.findViewById(R.id.exo_check); } } - - /** - * Forces tracks in a {@link TrackGroup} to be the only ones selected for a {@link C.TrackType}. - * No other tracks of that type will be selectable. If the forced tracks are not supported, then - * no tracks of that type will be selected. - * - * @param trackSelectionOverrides The current {@link TrackSelectionOverride overrides}. - * @param tracksInfo The current {@link TracksInfo}. - * @param forcedTrackGroupIndex The index of the {@link TrackGroup} in {@code tracksInfo} that - * should have its track selected. - * @param forcedTrackSelectionOverride The tracks to force selection of. - * @return The updated {@link TrackSelectionOverride overrides}. - */ - @Pure - private static TrackSelectionOverrides forceTrackSelection( - TrackSelectionOverrides trackSelectionOverrides, - TracksInfo tracksInfo, - int forcedTrackGroupIndex, - TrackSelectionOverride forcedTrackSelectionOverride) { - TrackSelectionOverrides.Builder overridesBuilder = trackSelectionOverrides.buildUpon(); - - @C.TrackType - int trackType = tracksInfo.getTrackGroupInfos().get(forcedTrackGroupIndex).getTrackType(); - overridesBuilder.setOverrideForType(forcedTrackSelectionOverride); - // TrackSelectionOverride doesn't currently guarantee that only overwritten track - // group of a given type are selected, so the others have to be explicitly disabled. - // This guarantee is provided in the following patch that removes the need for this method. - ImmutableList trackGroupInfos = tracksInfo.getTrackGroupInfos(); - for (int i = 0; i < trackGroupInfos.size(); i++) { - TrackGroupInfo trackGroupInfo = trackGroupInfos.get(i); - if (i != forcedTrackGroupIndex && trackGroupInfo.getTrackType() == trackType) { - TrackGroup trackGroup = trackGroupInfo.getTrackGroup(); - overridesBuilder.addOverride(new TrackSelectionOverride(trackGroup, ImmutableList.of())); - } - } - return overridesBuilder.build(); - } }