diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.silentaudio.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.silentaudio.dump new file mode 100644 index 0000000000..f4685803b7 --- /dev/null +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.silentaudio.dump @@ -0,0 +1,467 @@ +format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B +format 1: + sampleMimeType = audio/mp4a-latm + channelCount = 2 + sampleRate = 44100 + pcmEncoding = 2 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 23220 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 46440 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 69660 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 92880 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 116100 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 139320 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 162540 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 185760 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 208980 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 232200 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 255420 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 278640 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 301860 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 325080 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 348300 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 371520 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 394740 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 417960 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 441180 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 464400 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 487620 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 510840 +sample: + trackIndex = 0 + dataHashCode = -770308242 + size = 36692 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = -732087136 + size = 5312 + isKeyFrame = false + presentationTimeUs = 66733 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 534059 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 557279 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 580499 +sample: + trackIndex = 0 + dataHashCode = 468156717 + size = 599 + isKeyFrame = false + presentationTimeUs = 33366 +sample: + trackIndex = 0 + dataHashCode = 1150349584 + size = 7735 + isKeyFrame = false + presentationTimeUs = 200200 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 603719 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 626939 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 650159 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 673379 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 696599 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 719819 +sample: + trackIndex = 0 + dataHashCode = 1443582006 + size = 987 + isKeyFrame = false + presentationTimeUs = 133466 +sample: + trackIndex = 0 + dataHashCode = -310585145 + size = 673 + isKeyFrame = false + presentationTimeUs = 100100 +sample: + trackIndex = 0 + dataHashCode = 807460688 + size = 523 + isKeyFrame = false + presentationTimeUs = 166833 +sample: + trackIndex = 0 + dataHashCode = 1936487090 + size = 6061 + isKeyFrame = false + presentationTimeUs = 333666 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 743039 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 766259 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 789479 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 812699 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 835919 +sample: + trackIndex = 0 + dataHashCode = -32297181 + size = 992 + isKeyFrame = false + presentationTimeUs = 266933 +sample: + trackIndex = 0 + dataHashCode = 1529616406 + size = 623 + isKeyFrame = false + presentationTimeUs = 233566 +sample: + trackIndex = 0 + dataHashCode = 1949198785 + size = 421 + isKeyFrame = false + presentationTimeUs = 300300 +sample: + trackIndex = 0 + dataHashCode = -147880287 + size = 4899 + isKeyFrame = false + presentationTimeUs = 433766 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 859139 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 882359 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 905579 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 928799 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 952019 +sample: + trackIndex = 0 + dataHashCode = 1369083472 + size = 568 + isKeyFrame = false + presentationTimeUs = 400400 +sample: + trackIndex = 0 + dataHashCode = 965782073 + size = 620 + isKeyFrame = false + presentationTimeUs = 367033 +sample: + trackIndex = 0 + dataHashCode = -261176150 + size = 5450 + isKeyFrame = false + presentationTimeUs = 567233 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 975239 +sample: + trackIndex = 1 + dataHashCode = 1742602241 + size = 4096 + isKeyFrame = true + presentationTimeUs = 998459 +sample: + trackIndex = 1 + dataHashCode = -1029274849 + size = 409 + isKeyFrame = true + presentationTimeUs = 1021679 +sample: + trackIndex = 0 + dataHashCode = -1830836678 + size = 1051 + isKeyFrame = false + presentationTimeUs = 500500 +sample: + trackIndex = 0 + dataHashCode = 1767407540 + size = 874 + isKeyFrame = false + presentationTimeUs = 467133 +sample: + trackIndex = 0 + dataHashCode = 918440283 + size = 781 + isKeyFrame = false + presentationTimeUs = 533866 +sample: + trackIndex = 0 + dataHashCode = -1408463661 + size = 4725 + isKeyFrame = false + presentationTimeUs = 700700 +sample: + trackIndex = 0 + dataHashCode = 1569455924 + size = 1022 + isKeyFrame = false + presentationTimeUs = 633966 +sample: + trackIndex = 0 + dataHashCode = -1723778407 + size = 790 + isKeyFrame = false + presentationTimeUs = 600600 +sample: + trackIndex = 0 + dataHashCode = 1578275472 + size = 610 + isKeyFrame = false + presentationTimeUs = 667333 +sample: + trackIndex = 0 + dataHashCode = 1989768395 + size = 2751 + isKeyFrame = false + presentationTimeUs = 834166 +sample: + trackIndex = 0 + dataHashCode = -1215674502 + size = 745 + isKeyFrame = false + presentationTimeUs = 767433 +sample: + trackIndex = 0 + dataHashCode = -814473606 + size = 621 + isKeyFrame = false + presentationTimeUs = 734066 +sample: + trackIndex = 0 + dataHashCode = 498370894 + size = 505 + isKeyFrame = false + presentationTimeUs = 800800 +sample: + trackIndex = 0 + dataHashCode = -1051506468 + size = 1268 + isKeyFrame = false + presentationTimeUs = 967633 +sample: + trackIndex = 0 + dataHashCode = -1025604144 + size = 880 + isKeyFrame = false + presentationTimeUs = 900900 +sample: + trackIndex = 0 + dataHashCode = -913586520 + size = 530 + isKeyFrame = false + presentationTimeUs = 867533 +sample: + trackIndex = 0 + dataHashCode = 1340459242 + size = 568 + isKeyFrame = false + presentationTimeUs = 934266 +released = true diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioTranscodingSamplePipeline.java index 25793d6234..030b33e0c5 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioTranscodingSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioTranscodingSamplePipeline.java @@ -41,6 +41,7 @@ import org.checkerframework.dataflow.qual.Pure; private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024; + @Nullable private final SilentAudioGenerator silentAudioGenerator; private final DecoderInputBuffer inputBuffer; private final AudioProcessingPipeline audioProcessingPipeline; private final Codec encoder; @@ -52,12 +53,14 @@ import org.checkerframework.dataflow.qual.Pure; private long nextEncoderInputBufferTimeUs; private long encoderBufferDurationRemainder; + // TODO(b/260618558): Move silent audio generation upstream of this component. public AudioTranscodingSamplePipeline( Format inputFormat, long streamStartPositionUs, long streamOffsetUs, TransformationRequest transformationRequest, ImmutableList audioProcessors, + long forceSilentAudioDurationUs, Codec.EncoderFactory encoderFactory, MuxerWrapper muxerWrapper, Listener listener, @@ -71,6 +74,16 @@ import org.checkerframework.dataflow.qual.Pure; muxerWrapper, listener); + if (forceSilentAudioDurationUs != C.TIME_UNSET) { + silentAudioGenerator = + new SilentAudioGenerator( + forceSilentAudioDurationUs, + inputFormat.sampleRate, + Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, inputFormat.channelCount)); + } else { + silentAudioGenerator = null; + } + inputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); encoderInputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); encoderOutputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); @@ -160,11 +173,17 @@ import org.checkerframework.dataflow.qual.Pure; @Override protected boolean processDataUpToMuxer() throws TransformationException { - if (audioProcessingPipeline.isOperational()) { - return feedEncoderFromProcessingPipeline() || feedProcessingPipelineFromInput(); - } else { - return feedEncoderFromInput(); + if (!audioProcessingPipeline.isOperational()) { + return silentAudioGenerator == null ? feedEncoderFromInput() : feedEncoderFromSilence(); } + + if (feedEncoderFromProcessingPipeline()) { + return true; + } + + return silentAudioGenerator == null + ? feedProcessingPipelineFromInput() + : feedProcessingPipelineFromSilence(); } @Override @@ -268,6 +287,45 @@ import org.checkerframework.dataflow.qual.Pure; return true; } + /** + * Attempts to pass silent audio to the encoder. + * + * @return Whether it may be possible to feed more data immediately by calling this method again. + */ + private boolean feedEncoderFromSilence() throws TransformationException { + checkNotNull(silentAudioGenerator); + if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { + return false; + } + + if (silentAudioGenerator.isEnded()) { + queueEndOfStreamToEncoder(); + return false; + } + + ByteBuffer silence = silentAudioGenerator.getBuffer(); + feedEncoder(silence); + return true; + } + + /** + * Attempts to feed silent audio to the {@link AudioProcessingPipeline}. + * + * @return Whether it may be possible to feed more data immediately by calling this method again. + */ + private boolean feedProcessingPipelineFromSilence() { + checkNotNull(silentAudioGenerator); + if (silentAudioGenerator.isEnded()) { + audioProcessingPipeline.queueEndOfStream(); + return false; + } + checkState(!audioProcessingPipeline.isEnded()); + + ByteBuffer silence = silentAudioGenerator.getBuffer(); + audioProcessingPipeline.queueInput(silence); + return !silence.hasRemaining(); + } + /** * 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. diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SilentAudioGenerator.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SilentAudioGenerator.java new file mode 100644 index 0000000000..436e69f260 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SilentAudioGenerator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 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 androidx.media3.transformer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/* package */ final class SilentAudioGenerator { + private static final int DEFAULT_BUFFER_SIZE = 4096; + + private final ByteBuffer internalBuffer; + + private long remainingBytesToOutput; + + public SilentAudioGenerator(long totalDurationUs, long sampleRate, int frameSize) { + remainingBytesToOutput = (sampleRate * frameSize * totalDurationUs) / 1_000_000L; + internalBuffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE).order(ByteOrder.nativeOrder()); + internalBuffer.flip(); + } + + public ByteBuffer getBuffer() { + if (!internalBuffer.hasRemaining()) { + // "next" buffer. + internalBuffer.clear(); + if (remainingBytesToOutput < internalBuffer.capacity()) { + internalBuffer.limit((int) remainingBytesToOutput); + } + // Only reduce remaining bytes when we "generate" a new one. + remainingBytesToOutput -= internalBuffer.remaining(); + } + return internalBuffer; + } + + public boolean isEnded() { + return !internalBuffer.hasRemaining() && remainingBytesToOutput == 0; + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index bc3fc869cf..cb46072d6b 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -42,6 +42,7 @@ import androidx.media3.common.util.Util; import androidx.media3.effect.GlEffect; import androidx.media3.effect.GlEffectsFrameProcessor; import androidx.media3.effect.GlMatrixTransformation; +import androidx.media3.exoplayer.audio.SonicAudioProcessor; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.extractor.DefaultExtractorsFactory; @@ -88,6 +89,7 @@ public final class Transformer { private ImmutableList videoEffects; private boolean removeAudio; private boolean removeVideo; + private boolean forceSilentAudio; private ListenerSet listeners; private MediaSource.@MonotonicNonNull Factory mediaSourceFactory; private Codec.DecoderFactory decoderFactory; @@ -126,6 +128,7 @@ public final class Transformer { this.videoEffects = transformer.videoEffects; this.removeAudio = transformer.removeAudio; this.removeVideo = transformer.removeVideo; + this.forceSilentAudio = transformer.forceSilentAudio; this.listeners = transformer.listeners; this.mediaSourceFactory = transformer.mediaSourceFactory; this.decoderFactory = transformer.decoderFactory; @@ -416,6 +419,33 @@ public final class Transformer { return this; } + /** + * Sets whether to force silent audio for the output file, ignoring any existing audio. + * + *

This method is experimental and may be removed or changed without warning. + * + *

Audio properties/format: + * + *

    + *
  • Duration will match duration of the input media. + *
  • Sample mime type will match {@link TransformationRequest#audioMimeType}, or {@link + * MimeTypes#AUDIO_AAC} if {@code null}. + *
  • Sample rate will be 44100hz. This can be modified by passing a {@link + * SonicAudioProcessor} to {@link #setAudioProcessors(List)}, using {@link + * SonicAudioProcessor#setOutputSampleRateHz(int)}. + *
  • Channel count will be 2. This can be modified by implementing a custom {@link + * AudioProcessor} and passing it to {@link #setAudioProcessors(List)}. + *
+ * + * @param forceSilentAudio Whether to output silent audio for the output file. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder experimentalSetForceSilentAudio(boolean forceSilentAudio) { + this.forceSilentAudio = forceSilentAudio; + return this; + } + /** * Builds a {@link Transformer} instance. * @@ -445,6 +475,7 @@ public final class Transformer { videoEffects, removeAudio, removeVideo, + forceSilentAudio, listeners, mediaSourceFactory, decoderFactory, @@ -557,6 +588,7 @@ public final class Transformer { private final ImmutableList videoEffects; private final boolean removeAudio; private final boolean removeVideo; + private final boolean forceSilentAudio; private final ListenerSet listeners; private final MediaSource.Factory mediaSourceFactory; private final FrameProcessor.Factory frameProcessorFactory; @@ -574,7 +606,8 @@ public final class Transformer { ImmutableList videoEffects, boolean removeAudio, boolean removeVideo, - ListenerSet listeners, + boolean forceSilentAudio, + ListenerSet listeners, MediaSource.Factory mediaSourceFactory, Codec.DecoderFactory decoderFactory, Codec.EncoderFactory encoderFactory, @@ -583,6 +616,10 @@ public final class Transformer { Looper looper, DebugViewProvider debugViewProvider, Clock clock) { + if (forceSilentAudio) { + removeAudio = true; + } + checkState(!removeVideo || !forceSilentAudio, "Silent only audio track needs a video track."); checkState(!removeAudio || !removeVideo, "Audio and video cannot both be removed."); this.context = context; this.transformationRequest = transformationRequest; @@ -590,6 +627,7 @@ public final class Transformer { this.videoEffects = videoEffects; this.removeAudio = removeAudio; this.removeVideo = removeVideo; + this.forceSilentAudio = forceSilentAudio; this.listeners = listeners; this.mediaSourceFactory = mediaSourceFactory; this.decoderFactory = decoderFactory; @@ -728,6 +766,7 @@ public final class Transformer { videoEffects, removeAudio, removeVideo, + forceSilentAudio, mediaSourceFactory, decoderFactory, encoderFactory, diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java index 72db3d826f..b6906c68fa 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java @@ -97,6 +97,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final TransformationRequest transformationRequest; private final ImmutableList audioProcessors; private final ImmutableList videoEffects; + private final boolean forceSilentAudio; private final Codec.DecoderFactory decoderFactory; private final Codec.EncoderFactory encoderFactory; private final FrameProcessor.Factory frameProcessorFactory; @@ -114,6 +115,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable private DecoderInputBuffer pendingInputBuffer; private boolean isDrainingPipelines; + private int silentSamplePipelineIndex; private @Transformer.ProgressState int progressState; private long progressPositionMs; private long durationUs; @@ -131,6 +133,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ImmutableList videoEffects, boolean removeAudio, boolean removeVideo, + boolean forceSilentAudio, MediaSource.Factory mediaSourceFactory, Codec.DecoderFactory decoderFactory, Codec.EncoderFactory encoderFactory, @@ -145,6 +148,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.transformationRequest = transformationRequest; this.audioProcessors = audioProcessors; this.videoEffects = videoEffects; + this.forceSilentAudio = forceSilentAudio; this.decoderFactory = decoderFactory; this.encoderFactory = encoderFactory; this.frameProcessorFactory = frameProcessorFactory; @@ -168,6 +172,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; componentListener, clock); samplePipelines = new ArrayList<>(); + silentSamplePipelineIndex = C.INDEX_UNSET; dequeueBufferConditionVariable = new ConditionVariable(); muxerWrapper = new MuxerWrapper( @@ -258,6 +263,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; while (samplePipeline.processData()) {} pendingInputBuffer = samplePipeline.dequeueInputBuffer(); dequeueBufferConditionVariable.open(); + + if (forceSilentAudio) { + while (samplePipelines.get(silentSamplePipelineIndex).processData()) {} + } } private void queueInputInternal(int samplePipelineIndex) throws TransformationException { @@ -391,6 +400,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; trackRegistered = true; muxerWrapper.registerTrack(); fallbackListener.registerTrack(); + + if (forceSilentAudio) { + muxerWrapper.registerTrack(); + fallbackListener.registerTrack(); + } } @Override @@ -410,6 +424,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; int samplePipelineIndex = tracksAddedCount; tracksAddedCount++; + + if (forceSilentAudio) { + Format silentAudioFormat = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setSampleRate(44100) + .setChannelCount(2) + .build(); + SamplePipeline audioSamplePipeline = + getSamplePipeline(silentAudioFormat, streamStartPositionUs, streamOffsetUs); + internalHandler + .obtainMessage(MSG_REGISTER_SAMPLE_PIPELINE, audioSamplePipeline) + .sendToTarget(); + silentSamplePipelineIndex = tracksAddedCount; + tracksAddedCount++; + } + return new SamplePipelineInput(samplePipelineIndex, samplePipeline.expectsDecodedData()); } @@ -459,6 +490,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; streamOffsetUs, transformationRequest, audioProcessors, + forceSilentAudio ? durationUs : C.TIME_UNSET, encoderFactory, muxerWrapper, /* listener= */ this, @@ -509,6 +541,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (!audioProcessors.isEmpty()) { return true; } + if (forceSilentAudio) { + return true; + } + return false; } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/SilentAudioGeneratorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/SilentAudioGeneratorTest.java new file mode 100644 index 0000000000..8f88124893 --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/SilentAudioGeneratorTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 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 androidx.media3.transformer; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link SilentAudioGenerator}. */ +@RunWith(AndroidJUnit4.class) +public class SilentAudioGeneratorTest { + + @Test + public void numberOfBytesProduced_isCorrect() { + SilentAudioGenerator generator = + new SilentAudioGenerator( + /* totalDurationUs= */ 3_000_000, /* sampleRate= */ 88_200, /* frameSize= */ 12); + int bytesOutput = 0; + while (!generator.isEnded()) { + ByteBuffer output = generator.getBuffer(); + bytesOutput += output.remaining(); + // "Consume" buffer. + output.position(output.limit()); + } + + // 88_200 * 12 * 3s = 3175200 + assertThat(bytesOutput).isEqualTo(3_175_200); + } + + @Test + public void lastBufferProduced_isCorrectSize() { + SilentAudioGenerator generator = + new SilentAudioGenerator( + /* totalDurationUs= */ 1_000_000, /* sampleRate= */ 44_100, /* frameSize= */ 4); + + int currentBufferSize = 0; + while (!generator.isEnded()) { + ByteBuffer output = generator.getBuffer(); + currentBufferSize = output.remaining(); + // "Consume" buffer. + output.position(output.limit()); + } + + // Last buffer is smaller and only outputs the 'leftover' bytes. + // (44_100 * 4) % 4096 = 272 + assertThat(currentBufferSize).isEqualTo(272); + } + + @Test + public void totalBytesLowerThanDefaultBufferSize_smallBufferProduced() { + SilentAudioGenerator generator = + new SilentAudioGenerator( + /* totalDurationUs= */ 5_000, /* sampleRate= */ 48_000, /* frameSize= */ 4); + // 5_000 * 48_000 * 4 / 1_000_000 = 960 + assertThat(generator.getBuffer().remaining()).isEqualTo(960); + } +} diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java index 3a2be95566..6e7e2f32a8 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -265,6 +265,21 @@ public final class TransformerEndToEndTest { context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".novideo")); } + @Test + public void startTransformation_silentAudio_completesSuccessfully() throws Exception { + Transformer transformer = + createTransformerBuilder(/* enableFallback= */ false) + .experimentalSetForceSilentAudio(true) + .build(); + MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput( + context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".silentaudio")); + } + @Test public void startTransformation_withMultipleListeners_callsEachOnCompletion() throws Exception { Transformer.Listener mockListener1 = mock(Transformer.Listener.class);