Keep reference to audio processor (ChannelMappingAudioProcessor) without creating new SimpleExoPlayer instance everytime
See original GitHub issue### [REQUIRED] Issues. Need help change channel audio mapping processor while media on playing (on the fly).
### [REQUIRED] Searched documentation. I’ve search all documents related to ChannelMappingAudioProcessor : 2659 6288 6415 The only hint close to my issue is 6415 but i still don’t know where to start implement it from @andrewlewis comment.
### [REQUIRED] Question Keep reference to audio processor (ChannelMappingAudioProcessor) without creating new SimpleExoPlayer instance everytime
How can i keep a reference to audio processor when created and interact with the app? also synchronize access to its dynamic configuration. So i can change the audio channel while media still playing (on the fly).
@andrewlewis comment on #6415
If the output format from the mapping is staying the same (including the same number of channels) you can just keep a reference to your audio processor when you create it, and interact with it from your app. You will need to make sure you synchronize access to its dynamic configuration because ExoPlayer will use the processor on its internal playback thread. Note: the audio track buffer is downstream from the audio processor so modifications won’t be reflected in output audio instantly.
My testing code :
Structure:
-
AudioProcessor interface
-
BaseAudioProcessor class
-
ChannelMappingAudioProcessor class :
final class ChannelMappingAudioProcessor extends BaseAudioProcessor {
@Nullable private int[] pendingOutputChannels;
@Nullable private int[] outputChannels;
/**
* Resets the channel mapping. After calling this method, call {@link #configure(AudioFormat)} to
* start using the new channel map.
*
* @param outputChannels The mapping from input to output channel indices, or {@code null} to
* leave the input unchanged.
* @see AudioSink#configure(int, int, int, int, int[], int, int)
*/
public void setChannelMap(@Nullable int[] outputChannels) {
pendingOutputChannels = outputChannels;
}
@Override
public AudioFormat onConfigure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException {
@Nullable int[] outputChannels = pendingOutputChannels;
if (outputChannels == null) {
return AudioFormat.NOT_SET;
}
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
throw new UnhandledAudioFormatException(inputAudioFormat);
}
boolean active = inputAudioFormat.channelCount != outputChannels.length;
for (int i = 0; i < outputChannels.length; i++) {
int channelIndex = outputChannels[i];
if (channelIndex >= inputAudioFormat.channelCount) {
throw new UnhandledAudioFormatException(inputAudioFormat);
}
active |= (channelIndex != i);
}
return active
? new AudioFormat(inputAudioFormat.sampleRate, outputChannels.length, C.ENCODING_PCM_16BIT)
: AudioFormat.NOT_SET;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
int[] outputChannels = Assertions.checkNotNull(this.outputChannels);
int position = inputBuffer.position();
int limit = inputBuffer.limit();
int frameCount = (limit - position) / inputAudioFormat.bytesPerFrame;
int outputSize = frameCount * outputAudioFormat.bytesPerFrame;
ByteBuffer buffer = replaceOutputBuffer(outputSize);
while (position < limit) {
for (int channelIndex : outputChannels) {
buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));
}
position += inputAudioFormat.bytesPerFrame;
}
inputBuffer.position(limit);
buffer.flip();
}
@Override
protected void onFlush() {
outputChannels = pendingOutputChannels;
}
@Override
protected void onReset() {
outputChannels = null;
pendingOutputChannels = null;
}
}
All of them i copied from exoplayer github project structure in core library (audio) Testing use 1 button to switch the L + R channel to L+L. Note: i don’t need ability to change the volume on separate channel (basic only)
- MainActivity class
Method to initialize instance:
private void PrepareExoPlayerUri() {
try {
trackSelectionFactory = new AdaptiveTrackSelection.Factory();
inputAudioFormat = new AudioFormat(
44100, 2, C.ENCODING_PCM_16BIT
);
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
channelMappingAudioProcessor.setChannelMap(new int[]{0, 1}); // left + Right channel
channelMappingAudioProcessor.onConfigure(inputAudioFormat);
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this) {
@Override
protected AudioProcessor[] buildAudioProcessors() {
audioProcessor = new AudioProcessor[]{channelMappingAudioProcessor};
return audioProcessor;
}
};
trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder(this).build();
trackSelector = new DefaultTrackSelector(this, trackSelectionFactory);
trackSelector.setParameters(trackSelectorParameters);
// using uri data source
dataSourceFactory = new DefaultDataSourceFactory(this,
Util.getUserAgent(this, "exoplayer_video"));
// using uri data source
mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(videouri);
player = new SimpleExoPlayer.Builder(this, renderersFactory)
.setTrackSelector(trackSelector)
.build();
player.prepare(mediaSource);
playerView.setPlayer(player);
player.setPlayWhenReady(true);
player.addListener(playerListener);
} catch (Exception e) {
e.printStackTrace();
}
}
Button click trigger to change audio channel while media still playing :
btnSwitchChannel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
inputAudioFormat = new AudioFormat(
44100, 2, C.ENCODING_PCM_16BIT
);
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
channelMappingAudioProcessor.setChannelMap(new int[]{0, 0}); // left + left channel
channelMappingAudioProcessor.onConfigure(inputAudioFormat);
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getApplicationContext()) {
@Override
protected AudioProcessor[] buildAudioProcessors() {
audioProcessor = new AudioProcessor[]{channelMappingAudioProcessor};
return audioProcessor;
}
};
player = new SimpleExoPlayer.Builder(getApplicationContext(), renderersFactory)
.setTrackSelector(trackSelector)
.build();
player.setPlayWhenReady(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
From code above i’m still don’t understand how to change the outputChannel with new parameter on ChannelMappingAudioProcessor setChannelMap method. And how we can interact with audio processor reference on player instance without release the player object and create new player instance
Note: output format same (including number of channels)
Please correct and help me. Thanks.
Issue Analytics
- State:
- Created 3 years ago
- Comments:8
@lcf87 Thanks for your help.
after came back to this issue again, i finally figure it out.
hopefully i can help with someone encounter with the same issue as me.
for exo 2.11.8 we can set in buildAudioProcessor by changing with new channel map and call flush method quote from exoplayer dev : Clears any buffered data and pending output. If the audio processor is active, also prepares the audio processor to receive a new stream of input in the last configured (pending) format.
so the flush method that i’m missed before.
for 2.12.x we set the ChannelMappingAudioProcessor in AudioSink. same approach, call setChannelMap and the flush the audio buffer data and pendingOutputChannel.
example code:
channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); channelMappingAudioProcessor.setChannelMap(new int[]{0, 0}); -> int array depends on your media channel layout
renderersFactory = new DefaultRenderersFactory(this) { @Nullable @Override protected AudioSink buildAudioSink(Context context, boolean enableFloatOutput, boolean enableAudioTrackPlaybackParams, boolean enableOffload) {
i get this solution from another reference #8216 . thank to @timusus for pointing this out.
Sorry for the inactivity.
It seems to me that creating a player every time you hit a button isn’t the best choice, I was suggesting something that moves the audio processor, render factory and player creation out of the
onClickLIstener
, something like