Skip to content

Commit 33dfe7d

Browse files
ychaparovcopybara-github
authored andcommitted
Add CompositionPlayer API to use a custom AudioMixer.Factory
PiperOrigin-RevId: 786264876
1 parent c2a09a0 commit 33dfe7d

File tree

2 files changed

+125
-2
lines changed

2 files changed

+125
-2
lines changed

libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public static final class Builder {
123123

124124
private @MonotonicNonNull Looper looper;
125125
private @MonotonicNonNull AudioSink audioSink;
126+
private AudioMixer.Factory audioMixerFactory;
126127
private MediaSource.Factory mediaSourceFactory;
127128
private ImageDecoder.Factory imageDecoderFactory;
128129
private boolean videoPrewarmingEnabled;
@@ -140,6 +141,7 @@ public static final class Builder {
140141
*/
141142
public Builder(Context context) {
142143
this.context = context.getApplicationContext();
144+
audioMixerFactory = new DefaultAudioMixer.Factory();
143145
mediaSourceFactory = new DefaultMediaSourceFactory(context);
144146
imageDecoderFactory =
145147
new BitmapFactoryImageDecoder.Factory(context)
@@ -177,6 +179,21 @@ public Builder setAudioSink(AudioSink audioSink) {
177179
return this;
178180
}
179181

182+
/**
183+
* Sets the {@link AudioMixer.Factory} to be used when {@linkplain AudioMixer audio mixing} is
184+
* needed.
185+
*
186+
* <p>The default value is a {@link DefaultAudioMixer.Factory} with default values.
187+
*
188+
* @param audioMixerFactory A {@link AudioMixer.Factory}.
189+
* @return This builder.
190+
*/
191+
@CanIgnoreReturnValue
192+
public Builder setAudioMixerFactory(AudioMixer.Factory audioMixerFactory) {
193+
this.audioMixerFactory = audioMixerFactory;
194+
return this;
195+
}
196+
180197
/**
181198
* Sets the {@link MediaSource.Factory} that *creates* the {@link MediaSource} for {@link
182199
* EditedMediaItem#mediaItem MediaItems} in a {@link Composition}.
@@ -359,6 +376,7 @@ public CompositionPlayer build() {
359376
private final HandlerWrapper applicationHandler;
360377
private final List<SequencePlayerHolder> playerHolders;
361378
private final AudioSink finalAudioSink;
379+
private final AudioMixer.Factory audioMixerFactory;
362380
private final MediaSource.Factory mediaSourceFactory;
363381
private final ImageDecoder.Factory imageDecoderFactory;
364382
private final VideoGraph.Factory videoGraphFactory;
@@ -406,6 +424,7 @@ private CompositionPlayer(Builder builder) {
406424
clock = builder.clock;
407425
applicationHandler = clock.createHandler(builder.looper, /* callback= */ null);
408426
finalAudioSink = checkNotNull(builder.audioSink);
427+
audioMixerFactory = builder.audioMixerFactory;
409428
mediaSourceFactory = builder.mediaSourceFactory;
410429
imageDecoderFactory = new GapHandlingDecoderFactory(builder.imageDecoderFactory);
411430
videoGraphFactory = checkNotNull(builder.videoGraphFactory);
@@ -816,8 +835,7 @@ private void prepareCompositionPlayerInternal() {
816835
// must done on the playback thread only, to ensure related components are accessed from one
817836
// thread only.
818837
playbackAudioGraphWrapper =
819-
new PlaybackAudioGraphWrapper(
820-
new DefaultAudioMixer.Factory(), checkNotNull(finalAudioSink));
838+
new PlaybackAudioGraphWrapper(audioMixerFactory, checkNotNull(finalAudioSink));
821839
VideoFrameReleaseControl videoFrameReleaseControl =
822840
new VideoFrameReleaseControl(
823841
context, new CompositionFrameTimingEvaluator(), /* allowedJoiningTimeMs= */ 0);

libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerAudioPlaybackTest.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import android.content.Context;
2929
import androidx.media3.common.MediaItem;
3030
import androidx.media3.common.Player;
31+
import androidx.media3.common.audio.AudioProcessor;
3132
import androidx.media3.exoplayer.audio.AudioSink;
3233
import androidx.media3.test.utils.CapturingAudioSink;
3334
import androidx.media3.test.utils.DumpFileAsserts;
@@ -36,7 +37,9 @@
3637
import androidx.test.core.app.ApplicationProvider;
3738
import androidx.test.ext.junit.runners.AndroidJUnit4;
3839
import java.io.IOException;
40+
import java.nio.ByteBuffer;
3941
import java.util.concurrent.TimeoutException;
42+
import java.util.concurrent.atomic.AtomicLong;
4043
import org.junit.Before;
4144
import org.junit.Test;
4245
import org.junit.runner.RunWith;
@@ -720,6 +723,108 @@ public void playSingleSequence_replayAfterEnd_outputCorrectSamples() throws Exce
720723
PREVIEW_DUMP_FILE_EXTENSION + FILE_AUDIO_RAW + "_playedTwice.dump");
721724
}
722725

726+
@Test
727+
public void playSingleSequence_withCustomAudioMixer_mixesTheCorrectNumberOfBytes()
728+
throws Exception {
729+
AtomicLong bytesMixed = new AtomicLong();
730+
AudioMixer.Factory forwardingAudioMixerFactory =
731+
() ->
732+
new ForwardingAudioMixer(new DefaultAudioMixer.Factory().create()) {
733+
@Override
734+
public void queueInput(int sourceId, ByteBuffer sourceBuffer) {
735+
bytesMixed.addAndGet(sourceBuffer.remaining());
736+
super.queueInput(sourceId, sourceBuffer);
737+
}
738+
};
739+
CompositionPlayer player =
740+
new CompositionPlayer.Builder(context)
741+
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
742+
.setAudioMixerFactory(forwardingAudioMixerFactory)
743+
.build();
744+
EditedMediaItem editedMediaItem =
745+
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW))
746+
.setDurationUs(1_000_000L)
747+
.build();
748+
EditedMediaItemSequence sequence = new EditedMediaItemSequence.Builder(editedMediaItem).build();
749+
Composition composition = new Composition.Builder(sequence).build();
750+
751+
player.setComposition(composition);
752+
player.prepare();
753+
player.play();
754+
TestPlayerRunHelper.advance(player).untilState(Player.STATE_ENDED);
755+
player.release();
756+
757+
// Expect 1 second of single-channel, 44_100Hz, 2 bytes per sample.
758+
assertThat(bytesMixed.get()).isEqualTo(88_200);
759+
}
760+
761+
private static class ForwardingAudioMixer implements AudioMixer {
762+
763+
private final AudioMixer wrappedAudioMixer;
764+
765+
public ForwardingAudioMixer(AudioMixer audioMixer) {
766+
wrappedAudioMixer = audioMixer;
767+
}
768+
769+
@Override
770+
public void configure(
771+
AudioProcessor.AudioFormat outputAudioFormat, int bufferSizeMs, long startTimeUs)
772+
throws AudioProcessor.UnhandledAudioFormatException {
773+
wrappedAudioMixer.configure(outputAudioFormat, bufferSizeMs, startTimeUs);
774+
}
775+
776+
@Override
777+
public void setEndTimeUs(long endTimeUs) {
778+
wrappedAudioMixer.setEndTimeUs(endTimeUs);
779+
}
780+
781+
@Override
782+
public boolean supportsSourceAudioFormat(AudioProcessor.AudioFormat sourceFormat) {
783+
return wrappedAudioMixer.supportsSourceAudioFormat(sourceFormat);
784+
}
785+
786+
@Override
787+
public int addSource(AudioProcessor.AudioFormat sourceFormat, long startTimeUs)
788+
throws AudioProcessor.UnhandledAudioFormatException {
789+
return wrappedAudioMixer.addSource(sourceFormat, startTimeUs);
790+
}
791+
792+
@Override
793+
public boolean hasSource(int sourceId) {
794+
return wrappedAudioMixer.hasSource(sourceId);
795+
}
796+
797+
@Override
798+
public void setSourceVolume(int sourceId, float volume) {
799+
wrappedAudioMixer.setSourceVolume(sourceId, volume);
800+
}
801+
802+
@Override
803+
public void removeSource(int sourceId) {
804+
wrappedAudioMixer.removeSource(sourceId);
805+
}
806+
807+
@Override
808+
public void queueInput(int sourceId, ByteBuffer sourceBuffer) {
809+
wrappedAudioMixer.queueInput(sourceId, sourceBuffer);
810+
}
811+
812+
@Override
813+
public ByteBuffer getOutput() {
814+
return wrappedAudioMixer.getOutput();
815+
}
816+
817+
@Override
818+
public boolean isEnded() {
819+
return wrappedAudioMixer.isEnded();
820+
}
821+
822+
@Override
823+
public void reset() {
824+
wrappedAudioMixer.reset();
825+
}
826+
}
827+
723828
private static CompositionPlayer createCompositionPlayer(Context context, AudioSink audioSink) {
724829
return new CompositionPlayer.Builder(context)
725830
.setClock(new FakeClock(/* isAutoAdvancing= */ true))

0 commit comments

Comments
 (0)