mirror of
https://github.com/samsonjs/media.git
synced 2026-03-25 09:25:53 +00:00
Add an option to force a silent audio track.
PiperOrigin-RevId: 492154544
This commit is contained in:
parent
c5dc5aeb11
commit
0993479efe
7 changed files with 744 additions and 5 deletions
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue