实时方案中需要在开唱后实时同步歌曲进度,避免因歌曲误差而增加端到端延迟。同步歌曲需要基于 NTP 时间,不同设备的本地时钟并不一致,存在一定误差,因此需要引入腾讯云自研 NTP 服务。同时,中途加入合唱的用户也需要同步歌曲进度,只有同步进度后,才能参与合唱。
实现流程
歌曲同步的做法为:主唱用户约定在未来某个时间点(如延迟 N 秒后)开始播放歌曲,其他用户参与合唱。各端的时间都以 NTP 时间为准,NTP 时间会在 TRTC SDK 初始化后开始同步。
具体流程如下:
1. 各端进行 NTP 校时,向 TRTC 云端更新并获取最新的 NTP 时间 T。
2. 主唱端发送合唱信令(自定义消息),约定合唱开始时间 T2。
3. 本地根据 T2 预加载歌曲,定时播放。
4. 其他合唱用户接收到合唱信令后执行步骤 3。
5. 过程中对本地歌曲播放进度进行校验,当 TE 与 TC 差值超过 50ms 即 seek 校准。
说明:
这里的 50ms 误差是个典型值,可以根据业务容忍度适当调整,建议在 50ms 上下浮动。
时序图
歌曲同步时序主要可以分为三个部分:NTP 校时、发送及接收合唱信令、歌曲播放进度修正。下面将针对这四部分给出具体的代码实现。
关键代码实现
1. NTP 校时服务
TXLiveBase.setListener(new TXLiveBaseListener() {@Overridepublic void onUpdateNetworkTime(int errCode, String errMsg) {super.onUpdateNetworkTime(errCode, errMsg);// errCode 0: 校时成功且偏差在30ms以内;1: 校时成功但偏差可能在30ms以上;-1: 校时失败if (errCode == 0) {// 调用TXLivebase的getNetworkTimestamp即可获取NTP时间戳long ntpTime = TXLiveBase.getNetworkTimestamp();} else {// 重新调用updateNetworkTime启动一次校时TXLiveBase.updateNetworkTime();}}});TXLiveBase.updateNetworkTime();
2. 主唱端发送合唱信令
JSONObject jsonObject = new JSONObject();jsonObject.put("cmd", "startChorus");// 约定时间合唱jsonObject.put("startPlayMusicTS", startTs);jsonObject.put("musicId", "musicId");String body = jsonObject.toString();mTRTCCloud.sendCustomCmdMsg(0, body.getBytes(), false, false);
说明:
建议主唱以固定时间频率循环向房间内发送合唱信令消息,以便合唱者可以中途加入合唱。
不使用 SEI 消息发送合唱信令的原因:SEI 信息会插入到视频帧中,导致观众侧拉取的视频流中携带很多无效信息。
3. 合唱端接收合唱信令
public void onRecvCustomCmdMsg(String userId, int cmdID, int seq, byte[] message) {JSONObject json = new JSONObject(new String(message, "UTF-8"));String cmd = json.getString("cmd");// 合唱命令if (cmd.equals("startChorus")) {// 合唱开始时间long startPlayMusicTs = json.getLong("startPlayMusicTS");int musicId = json.getInt("musicId");// 合唱约定时间和当前时间差值long delayMs = Math.abs(startPlayMusicTs - getNtpTime());// 开启预加载,根据合唱约定时间和当前ntp差值,跳跃歌曲进度mTRTCCloud.callExperimentalAPI("{\\"api\\":\\"preloadMusic\\",\\"params\\": {\\"musicId\\":musicId,\\"path\\":\\"path\\",\\"startTimeMS\\":delayMs}}");// 播放歌曲TXAudioEffectManager.AudioMusicParam param = new TXAudioEffectManager.AudioMusicParam(musicId, musicPath);param.publish = false;mTRTCCloud.getAudioEffectManager().startPlayMusic(param);}
说明:
合唱端接收到第一个 startChorus 信令后,状态应从“未合唱”转为“合唱中”,此后不再响应 startChorus 信令,避免重复启动 BGM 播放。
4. 歌曲播放进度修正
long mStartPlayMusicTs = "最初约定的合唱时间";long currentProgress = subCloud.getAudioEffectManager().getMusicCurrentPosInMS(musicID);// 当前歌曲的理想播放时间进度long estimatedProgress = getNtpTime() - mStartPlayMusicTs;// 当播放进度超过50ms,进行修正if (estimatedProgress >= 0 &&; Math.abs(currentProgress - estimatedProgress) > 50) {subCloud.getAudioEffectManager().seekMusicToPosInMS(mMusicID, (int) estimatedProgress);}