Add an option to force a silent audio track.

PiperOrigin-RevId: 492154544
This commit is contained in:
samrobinson 2022-12-01 11:45:40 +00:00 committed by Ian Baker
parent c5dc5aeb11
commit 0993479efe
7 changed files with 744 additions and 5 deletions

View file

@ -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

View file

@ -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<AudioProcessor> 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.

View file

@ -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;
}
}

View file

@ -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<Effect> videoEffects;
private boolean removeAudio;
private boolean removeVideo;
private boolean forceSilentAudio;
private ListenerSet<Transformer.Listener> 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.
*
* <p>This method is experimental and may be removed or changed without warning.
*
* <p>Audio properties/format:
*
* <ul>
* <li>Duration will match duration of the input media.
* <li>Sample mime type will match {@link TransformationRequest#audioMimeType}, or {@link
* MimeTypes#AUDIO_AAC} if {@code null}.
* <li>Sample rate will be 44100hz. This can be modified by passing a {@link
* SonicAudioProcessor} to {@link #setAudioProcessors(List)}, using {@link
* SonicAudioProcessor#setOutputSampleRateHz(int)}.
* <li>Channel count will be 2. This can be modified by implementing a custom {@link
* AudioProcessor} and passing it to {@link #setAudioProcessors(List)}.
* </ul>
*
* @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<Effect> videoEffects;
private final boolean removeAudio;
private final boolean removeVideo;
private final boolean forceSilentAudio;
private final ListenerSet<Transformer.Listener> listeners;
private final MediaSource.Factory mediaSourceFactory;
private final FrameProcessor.Factory frameProcessorFactory;
@ -574,7 +606,8 @@ public final class Transformer {
ImmutableList<Effect> videoEffects,
boolean removeAudio,
boolean removeVideo,
ListenerSet<Transformer.Listener> listeners,
boolean forceSilentAudio,
ListenerSet<Listener> 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,

View file

@ -97,6 +97,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final TransformationRequest transformationRequest;
private final ImmutableList<AudioProcessor> audioProcessors;
private final ImmutableList<Effect> 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<Effect> 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;
}

View file

@ -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);
}
}

View file

@ -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);