在用户使用 “一起看”、“一起听” 等互动娱乐类应用时,如果需要在通话过程中播放音乐文件等,而且让房间内的其他成员也听到声音,需要使用音乐播放类。
播放音乐指的是时长较长的音频或 PCM 数据,例如,伴奏、背景音乐等。同一时间,只能播放 4 个音乐文件。
一般来说,时长小于 20 s 的音频为音效,应使用音效播放器接口,参考 播放音效 。
前提条件
你已经集成 RTC SDK,实现了基本的音视频通话。
支持音乐播放的 SDK 详见API 及回调。
功能实现
参考以下顺序,调用 API 实现此功能:
调用目标 | 非 PCM 音频文件 | PCM 音频文件 |
---|
初始化 | getMediaPlayer | getMediaPlayer |
设置回调句柄 | setEventHandler | setEventHandler |
启动 | open | openWithCustomSource |
开始播放 | start | pushExternalAudioFrame |
结束 | stop | stop |
创建引擎
创建音视频引擎类后,你可以通过调用 getMediaPlayer
,传入播放器 ID 创建一个音乐播放器类。
如果你需要将音频发送到远端,还需要加入房间并发布音频流,参考 构建 RTC 应用 获取详细步骤。
// 创建引擎
rtcVideo = RTCVideo.createRTCVideo(this, Constants.APP_ID, rtcVideoEventHandler, null, null);
// 开启本地音频采集
rtcVideo.startAudioCapture();
//播放非 PCM 文件
mediaPlayer = rtcVideo.getMediaPlayer(PLAYER_ID_1);
// 创建引擎
self.rtcVideo = ByteRTCVideo.createRTCVideo(kAppID, delegate: self, parameters: [:])
// 开启本地音频采集
self.rtcVideo?.startAudioCapture()
//播放非 PCM 文件
self.mediaPlayerAudio = self.rtcVideo?.getMediaPlayer(audioPlayerId)
// 创建引擎
video = bytertc::createRTCVideo(appid, handler, nullptr);
// 开启本地音频采集
video->startAudioCapture();
//播放非 PCM 文件
player_audio = video->getMediaPlayer(id1);
设置回调
你可以通过回调感知音乐的播放状态和播放位置。
// handler 建议使用强引用
mediaPlayer.setEventHandler(playerEventHandler);
// 注意使用弱引用
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) {
//播放进度回调
}
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 设置播放间隔,该接口需在播放状态中调用
播放非 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);
}
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)
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);
手动播放
已经调用 open
,且 auto_play = false
时,需要调用 start
播放音乐。
self.mediaPlayerAudio?.start()
int ret = player_audio->start();
停止播放
如果你需要在播放中主动终止播放,可以调用 stop
方法。
self.mediaPlayerAudio?.stop()
int ret = player_audio->stop();
暂停/恢复音乐
mediaPlayer.pause()
mediaPlayer.resume()
self.mediaPlayerAudio?.pause()
self.mediaPlayerAudio?.resume()
int ret = player_audio->pause();
int ret = player_audio->resume();
获取和设置音乐属性
所有的混音相关设置都必须在开始播放以后,停止播放之前进行,否则不生效,包括但不限于设置播放音量,设置播放起始位置等。
// 获取音量
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);
// 获取音量
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)
// 获取音量
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);
播放 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();
}
@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)
};
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);
播放混音
调用 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);
}
};
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)
}
}
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);
}});
结束混音
如果你需要在播放中主动终止播放,可以调用 stop
方法。
@objc func stopPcmMix() {
self.mediaPlayerPCM?.stop()
self.timer?.cancel()
}
int ret = player_pcm->stop();
m_timer.stop();
调整混音音量
音量设置必须在开始播放以后,停止播放之前进行,否则不生效。
通过 setVolume
接口逐个指定混音音量。
pcmPlayer.setVolume(volume, AudioMixingType.AUDIO_MIXING_TYPE_PLAYOUT_AND_PUBLISH);
self.mediaPlayerAudio?.setVolume(volume, type: .playoutAndPublish)
int ret = player_pcm->setVolume(volume, type);
示例项目
API 参考
说明:表格中的 macOS API 接口为 Objective-C,而示例项目中的 macOS 项目使用的是 Windows SDK 中的 API 接口。
变更日志
自客户端 SDK 3.32 起,支持对音乐文件的声道进行设置。
自客户端 SDK 3.36 起,全平台支持在线音频文件混音。
自客户端 SDK 3.38 起,支持调整音视频通话中使用的音频文件的播放速度。
自客户端 SDK 3.45.4 起,支持获取混音音频文件的实际播放时长,即歌曲不受停止、跳转、倍速、卡顿影响的播放时长。
自客户端 SDK 3.52 起,支持混音播放 24kHz 的 mp3 文件。3.52 之前版本仅支持以下采样率的音频文件:8kHz、16kHz、22.05kHz、32kHz、44.1kHz、48kHz、64kHz、96kHz、192kHz。
自客户端 SDK 3.54 起,Native 平台废弃原有的混音类,分为音效混音和音乐混音,支持分别控制。