在用户使用 “一起看”、“一起听” 等互动娱乐类应用时,如果需要在通话过程中播放音乐文件等,而且让房间内的其他成员也听到声音,需要使用音乐播放类。
播放音乐指的是时长较长的音频或 PCM 数据,例如,伴奏、背景音乐等。同一时间,只能播放 4 个音乐文件。

一般来说,时长小于 20 s 的音频为音效,应使用音效播放器接口,参考 播放音效 。

前提条件

你已经集成 RTC SDK,实现了基本的音视频通话。
支持音乐播放的 SDK 详见API 及回调。

功能实现

参考以下顺序,调用 API 实现此功能:

调用目标非 PCM 音频文件PCM 音频文件
初始化getMediaPlayergetMediaPlayer
设置回调句柄setEventHandlersetEventHandler
启动openopenWithCustomSource
开始播放startpushExternalAudioFrame
结束stopstop
创建引擎

创建音视频引擎类后,你可以通过调用 getMediaPlayer,传入播放器 ID 创建一个音乐播放器类。

如果你需要将音频发送到远端,还需要加入房间并发布音频流,参考 构建 RTC 应用 获取详细步骤。

// 创建引擎
rtcVideo = RTCVideo.createRTCVideo(this, Constants.APP_ID, rtcVideoEventHandler, null, null);
// 开启本地音频采集
rtcVideo.startAudioCapture();
//播放非 PCM 文件
mediaPlayer = rtcVideo.getMediaPlayer(PLAYER_ID_1);
java
// 创建引擎
self.rtcVideo = ByteRTCVideo.createRTCVideo(kAppID, delegate: self, parameters: [:])
// 开启本地音频采集
self.rtcVideo?.startAudioCapture()
//播放非 PCM 文件
self.mediaPlayerAudio = self.rtcVideo?.getMediaPlayer(audioPlayerId)
swift
// 创建引擎
video = bytertc::createRTCVideo(appid, handler, nullptr);
// 开启本地音频采集
video->startAudioCapture();
//播放非 PCM 文件
player_audio = video->getMediaPlayer(id1); 
cpp
设置回调

你可以通过回调感知音乐的播放状态和播放位置。

// handler 建议使用强引用
mediaPlayer.setEventHandler(playerEventHandler);
java
// 注意使用弱引用
weak var weakSelf = self
self.mediaPlayerAudio?.setEventHandler(weakSelf)
// 设置播放进度回调间隔,以每 1s 回调一次为例
self.mediaPlayerAudio?.setProgressInterval(1000)

func onMediaPlayerStateChanged(_ playerId: Int32, state: ByteRTCPlayerState, error: ByteRTCPlayerError) {
    //播放状态回调
}

func onMediaPlayerPlayingProgress(_ playerId: Int32, progress: Int64) {
    //播放进度回调
}
swift
handler = new EventHandler();
player_audio->setEventHandler(handler);

EventHandler:public bytertc::IMediaPlayerEventHandler {
virtual void onMediaPlayerStateChanged(int playerId, bytertc::PlayerState state, bytertc::PlayerError error){
    //播放状态回调
}

virtual void onMediaPlayerPlayingProgress(int playerId, int64_t progress){
    //播放进度回调
}
};

// setProgressInterval 设置播放间隔,该接口需在播放状态中调用
cpp
播放非 PCM 音频文件

对同一个音频文件进行操作时,混音 ID 应保持一致

打开音乐文件
private void openMedia(String filePath) {
    MediaPlayerConfig playerConfig = new MediaPlayerConfig();
    playerConfig.startPos = 0;
	// autoPlay=true时自动播放,无需再调用 start 接口
	// autoPlay=false时,不自动播放,需要调用 start 接口
    playerConfig.autoPlay = false;
    playerConfig.playCount = 1; // 播放次数
    playerConfig.type = AudioMixingType.AUDIO_MIXING_TYPE_PLAYOUT_AND_PUBLISH;
    playerConfig.callbackOnProgressInterval = 500;
    mediaPlayer.open(filePath, playerConfig);
}
java
let config = ByteRTCMediaPlayerConfig.init()
config.type = .playoutAndPublish
config.playCount = 1 // 播放次数
// autoPlay=true时自动播放,无需再调用 start 接口
// autoPlay=false时,不自动播放,需要调用 start 接口
config.autoPlay = true

self.mediaPlayerAudio?.open(filePath, config: config)
swift
bytertc::MediaPlayerConfig config;
// auto_play=true 时自动播放,无需再调用 start 接口, 
// auto_play=false 时,不自动播放,需要调用 start 接口
config.auto_play = true; 
config.callback_on_progress_interval = 500;
config.play_count = 1;
config.start_pos = 0;
config.sync_progress_to_record_frame = true; 
config.type = bytertc::kAudioMixingTypePlayoutAndPublish;
int ret = player_audio->open(url, config);
cpp
手动播放

已经调用 open,且 auto_play = false 时,需要调用 start 播放音乐。

mediaPlayer.start()
java
self.mediaPlayerAudio?.start()
swift
int ret = player_audio->start();
cpp
停止播放

如果你需要在播放中主动终止播放,可以调用 stop 方法。

mediaPlayer.stop()
java
self.mediaPlayerAudio?.stop()
swift
int ret = player_audio->stop();
cpp
暂停/恢复音乐
mediaPlayer.pause()
mediaPlayer.resume()
java
self.mediaPlayerAudio?.pause()
self.mediaPlayerAudio?.resume()
swift
int ret = player_audio->pause();
int ret = player_audio->resume();
cpp
获取和设置音乐属性

所有的混音相关设置都必须在开始播放以后,停止播放之前进行,否则不生效,包括但不限于设置播放音量,设置播放起始位置等。

// 获取音量
float volume = mediaPlayer.getVolume();
// 设置音量
mediaPlayer.setVolume(value, type);

// 文件总时长,单位 ms
int totalTime = mediaPlayer.getTotalDuration();
// 播放时长,单位 ms
int playbackDuration = mediaPlayer.getPlaybackDuration();
// 播放位置,单位 ms
int currentPosition = mediaPlayer.getPosition();
// 设置起播位置,单位 ms
mediaPlayer.setPosition(value);

// 音轨
int trackCount = mediaPlayer.getAudioTrackCount();
// 选择播放指定的音轨音乐
mediaPlayer.selectAudioTrack(track);

// 播放速度,单位:%,取值范围为[50, 200],默认值为100
mediaPlayer.setPlaybackSpeed(speed);
java
// 获取音量
self.mediaPlayerAudio?.getVolume(.playoutAndPublish)
// 设置音量
self.mediaPlayerAudio?.setVolume(value, type: .playoutAndPublish)

//文件总时长,单位 ms
let totalTime = self.mediaPlayerAudio?.getTotalDuration()
// 播放时长,单位 ms
self.mediaPlayerAudio?.getPlaybackDuration()
// 播放位置,单位 ms
self.mediaPlayerAudio?.getPosition()
// 设置起播位置,单位 ms
self.mediaPlayerAudio?.setPosition(value)

// 变调,取值范围 [-12, 12]
self.mediaPlayerAudio?.setAudioPitch(value)

// 声道模式
self.mediaPlayerAudio?.setAudioDualMonoMode(mode)

// 音轨
let tracks = self.mediaPlayerAudio?.getAudioTrackCount()

//选择播放指定的音轨音乐
self.mediaPlayerAudio?.selectAudioTrack(track)

// 播放速度,单位:%,取值范围为[50, 200],默认值为100
self.mediaPlayerAudio?.setPlaybackSpeed(speed)
swift
// 获取音量
int volume = player_audio->getVolume(type);
// 设置音量
int ret = player_audio->setVolume(vol);

//文件总时长,单位 ms
int length = player_audio->getTotalDuration();
// 播放时长,单位 ms
int length = player_audio->getPlaybackDuration();
// 播放位置,单位 ms
int pos = player_audio->getPosition(); 
// 设置起播位置,单位ms
int ret = player_audio->setPosition(); 

// 变调,取值范围 [-12, 12]
int ret = player_audio->setAudioPitch(pitch); 

// 声道模式
int ret = player_audio->setAudioDualMonoMode(mode);

//获取音轨数
int count = player_audio->getAudioTrackCount(); 
//选择播放指定的音轨音乐
int ret = player_audio->selectAudioTrack(index);

// 播放速度,单位:%,取值范围为 [50,200],默认值为 100
int ret = player_audio->setPlaybackSpeed(speed); 
cpp
播放 PCM 文件
启动混音

在调用 openWithCustomSource 启动混音前,可以进行初始化设置,包括是否自动播放、播放次数等。

private void startPlayPCM() {
    openFile(pcmPath);

    MediaPlayerCustomSource source = new MediaPlayerCustomSource();
    source.mode = MediaPlayerCustomSourceMode.PUSH;
    source.type = MediaPlayerCustomSourceStreamType.RAW;
    source.provider = null;

    MediaPlayerConfig playerConfig = new MediaPlayerConfig();
    playerConfig.startPos = 0;
	// autoPlay=true时自动播放,无需再调用 start 接口
	// autoPlay=false时,不自动播放,需要调用 start 接口
    playerConfig.autoPlay = true;
    playerConfig.playCount = 1;
    playerConfig.type = AudioMixingType.AUDIO_MIXING_TYPE_PLAYOUT_AND_PUBLISH;
    playerConfig.callbackOnProgressInterval = 500;

    pcmPlayer.openWithCustomSource(source, playerConfig);
    startPushPCM();
}
java
@objc func startPcmMix() {
    let source = ByteRTCMediaPlayerCustomSource.init()
    source.mode = .push
    source.type = .raw
    
    let config  = ByteRTCMediaPlayerConfig.init()
    config.type = .playoutAndPublish
    config.playCount = 1
	// autoPlay=true时自动播放,无需再调用 start 接口
	// autoPlay=false时,不自动播放,需要调用 start 接口
    config.autoPlay = true
    
    self.mediaPlayerPCM?.open(with: source, config: config)
};
swift
bytertc::MediaPlayerCustomSource source;
source.mode = bytertc::kMediaPlayerCustomSourceModePush;
source.provider = nullptr;
source.type = bytertc::kMediaPlayerCustomSourceStreamTypeRaw;
bytertc::MediaPlayerConfig config;
// auto_play=true时自动播放,无需再调用 start 接口
// auto_play=false时,不自动播放,需要调用 start 接口
config.auto_play = true; 
config.callback_on_progress_interval = 1000;
config.play_count = 1;
config.start_pos = 0;
config.sync_progress_to_record_frame = true;
config.type = bytertc::kAudioMixingTypePlayoutAndPublish;

int ret = player->openWithCustomSource(source, config);
cpp
播放混音

调用 pushExternalAudioFrame 向 RTC 推送音频数据前,你需要按照 RTC SDK 要求对数据进行处理。以下代码片段以示例项目中提供的 PCM 音频文件为例,说明数据的格式要求。

Runnable pushAudioFrameTask = new Runnable() {
    @Override
    public void run() {
        // 每10ms的数据大小
        int size = 16 * 10;
        if (isFirstPush) {
            // 建议第一次时传入 200ms 的缓冲数据,避免噪音
            size = size * 20;
            isFirstPush = false;
        }
        ByteBuffer pushBuffer = ByteBuffer.allocate(size * 2);
        AudioFrame audioFrame = new AudioFrame();
        audioFrame.channel = AudioChannel.AUDIO_CHANNEL_MONO;
        audioFrame.sampleRate = AudioSampleRate.AUDIO_SAMPLE_RATE_16000;
        audioFrame.samples = size;

        int nRead = 0;
        if (fileData != null) {
            nRead = Integer.min(size * 2, fileDataBufferSize - fileDataOffset);
            pushBuffer.put(fileData, fileDataOffset, nRead);
            fileDataOffset += nRead;
        }
        Log.i(TAG, "offset:" + fileDataOffset + " read:" + nRead);
        if (nRead == 0) {
            stopPlayPCM();
            return;
        }
        audioFrame.buffer = pushBuffer.array();
        pcmPlayer.pushExternalAudioFrame(audioFrame);
    }
};
java
func pushPCMData() {
    if let mediaPlayerPCM = self.mediaPlayerPCM {
        var size = 160 * 1 * 2 // 每 10ms 的数据 = 采样数 * 声道数 * 采样占字节数,该pcm文件是16000采样率,单声道,每个样本占2个字节
        
        if self.firstPush {
            size = size * 20 // 第一次建议推送 200ms 数据,避免噪音
            self.firstPush = false
        }
        
        // 本地文件
        let filePath = Bundle.main.path(forResource: "16k-mono-speech_fft_32s", ofType: "pcm")!
        let url = URL(fileURLWithPath: filePath)
        
        guard let fileData = try? Data(contentsOf: url) else {
            print("Failed to read PCM file data.")
            return
        }
        
        var endOffset = self.startOffset + size
        
        // 如果偏移量超过文件长度,重置为 0,表示继续播放
        if endOffset >= fileData.count {
            self.startOffset = 0
            endOffset = self.startOffset + size
        }
        
        let audioData: Data = fileData.subdata(in: startOffset..<endOffset)
        self.startOffset = endOffset
        
        let audioFrame = ByteRTCAudioFrame()
        audioFrame.channel = .mono
        audioFrame.sampleRate = .rate16000
        audioFrame.samples = Int32(size) / 2 // 每个采样占2字节
        audioFrame.buffer = audioData
        
        mediaPlayerPCM.pushExternalAudioFrame(audioFrame)
    }
}
swift
m_pcm_file = new QFile(name);
m_pcm_file->seek(0); // pcm 音频文件 seek 到初始位置

m_timer.start(10); //开启定时器

connect(m_timer, &QTimer::timeout, [](){
// 每 10ms 的数据大小 = 采样数*声道数*每个采样占字节数,该 pcm 文件是 16000 采样率,单声道,每个样本占 2个字节
    int size = 160 * 1 * 2; 
    if (m_first_push) {
        size = 20 * size; // 建议第一次时传入200ms的缓冲数据,避免噪音
        m_first_push = false;
    }

    if (m_pcm_file) {
        QByteArray data = m_pcm_file->read(size);
        if (data.size() == 0) {
            return;
        }
        bytertc::AudioFrameBuilder builder;
        builder.channel = bytertc::kAudioChannelMono;//根据 PCM 文件的声道数指定
        builder.sample_rate = bytertc::kAudioSampleRate16000; //根据 PCM 文件的采样率指定
        builder.data = (uint8_t*)data.data();
        builder.data_size = size;
        auto frame = bytertc::buildAudioFrame(builder);

        int ret = player_pcm->pushExternalAudioFrame(frame);
}});
cpp
结束混音

如果你需要在播放中主动终止播放,可以调用 stop 方法。

pcmPlayer.stop();
java
@objc func stopPcmMix()  {
    self.mediaPlayerPCM?.stop()
    self.timer?.cancel()
}
swift
int ret = player_pcm->stop();
m_timer.stop(); 
cpp
调整混音音量

音量设置必须在开始播放以后,停止播放之前进行,否则不生效。
移动端可以通过 setVolumeAll 接口,批量设置全部混音音量。PC端只能通过 setVolume 接口逐个指定混音音量。

pcmPlayer.setVolume(volume, AudioMixingType.AUDIO_MIXING_TYPE_PLAYOUT_AND_PUBLISH);
java
self.mediaPlayerAudio?.setVolume(volume, type: .playoutAndPublish)
swift
int ret = player_pcm->setVolume(volume, type);
cpp
示例项目

API 参考

说明:表格中的 macOS API 接口为 Objective-C,而示例项目中的 macOS 项目使用的是 Windows SDK 中的 API 接口。

平台AndroidiOSmacOSWindowsLinuxELectronFlutterWeb

初始化音乐管理类

getMediaPlayer

getMediaPlayer

getMediaPlayer

getMediaPlayer

getMediaPlayer

getMediaPlayer

getMediaPlayer

getAudioMixingManager

音乐混音管理类IMediaPlayerByteRTCMediaPlayerByteRTCMediaPlayerIMediaPlayerMediaPlayerRTCMediaPlayerRTCMediaPlayerAudioMixingManager
音乐播放器状态回调onMediaPlayerStateChangedonMediaPlayerStateChanged:state:error:onMediaPlayerStateChanged:state:error:onMediaPlayerStateChangedonMediaPlayerStateChangedonMediaPlayerStateChangedonMediaPlayerStateChanged无
音乐播放进度回调onMediaPlayerPlayingProgressonMediaPlayerPlayingProgress:progress:onMediaPlayerPlayingProgress:progress:onMediaPlayerPlayingProgressonMediaPlayerPlayingProgressonMediaPlayerPlayingProgressonMediaPlayerPlayingProgress无
变更日志
  1. 自客户端 SDK 3.32 起,支持对音乐文件的声道进行设置。

  2. 自客户端 SDK 3.36 起,全平台支持在线音频文件混音。

  3. 自客户端 SDK 3.38 起,支持调整音视频通话中使用的音频文件的播放速度。

  4. 自客户端 SDK 3.45.4 起,支持获取混音音频文件的实际播放时长,即歌曲不受停止、跳转、倍速、卡顿影响的播放时长。

  5. 自客户端 SDK 3.52 起,支持混音播放 24kHz 的 mp3 文件。3.52 之前版本仅支持以下采样率的音频文件:8kHz、16kHz、22.05kHz、32kHz、44.1kHz、48kHz、64kHz、96kHz、192kHz。

  6. 自客户端 SDK 3.54 起,Native 平台废弃原有的混音类,分为音效混音和音乐混音,支持分别控制。