Skip to content

Commit

Permalink
feat: support multiple audio tracks (base on ExoPlayer version 2.17.1)
Browse files Browse the repository at this point in the history
多音轨支持的一般思路:
1. 自定义 AudioRenderer 和 RendererFactory,创建多个 audio renderer
2. 自定义 TrackSelector,建立 track 和 renderer 之间的映射关系
3. build ExoPlayer 时设置自定义的 RendererFactory 和 TrackSelector
参考 google#6589

此外,ExoPlayer 音频播放使用系统 media 的 AudioTrack,在版本 2.13.0 时,
ExoPlayer 变更了 audioSessionId 的生成方式和时机,导致多个音轨 renderer
对应了同一个 audioSessionId 的 AudioTrack,从而播放异常。
所以,此版本 ExoPlayer 的多音轨支持基于上述思路又额外重构了 audioSessionId
的生成方式。
ExoPlayer audioSessionId 相关变动参考:
google@a95b2eb

note for dev:
```kotlin
val renderersFactory = MultiAudioRenderersFactory(
    context,
    audioRenderersCount = 2,
    mediaClockRendererIndex = 0
)
val exoPlayer = ExoPlayer.Builder(context)
    .setRenderersFactory(renderersFactory)
    .build()
```
  • Loading branch information
Bob committed Jun 2, 2022
1 parent 8129675 commit 773ea06
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import androidx.annotation.Nullable;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer.InsufficientCapacityException;
import com.google.android.exoplayer2.source.SampleStream;
Expand Down Expand Up @@ -80,6 +81,12 @@ public MediaClock getMediaClock() {
return null;
}

@Override
@Nullable
public AudioSink getAudioSink() {
return null;
}

@Override
public final int getState() {
return state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,11 @@ protected AudioSink buildAudioSink(
.build();
}

@Nullable
protected AudioSink buildAudioSink(){
return buildAudioSink(context, enableFloatOutput, enableAudioTrackPlaybackParams, enableOffload);
}

/**
* Returns the {@link MediaCodecAdapter.Factory} that will be used when creating {@link
* com.google.android.exoplayer2.mediacodec.MediaCodecRenderer} instances.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,7 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer)
playlistMetadata = MediaMetadata.EMPTY;
staticAndDynamicMediaMetadata = MediaMetadata.EMPTY;
maskingWindowIndex = C.INDEX_UNSET;
if (Util.SDK_INT < 21) {
audioSessionId = initializeKeepSessionIdAudioTrack(C.AUDIO_SESSION_ID_UNSET);
} else {
audioSessionId = Util.generateAudioSessionIdV21(applicationContext);
}
audioSessionId = findAudioSessionIdByMediaClock();
currentCues = ImmutableList.of();
throwsWhenUsingWrongThread = true;

Expand All @@ -362,8 +358,7 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer)
deviceInfo = createDeviceInfo(streamVolumeManager);
videoSize = VideoSize.UNKNOWN;

sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
sendAudioSessionIdToRenderers();
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, audioAttributes);
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_SCALING_MODE, videoScalingMode);
sendRendererMessage(
Expand All @@ -378,6 +373,43 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer)
}
}

private int findAudioSessionIdByMediaClock() {
int audioSessionId = C.AUDIO_SESSION_ID_UNSET;
if (renderers.length > 0) {
int firstAudioTrackSessionId = C.AUDIO_SESSION_ID_UNSET;
boolean foundMediaClockAudioTrackSessionId = false;
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == TRACK_TYPE_AUDIO
&& renderer.getAudioSink() != null) {
int currentSessionId = renderer.getAudioSink().getAudioSessionId();
if (renderer.getMediaClock() != null) {
// 寻找基准时钟所在音轨的 audioSessionId
audioSessionId = currentSessionId;
foundMediaClockAudioTrackSessionId = true;
break;
}
if (firstAudioTrackSessionId == C.AUDIO_SESSION_ID_UNSET) {
// 记录第一个音轨的 audioSessionId
firstAudioTrackSessionId = currentSessionId;
}
}
}
if (!foundMediaClockAudioTrackSessionId) {
// 没找着作为基准时钟的音轨,则使用第一个音轨的 audioSessionId
audioSessionId = firstAudioTrackSessionId;
}
}
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
// 所有 renderers 均未设置 audioSessionId
if (Util.SDK_INT < 21) {
audioSessionId = initializeKeepSessionIdAudioTrack(C.AUDIO_SESSION_ID_UNSET);
} else {
audioSessionId = Util.generateAudioSessionIdV21(applicationContext);
}
}
return audioSessionId;
}

@SuppressWarnings("deprecation") // Returning deprecated class.
@Override
@Deprecated
Expand Down Expand Up @@ -1388,8 +1420,7 @@ public void setAudioSessionId(int audioSessionId) {
initializeKeepSessionIdAudioTrack(audioSessionId);
}
this.audioSessionId = audioSessionId;
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
sendAudioSessionIdToRenderers();
int finalAudioSessionId = audioSessionId;
listeners.sendEvent(
EVENT_AUDIO_SESSION_ID, listener -> listener.onAudioSessionIdChanged(finalAudioSessionId));
Expand Down Expand Up @@ -2608,6 +2639,16 @@ private void sendRendererMessage(
}
}

private void sendAudioSessionIdToRenderers() {
for (Renderer renderer : renderers) {
int trackType = renderer.getTrackType();
if (trackType == TRACK_TYPE_VIDEO ||
(trackType == TRACK_TYPE_AUDIO && renderer.getMediaClock() != null)) {
createMessageInternal(renderer).setType(MSG_SET_AUDIO_SESSION_ID).setPayload(audioSessionId).send();
}
}
}

/**
* Initializes {@link #keepSessionIdAudioTrack} to keep an audio session ID alive. If the audio
* session ID is {@link C#AUDIO_SESSION_ID_UNSET} then a new audio session ID is generated.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.google.android.exoplayer2;

import android.content.Context;
import android.os.Handler;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.MultiAudioRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.util.Util;
import java.lang.reflect.Constructor;
import java.util.ArrayList;

public class MultiAudioRenderersFactory extends DefaultRenderersFactory {

private final int audioRenderersCount;
private final int mediaClockRendererIndex;

/**
* @param context A {@link Context}.
*/
public MultiAudioRenderersFactory(
Context context,
int audioRenderersCount,
int mediaClockRendererIndex) {
super(context);
this.audioRenderersCount = Math.max(audioRenderersCount, 1);
this.mediaClockRendererIndex = mediaClockRendererIndex;
}

@Override
protected void buildAudioRenderers(
Context context,
@ExtensionRendererMode int extensionRendererMode,
MediaCodecSelector mediaCodecSelector,
boolean enableDecoderFallback,
AudioSink audioSink,
Handler eventHandler,
AudioRendererEventListener eventListener,
ArrayList<Renderer> out) {
for (int i = 0; i < audioRenderersCount; i++) {
AudioSink uniqueAudioSink = buildAudioSink();
if (Util.SDK_INT >= 21) {
int audioSessionId = Util.generateAudioSessionIdV21(context);
uniqueAudioSink.setAudioSessionId(audioSessionId);
} else {
// fixme
}
boolean provideMediaClock = mediaClockRendererIndex == i;
MultiAudioRenderer renderer = new MultiAudioRenderer(
context,
mediaCodecSelector,
enableDecoderFallback,
eventHandler,
eventListener,
uniqueAudioSink,
provideMediaClock
);
out.add(renderer);
}

if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return;
}
int extensionRendererIndex = out.size();
if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
extensionRendererIndex--;
}

try {
// Full class names used for constructor args so the LINT rule triggers if any of them move.
Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
Constructor<?> constructor =
clazz.getConstructor(
android.os.Handler.class,
com.google.android.exoplayer2.audio.AudioRendererEventListener.class,
com.google.android.exoplayer2.audio.AudioSink.class);
Renderer renderer =
(Renderer) constructor.newInstance(eventHandler, eventListener, audioSink);
out.add(extensionRendererIndex++, renderer);
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
// The extension is present, but instantiation failed.
throw new RuntimeException("Error instantiating Opus extension", e);
}

try {
// Full class names used for constructor args so the LINT rule triggers if any of them move.
Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
Constructor<?> constructor =
clazz.getConstructor(
android.os.Handler.class,
com.google.android.exoplayer2.audio.AudioRendererEventListener.class,
com.google.android.exoplayer2.audio.AudioSink.class);
Renderer renderer =
(Renderer) constructor.newInstance(eventHandler, eventListener, audioSink);
out.add(extensionRendererIndex++, renderer);
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
// The extension is present, but instantiation failed.
throw new RuntimeException("Error instantiating FLAC extension", e);
}

try {
// Full class names used for constructor args so the LINT rule triggers if any of them move.
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
Constructor<?> constructor =
clazz.getConstructor(
android.os.Handler.class,
com.google.android.exoplayer2.audio.AudioRendererEventListener.class,
com.google.android.exoplayer2.audio.AudioSink.class);
Renderer renderer =
(Renderer) constructor.newInstance(eventHandler, eventListener, audioSink);
out.add(extensionRendererIndex++, renderer);
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
// The extension is present, but instantiation failed.
throw new RuntimeException("Error instantiating FFmpeg extension", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.MediaClock;
Expand Down Expand Up @@ -273,6 +274,9 @@ interface WakeupListener {
@Nullable
MediaClock getMediaClock();

@Nullable
AudioSink getAudioSink();

/**
* Returns the current state of the renderer.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,9 @@ boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs, int encodedAcce
/** Sets the audio session id. */
void setAudioSessionId(int audioSessionId);

/** Gets the audio session id. */
int getAudioSessionId();

/** Sets the auxiliary effect. */
void setAuxEffectInfo(AuxEffectInfo auxEffectInfo);

Expand All @@ -443,6 +446,8 @@ boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs, int encodedAcce
*/
void setVolume(float volume);

float getVolume();

/** Pauses playback. */
void pause();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,11 @@ public void setAudioSessionId(int audioSessionId) {
}
}

@Override
public int getAudioSessionId() {
return audioSessionId;
}

@Override
public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) {
if (this.auxEffectInfo.equals(auxEffectInfo)) {
Expand Down Expand Up @@ -1381,6 +1386,11 @@ private void setVolumeInternal() {
}
}

@Override
public float getVolume(){
return volume;
}

@Override
public void pause() {
playing = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ public void setAudioSessionId(int audioSessionId) {
sink.setAudioSessionId(audioSessionId);
}

@Override
public int getAudioSessionId() {
return sink.getAudioSessionId();
}

@Override
public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) {
sink.setAuxEffectInfo(auxEffectInfo);
Expand All @@ -149,6 +154,11 @@ public void setVolume(float volume) {
sink.setVolume(volume);
}

@Override
public float getVolume() {
return sink.getVolume();
}

@Override
public void pause() {
sink.pause();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ public MediaClock getMediaClock() {
return this;
}

@Override
public AudioSink getAudioSink(){
return audioSink;
}

@Override
protected float getCodecOperatingRateV23(
float targetPlaybackSpeed, Format format, Format[] streamFormats) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.google.android.exoplayer2.audio;

import android.content.Context;
import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.util.MediaClock;

public class MultiAudioRenderer extends MediaCodecAudioRenderer {

private final boolean provideMediaClock;

public MultiAudioRenderer(
Context context,
MediaCodecSelector mediaCodecSelector,
boolean enableDecoderFallback,
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioSink audioSink,
boolean provideMediaClock) {
super(
context,
mediaCodecSelector,
enableDecoderFallback,
eventHandler,
eventListener,
audioSink);
this.provideMediaClock = provideMediaClock;
}

@Nullable
@Override
public MediaClock getMediaClock() {
return provideMediaClock ? this : null;
}
}
Loading

0 comments on commit 773ea06

Please sign in to comment.