有奖捉虫:行业应用 & 管理与支持文档专题 HOT

业务流程

本节汇总了在线 K 歌中一些常见的业务流程,帮助您更好地理解整个场景的实现流程。
点歌流程
独唱流程
主唱流程
合唱流程
听众流程
下图展示了使用音速达直播音乐版权引擎点歌,并使用 TRTC SDK 播放的流程。
?
?
?
下图展示了排麦独唱玩法中,演唱者进房演唱及停止演唱并退房的流程。
?
?
?
下图展示了实时合唱玩法中,主唱者发起合唱及停止合唱并退房的流程。
?
?
下图展示了实时合唱玩法中,合唱者参与合唱及停止合唱并退房的流程。
?
?
?
下图展示了在线 K 歌场景中,听众进房听歌及歌词同步的流程。
?
?
?

接入准备

步骤一:开通服务

在线 K 歌场景通常需要依赖腾讯云 实时音视频 TRTC音速达直播音乐版权引擎 两项付费 PaaS 服务构建。其中 TRTC 负责提供实时音频互动能力,音速达负责提供正版音乐资源。
开通 TRTC 服务
开通音速达服务
1. 首先,您需要登录 实时音视频 TRTC 控制台 创建应用,您可根据需要选择升级 TRTC 应用版本,例如旗舰版可解锁更多增值功能服务。
?
?
?
说明:
建议创建两个应用分别用于测试环境和生产环境,首次开通 TRTC 服务可前往 试用中心 免费领取 10000 分钟试用时长包。
TRTC 包月套餐(入门版、基础版、尊享版、旗舰版)可以解锁不同的增值功能服务,详情可见 包月套餐说明
2. 应用创建完毕之后,您可以在应用管理-应用概览栏目看到该应用的基本信息,其中需要您保管好 SDKAppIDSDKSecretKey 便于后续使用,同时应避免密钥泄露造成流量盗刷。
?
?
?
1. 首先,您需要登录 音速达直播音乐版权引擎控制台,进入应用管理页面创建应用。初次可以创建一个测试应用以便接入测试,测试版 License 的有效期为30天,包含5000次测试播放次数。
?
?
?
2. 应用创建完毕之后,您可以在控制台-应用管理中看到您创建的正式/测试应用,其中需要您保管好 License KeyLicense Url 便于后续使用,同时应避免泄露造成盗刷。
?
?
?

步骤二:导入 SDK

导入 TRTC SDK
导入音速达 SDK
TRTC SDK 已经发布到 mavenCentral 库,您可以通过配置 gradle 自动下载更新。
1. 在 dependencies 中添加合适版本 SDK 的依赖。
// TRTC 精简版 SDK, 包含 TRTC 和直播播放两项功能, 体积小巧
dependencies {
implementation 'com.tencent.liteav:LiteAVSDK_TRTC:latest.release'
}
?
// TRTC 全功能版 SDK, 另含直播、短视频、点播等多项功能, 体积略大
dependencies {
implementation 'com.tencent.liteav:LiteAVSDK_Professional:latest.release'
}
说明:
自动加载(aar)的方案需要确保您在 repositories 中添加了 mavenCentral 仓库。
SDK 版本号推荐使用最新版本,如需使用稳定版本或指定版本号请 联系我们
除了推荐的自动加载方式,您还可以选择下载 SDK 并手动导入,详见 手动导入 TRTC SDK
2. 在 defaultConfig 中,指定 App 使用的 CPU 架构。
defaultConfig {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
说明:
TRTC SDK 支持 armeabi/armeabi-v7a/arm64-v8a 架构,Android 端另有支持 x86/x86_64 架构的 模拟器专用版 SDK
?
音速达 SDK 目前仅支持本地手动导入,您需要先在 SDK 下载 页面下载 SDK 并解压到本地,然后按照如下步骤手动导入到项目工程中:
1. 将解压的 aar 文件拷贝到工程的 app/libs 目录下。
2. 在工程根目录的 build.gradle 中添加 flatDir,指定本地仓库路径。
allprojects {
repositories {
flatDir {
dirs 'libs'
dirs project(':app').file('libs')
}
}
}
3. 在 app/build.gradle 中添加引用 aar 包的代码。
implementation (name: 'TXCopyrightedMedia-Android-trtc-proxy-ksong-ysd-xxx', ext: "aar")
4. 单击 Sync Now 即可完成音速达 SDK 的集成工作。

步骤三:工程配置

1. 权限配置
在 AndroidManifest.xml 中配置 App 权限,K 歌场景下 TRTC SDK 及音速达 SDK 需要以下权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
注意:
TRTC SDK 没有内置权限申请逻辑,需要您自行声明相应的权限和特性,部分权限(如存储、录音等)还需要在运行时动态申请。
若 Android 项目 targetSdkVersion 为 31 或者目标设备涉及到 Android 12 及更高系统版本,官方要求需要在代码中动态申请 android.permission.BLUETOOTH_CONNECT 权限,以正常使用蓝牙功能,具体信息请参见 Android 官方说明
2. 混淆配置
由于我们在 SDK 内部使用了 Java 的反射特性,需要您在 proguard-rules.pro 文件中将 SDK 相关类加入不混淆名单:
-keep class com.tencent.** { *; }

步骤四:鉴权与许可

TRTC 鉴权凭证
音速达鉴权许可
UserSig 是腾讯云设计的一种安全保护签名,目的是为了阻止恶意攻击者盗用您的云服务使用权,TRTC 在进房时校验该鉴权凭证。
调试跑通阶段:可以通过 客户端示例代码控制台 两种方法计算生成 UserSig,仅用于调试测试。
正式运行阶段:推荐安全等级更高的服务端计算 UserSig 方案,防止客户端被逆向破解泄露密钥。
具体实现流程如下:
1. 您的 App 在调用 SDK 的初始化函数之前,首先要向您的服务器请求 UserSig。
2. 您的服务器根据 SDKAppID 和 UserID 计算 UserSig。
3. 服务器将计算好的 UserSig 返回给您的 App。
4. 您的 App 将获得的 UserSig 通过特定 API 传递给 SDK。
5. SDK 将 SDKAppID + UserID + UserSig 提交给腾讯云服务器进行校验。
6. 腾讯云校验 UserSig,确认合法性。
7. 校验通过后,会向 TRTC SDK 提供实时音视频服务。
?
?
?
注意:
调试跑通阶段的本地 UserSig 计算方式不推荐应用到线上环境,容易被逆向破解导致密钥泄露。
我们提供了多个语言版本(Java/GO/PHP/Nodejs/Python/C#/C++)的 UserSig 服务端计算源代码,详见 UserSig 计算源码
?
使用音速达获取正版曲库资源之前,需要向腾讯云校验许可凭证。设置 License 需要用到 License Key 和 License Url,示例代码如下。
import com.tencent.txcopyrightedmedia.TXCopyrightedMedia;
?
TXCopyrightedMedia.instance().setLicense(context, licenceUrl, licenceKey);
注意:
建议在相关业务模块的初始化代码中触发鉴权许可,避免在使用前才临时去下载 License,同时鉴权时应具有网络权限。
实际应用的包名必须和创建音速达应用时绑定的 Package Name 完全匹配,否则会导致 License 校验失败。
?

步骤五:初始化 SDK

初始化 TRTC SDK
初始化音速达 SDK
// 创建 TRTC SDK 实例(单例模式)
TRTCCloud mTRTCCloud = TRTCCloud.sharedInstance(context);
// 设置事件监听器
mTRTCCloud.setListener(trtcSdkListener);
?
// 来自 SDK 的各类事件通知(比如:错误码,警告码,音视频状态参数等)
private TRTCCloudListener trtcSdkListener = new TRTCCloudListener() {
@Override
public void onError(int errCode, String errMsg, Bundle extraInfo) {
Log.d(TAG, errCode + errMsg);
}
@Override
public void onWarning(int warningCode, String warningMsg, Bundle extraInfo) {
Log.d(TAG, warningCode + warningMsg);
}
};
?
// 移除事件监听器
mTRTCCloud.setListener(null);
// 销毁 TRTC SDK 实例(单例模式)
TRTCCloud.destroySharedInstance();
说明:
建议监听 SDK 事件通知,对一些常见错误进行日志打印和处理,详见 错误码表
?
// 获取 TXCopyrightedMedia 单例
TXCopyrightedMedia copyrightedMedia = TXCopyrightedMedia.instance();
?
// 初始化 TXCopyrightedMedia
copyrightedMedia.init();
?
// 销毁 TXCopyrightedMedia
copyrightedMedia.destroy();

场景一:排麦独唱

视角一:演唱者动作

时序图

?
?
?
1. 进入房间
public void enterRoom(String roomId, String userId) {
TRTCCloudDef.TRTCParams params = new TRTCCloudDef.TRTCParams();
// 以字符串房间号为例
params.strRoomId = roomId;
params.userId = userId;
// 从业务后台获取到的 UserSig
params.userSig = getUserSig(userId);
// 替换成您的 SDKAppID
params.sdkAppId = SDKAppID;
// 建议均以观众角色进房
params.role = TRTCCloudDef.TRTCRoleAudience;
// 进房场景须选择 LIVE
mTRTCCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
}
注意:
为了更好地透传 SEI 消息用于歌词同步,建议进房场景选用 TRTC_APP_SCENE_LIVE
// 进房结果事件回调
@Override
public void onEnterRoom(long result) {
if (result > 0) {
// result 代表加入房间所消耗的时间(毫秒)
Log.d(TAG, "Enter room succeed");
// 开启补黑帧的实验性接口
mTRTCCloud.callExperimentalAPI("{\\"api\\":\\"enableBlackStream\\",\\"params\\": {\\"enable\\":true}}");
} else {
// result 代表进房失败的错误码
Log.d(TAG, "Enter room failed");
}
}
注意:
纯音频模式下演唱者需要开启补黑帧以携带 SEI 消息,该接口需要在进房成功之后调用。
2. 上麦推流
// 切换为主播角色
mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAnchor);
?
// 切换角色事件回调
@Override
public void onSwitchRole(int errCode, String errMsg) {
if (errCode == TXLiteAVCode.ERR_NULL) {
// 设置媒体音量类型
mTRTCCloud.setSystemVolumeType(TRTCCloudDef.TRTCSystemVolumeTypeMedia);
// 上行本地音频流,设置音质
mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_MUSIC);
}
}
注意:
K 歌场景下建议设置全程媒体音量、Music 音质,以获得高保真听感体验。
3. 点歌演唱
搜索歌曲,获取音乐资源
首先,调用 REST API SearchKTVMusics 传入关键词,搜索相匹配的歌曲列表并记录 MusicId。
然后,调用 REST API BatchDescribeKTVMusicDetails 传入目标歌曲的 MusicId,获取 PlayToken 和 LyricsUrl。
最后,调用音速达 SDK API 获取 MusicUri 交由 TRTC 播放,示例代码如下。
// 最终交由 TRTC 播放的原唱 URI
String originMusicUri = "";
// 最终交由 TRTC 播放的伴奏 URI
String accompMusicUri = "";
?
// 指定下载完整歌曲,可选高潮片段 CLIP_CHORUS
String extParams = new TXCMMusicExtParams().setClipType(TXCopyrightedMedia.CLIP_FULL).toString();
// 判断歌曲是否完成预加载
if (copyrightedMedia.isMusicPreloaded(musicId, extParams)) {
// 创建音乐轨道类,获取 MusicUri
createMusicTrack(musicId);
} else {
// 预加载 Music 数据
preloadMusic(musicId, playToken);
}
?
public void createMusicTrack(String musicId) {
// 创建音乐轨道类
TXCMMusicInfo musicInfo = new TXCMMusicInfo(musicId, extParams, TXCopyrightedMedia.TYPE_ORIGIN);
musicTrack = copyrightedMedia.createMusicTrack(musicInfo);
// 设置 prepare 处理回调
musicTrack.setOnPreparedListener(new ITXCMMusicTrack.OnPreparedListener() {
@Override
public void onPrepared() {
// 开始音频解码
musicTrack.start();
// bgmType: 0 原唱;1 伴奏
originMusicUri = TXCopyrightedMedia.genMusicURI(musicId, 0, extParams);
accompMusicUri = TXCopyrightedMedia.genMusicURI(musicId, 1, extParams);
}
});
// 设置错误事件回调
musicTrack.setOnErrorListener(new ITXCMMusicTrack.OnErrorListener() {
@Override
public void onError(int errCode, String msg) {
Log.d(TAG, "error: " + code + ", msg: " + msg);
}
});
// 准备音频数据,异步回调 onPrepared
musicTrack.prepare();
}
?
public void preloadMusic(Sting musicId, String playToken) {
ITXMusicPreloadCallback callback = new ITXMusicPreloadCallback() {
@Override
public void onPreloadStart(String musicId, String musicExtParams) {
// 界面提示音乐开始加载
}
?
@Override
public void onPreloadProgress(String musicId, String musicExtParams, float progress) {
// 界面显示加载进度
}
?
@Override
public void onPreloadComplete(String musicId, String musicExtParams, int errorCode, String msg) {
if (errorCode == ErrorCode.ERR_NONE) {
// 预加载完成,创建音乐轨道类,获取 MusicUri
createMusicTrack(musicId);
} else {
Log.d(TAG, "error: " + errorCode + ", msg: " + errMsg);
}
}
};
copyrightedMedia.preloadMusic(musicId, extParams, playToken, callback);
}
播放伴奏,开始演唱
// 获取音频特效管理类
TXAudioEffectManager mTXAudioEffectManager = mTRTCCloud.getAudioEffectManager();
?
// originMusicId: 自定义原唱标识;originMusicUri: 音速达获取原唱资源
TXAudioEffectManager.AudioMusicParam originMusicParam = new TXAudioEffectManager.AudioMusicParam(originMusicId, originMusicUri);
// 是否将原唱发布到远端(否则仅本地播放)
originMusicParam.publish = true;
?
// accompMusicId: 自定义伴奏标识;accompMusicUri: 音速达获取伴奏资源
TXAudioEffectManager.AudioMusicParam accompMusicParam = new TXAudioEffectManager.AudioMusicParam(accompMusicId, accompMusicUri);
// 是否将伴奏发布到远端(否则仅本地播放)
accompMusicParam.publish = true;
?
// 开始播放原唱音乐
mTXAudioEffectManager.startPlayMusic(originMusicParam);
// 开始播放伴奏音乐
mTXAudioEffectManager.startPlayMusic(accompMusicParam);
?
// 切换至原唱音乐
mTXAudioEffectManager.setMusicPlayoutVolume(originMusicId, 100);
mTXAudioEffectManager.setMusicPlayoutVolume(accompMusicId, 0);
mTXAudioEffectManager.setMusicPublishVolume(originMusicId, 100);
mTXAudioEffectManager.setMusicPublishVolume(accompMusicId, 0);
?
// 切换至伴奏音乐
mTXAudioEffectManager.setMusicPlayoutVolume(originMusicId, 0);
mTXAudioEffectManager.setMusicPlayoutVolume(accompMusicId, 100);
mTXAudioEffectManager.setMusicPublishVolume(originMusicId, 0);
mTXAudioEffectManager.setMusicPublishVolume(accompMusicId, 100);
注意:
K 歌场景下需要同时播放原唱和伴奏(使用 MusicID 区分),通过调整本地和远端播放音量来实现原唱和伴奏的切换。
如果播放的是双音轨(包含原唱和伴奏)音乐,可通过 setMusicTrack 指定音乐的播放音轨来实现原唱和伴奏的切换。
4. 歌词同步
下载歌词
通过批量获取歌曲详情,即调用 REST API BatchDescribeKTVMusicDetails,获取歌词下载链接 LyricsUrl,将歌词缓存到本地。
本地歌词同步,以及 SEI 传递歌曲进度
mTXAudioEffectManager.setMusicObserver(musicId, new TXAudioEffectManager.TXMusicPlayObserver() {
@Override
public void onStart(int id, int errCode) {
// 音乐开始播放
}
@Override
public void onPlayProgress(int id, long curPtsMs, long durationMs) {
// 根据最新进度和本地歌词进度误差,判断是否需要 seek
// 通过发送 SEI 消息传递歌曲进度
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("musicId", id);
jsonObject.put("progress", curPtsMs);
jsonObject.put("duration", durationMs);
mTRTCCloud.sendSEIMsg(jsonObject.toString().getBytes(), 1);
} catch (JSONException e) {
e.printStackTrace();
}
}
@Override
public void onComplete(int id, int errCode) {
// 音乐播放完成
}
});
注意:
请在播放背景音乐之前使用该接口设置播放事件回调,以便感知背景音乐的播放进度。
演唱者发送 SEI 消息频率由事件回调频率决定,这里也可通过 getMusicCurrentPosInMS 主动获取播放进度定时同步。
5. 下麦退房
// 切换为观众角色
mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAudience);
?
// 切换角色事件回调
@Override
public void onSwitchRole(int errCode, String errMsg) {
if (errCode == TXLiteAVCode.ERR_NULL) {
// 停止播放伴奏音乐
mTRTCCloud.getAudioEffectManager().stopPlayMusic(musicId);
// 停止本地音频的采集和发布
mTRTCCloud.stopLocalAudio();
}
}
?
// 退出房间
mTRTCCloud.exitRoom();
?
// 退出房间事件回调
@Override
public void onExitRoom(int reason) {
if (reason == 0) {
Log.d(TAG, "主动调用 exitRoom 退出房间");
} else if (reason == 1) {
Log.d(TAG, "被服务器踢出当前房间");
} else if (reason == 2) {
Log.d(TAG, "当前房间整个被解散");
}
}
注意:
待 SDK 占用的所有资源释放完毕后,SDK 会抛出 onExitRoom 回调通知到您。
如果您要再次调用 enterRoom 或切换到其他音视频 SDK,请等待 onExitRoom 回调到来后再执行相关操作。否则可能会遇到例如摄像头、麦克风设备被强占等各种异常问题。

视角二:听众动作

时序图

?
?
?
1. 进入房间
public void enterRoom(String roomId, String userId) {
TRTCCloudDef.TRTCParams params = new TRTCCloudDef.TRTCParams();
// 以字符串房间号为例
params.strRoomId = roomId;
params.userId = userId;
// 从业务后台获取到的 UserSig
params.userSig = getUserSig(userId);
// 替换成您的 SDKAppID
params.sdkAppId = SDKAppID;
// 建议均以观众角色进房
params.role = TRTCCloudDef.TRTCRoleAudience;
// 进房场景须选择 LIVE
mTRTCCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
}
?
// 进房结果事件回调
@Override
public void onEnterRoom(long result) {
if (result > 0) {
// result 代表加入房间所消耗的时间(毫秒)
Log.d(TAG, "Enter room succeed");
} else {
// result 代表进房失败的错误码
Log.d(TAG, "Enter room failed");
}
}
注意:
为了更好地透传 SEI 消息用于歌词同步,建议进房场景选用 TRTC_APP_SCENE_LIVE
自动订阅模式下(默认)观众进房会自动订阅并播放麦上主播音频流。
2. 歌词同步
下载歌词
通过批量获取歌曲详情,即调用 REST API BatchDescribeKTVMusicDetails,获取歌词下载链接 LyricsUrl,将歌词缓存到本地。
听众端歌词同步
@Override
public void onUserVideoAvailable(String userId, boolean available) {
if (available) {
mTRTCCloud.startRemoteView(userId, null);
} else {
mTRTCCloud.stopRemoteView(userId);
}
}
?
@Override
public void onRecvSEIMsg(String userId, byte[] data) {
String result = new String(data);
try {
JSONObject jsonObject = new JSONObject(result);
int musicId = jsonObject.getInt("musicId");
long progress = jsonObject.getLong("progress");
long duration = jsonObject.getLong("duration");
} catch (JSONException e) {
e.printStackTrace();
}
...
// TODO 更新歌词控件逻辑:
// 根据接收到的最新进度和本地歌词进度误差,判断是否需要 seek 歌词控件
...
}
注意:
听众需要主动订阅演唱者的视频流,以便接收黑帧携带的 SEI 消息。
3. 退出房间
// 退出房间
mTRTCCloud.exitRoom();
?
// 退出房间事件回调
@Override
public void onExitRoom(int reason) {
if (reason == 0) {
Log.d(TAG, "主动调用 exitRoom 退出房间");
} else if (reason == 1) {
Log.d(TAG, "被服务器踢出当前房间");
} else if (reason == 2) {
Log.d(TAG, "当前房间整个被解散");
}
}

场景二:实时合唱

视角一:主唱动作

时序图

?
?
?
1. 双实例进房
// 创建 TRTCCloud 主实例(人声实例)
TRTCCloud mTRTCCloud = TRTCCloud.sharedInstance(context);
// 创建 TRTCCloud 子实例(音乐实例)
TRTCCloud subCloud = mTRTCCloud.createSubCloud();
?
// 主实例(人声实例)进房
TRTCCloudDef.TRTCParams params = new TRTCCloudDef.TRTCParams();
params.sdkAppId = SDKAppId;
params.userId = UserId;
params.userSig = UserSig;
params.role = TRTCCloudDef.TRTCRoleAnchor;
params.strRoomId = RoomId;
mTRTCCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
?
// 子实例开启手动订阅模式,默认不订阅远端流
subCloud.setDefaultStreamRecvMode(false, false);
?
// 子实例(音乐实例)进房
TRTCCloudDef.TRTCParams bgmParams = new TRTCCloudDef.TRTCParams();
bgmParams.sdkAppId = SDKAppId;
// 子实例用户名不能与房间内其他用户重复
bgmParams.userId = UserId + "_bgm";
bgmParams.userSig = UserSig;
bgmParams.role = TRTCCloudDef.TRTCRoleAnchor;
bgmParams.strRoomId = RoomId;
subCloud.enterRoom(bgmParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
注意:
实时合唱方案中,主唱端需要分别创建主实例和子实例,分别用于上行人声及伴奏音乐。
子实例无需订阅房间内其他用户音频流,因此建议开启手动订阅模式,须在进房前开启。
2. 进房后设置
// 主实例进房结果事件回调
@Override
public void onEnterRoom(long result) {
if (result > 0) {
// 主实例取消订阅子实例发布的音乐流
mTRTCCloud.muteRemoteAudio(UserId + "_bgm", true);
// 主实例开启补黑帧的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"enableBlackStream\\",\\"params\\": {\\"enable\\":true}}");
// 主实例开启合唱模式的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"enableChorus\\",\\"params\\":{\\"enable\\":true,\\"audioSource\\":0}}");
// 主实例开启低延时模式的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"setLowLatencyModeEnabled\\",\\"params\\":{\\"enable\\":true}}");
// 主实例启用音量大小回调
mTRTCCloud.enableAudioVolumeEvaluation(300, false);
// 主实例设置全程媒体音量类型
mTRTCCloud.setSystemVolumeType(TRTCCloudDef.TRTCSystemVolumeTypeMedia);
// 主实例采集和发布本地音频,同时设置音质
mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_MUSIC);
} else {
// result 代表进房失败的错误码
Log.d(TAG, "Enter room failed");
}
}
?
// 子实例进房结果事件回调
@Override
public void onEnterRoom(long result) {
if (result > 0) {
// 子实例开启合唱模式的实验性接口
subCloud.callExperimentalAPI(
"{\\"api\\":\\"enableChorus\\",\\"params\\":{\\"enable\\":true,\\"audioSource\\":1}}");
// 子实例开启低延时模式的实验性接口
subCloud.callExperimentalAPI(
"{\\"api\\":\\"setLowLatencyModeEnabled\\",\\"params\\":{\\"enable\\":true}}");
// 子实例设置全程媒体音量类型
subCloud.setSystemVolumeType(TRTCCloudDef.TRTCSystemVolumeTypeMedia);
// 子实例设置音质
subCloud.setAudioQuality(TRTCCloudDef.TRTC_AUDIO_QUALITY_MUSIC);
} else {
// result 代表进房失败的错误码
Log.d(TAG, "Enter room failed");
}
}
注意:
主实例和子实例均需使用实验性接口开启合唱模式和低延时模式以优化合唱体验,需注意 audioSource 参数的不同。
3. 混流回推房间
private void startPublishMediaToRoom(String roomId, String userId) {
// 创建 TRTCPublishTarget 对象
TRTCCloudDef.TRTCPublishTarget target = new TRTCCloudDef.TRTCPublishTarget();
// 混流后回推到房间
target.mode = TRTCCloudDef.TRTC_PublishMixStream_ToRoom;
target.mixStreamIdentity.strRoomId = roomId;
// 混流机器人用户名不能与房间内其他用户重复
target.mixStreamIdentity.userId = userId + "_robot";
?
// 设置转码后的音频流的编码参数(可自定义)
TRTCCloudDef.TRTCStreamEncoderParam trtcStreamEncoderParam = new TRTCCloudDef.TRTCStreamEncoderParam();
trtcStreamEncoderParam.audioEncodedChannelNum = 2;
trtcStreamEncoderParam.audioEncodedKbps = 64;
trtcStreamEncoderParam.audioEncodedCodecType = 2;
trtcStreamEncoderParam.audioEncodedSampleRate = 48000;
?
// 设置转码后的视频流的编码参数(混入黑帧必填)
trtcStreamEncoderParam.videoEncodedFPS = 15;
trtcStreamEncoderParam.videoEncodedGOP = 3;
trtcStreamEncoderParam.videoEncodedKbps = 30;
trtcStreamEncoderParam.videoEncodedWidth = 64;
trtcStreamEncoderParam.videoEncodedHeight = 64;
?
// 设置音频混流参数
TRTCCloudDef.TRTCStreamMixingConfig trtcStreamMixingConfig = new TRTCCloudDef.TRTCStreamMixingConfig();
// 默认情况下填空值即可,代表会混合房间中的所有音频
trtcStreamMixingConfig.audioMixUserList = null;
?
// 配置视频混流模板(混入黑帧必填)
TRTCCloudDef.TRTCVideoLayout videoLayout = new TRTCCloudDef.TRTCVideoLayout();
trtcStreamMixingConfig.videoLayoutList.add(videoLayout);
?
// 开始混流回推
mTRTCCloud.startPublishMediaStream(target, trtcStreamEncoderParam, trtcStreamMixingConfig);
}
注意:
为了保持合唱人声和伴奏音乐的对齐,建议开启混流回推房间,麦上合唱者互相订阅单流,麦下听众默认只订阅混流。
混流机器人作为一个独立用户进房拉流、混流及转推,其用户名不能与房间内其他用户名重复,否则会引起互踢。
4. 搜索与点歌
首先,调用 REST API SearchKTVMusics 传入关键词,搜索相匹配的歌曲列表并记录 MusicId。
然后,调用 REST API BatchDescribeKTVMusicDetails 传入目标歌曲的 MusicId,获取 PlayToken 和 LyricsUrl。
最后,调用音速达 SDK API 获取 MusicUri 交由 TRTC 播放,示例代码如下。
// 最终交由 TRTC 播放的原唱 URI
String originMusicUri = "";
// 最终交由 TRTC 播放的伴奏 URI
String accompMusicUri = "";
?
// 指定下载完整歌曲,可选高潮片段 CLIP_CHORUS
String extParams = new TXCMMusicExtParams().setClipType(TXCopyrightedMedia.CLIP_FULL).();
// 判断歌曲是否完成预加载
if (copyrightedMedia.isMusicPreloaded(musicId, extParams)) {
// 创建音乐轨道类,获取 MusicUri
createMusicTrack(musicId);
} else {
// 预加载 Music 数据
preloadMusic(musicId, playToken);
}
?
public void createMusicTrack(String musicId) {
// 创建音乐轨道类
TXCMMusicInfo musicInfo = new TXCMMusicInfo(musicId, extParams, TXCopyrightedMedia.TYPE_ORIGIN);
musicTrack = copyrightedMedia.createMusicTrack(musicInfo);
// 设置 prepare 处理回调
musicTrack.setOnPreparedListener(new ITXCMMusicTrack.OnPreparedListener() {
@Override
public void onPrepared() {
// 开始音频解码
musicTrack.start();
// bgmType: 0 原唱;1 伴奏
originMusicUri = TXCopyrightedMedia.genMusicURI(musicId, 0, extParams);
accompMusicUri = TXCopyrightedMedia.genMusicURI(musicId, 1, extParams);
}
});
// 设置错误事件回调
musicTrack.setOnErrorListener(new ITXCMMusicTrack.OnErrorListener() {
@Override
public void onError(int errCode, String msg) {
Log.d(TAG, "error: " + code + ", msg: " + msg);
}
});
// 准备音频数据,异步回调 onPrepared
musicTrack.prepare();
}
?
public void preloadMusic(Sting musicId, String playToken) {
ITXMusicPreloadCallback callback = new ITXMusicPreloadCallback() {
@Override
public void onPreloadStart(String musicId, String musicExtParams) {
// 界面提示音乐开始加载
}
?
@Override
public void onPreloadProgress(String musicId, String musicExtParams, float progress) {
// 界面显示加载进度
}
?
@Override
public void onPreloadComplete(String musicId, String musicExtParams, int errorCode, String msg) {
if (errorCode == ErrorCode.ERR_NONE) {
// 预加载完成,创建音乐轨道类,获取 MusicUri
createMusicTrack(musicId);
} else {
Log.d(TAG, "error: " + errorCode + ", msg: " + errMsg);
}
}
};
copyrightedMedia.preloadMusic(musicId, extParams, playToken, callback);
}
5. NTP 校时
TXLiveBase.setListener(new TXLiveBaseListener() {
@Override
public void onUpdateNetworkTime(int errCode, String errMsg) {
super.onUpdateNetworkTime(errCode, errMsg);
// errCode 0: 校时成功且偏差在30ms以内;1: 校时成功但偏差可能在30ms以上;-1: 校时失败
if (errCode == 0) {
// 校时成功,获取 NTP 时间戳
long ntpTime = TXLiveBase.getNetworkTimestamp();
} else {
// 校时失败,可尝试重新校时
TXLiveBase.updateNetworkTime();
}
}
});
?
TXLiveBase.updateNetworkTime();
注意:
NTP 校时结果能够反应用户当前的网络质量,为了保证良好的合唱体验,建议在校时失败时不允许用户发起合唱。
6. 发送合唱信令
Timer mTimer = new Timer();
mTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("cmd", "startChorus");
// 约定合唱开始时间: 当前 NTP 时间 + 延迟播放时间(例如3秒)
jsonObject.put("startPlayMusicTS", TXLiveBase.getNetworkTimestamp() + 3000);
jsonObject.put("musicId", musicId);
jsonObject.put("musicDuration", subCloud.getAudioEffectManager().getMusicDurationInMS(originMusicUri));
mTRTCCloud.sendCustomCmdMsg(1, jsonObject.toString().getBytes(), false, false);
} catch (JSONException e) {
e.printStackTrace();
}
}
}, 0, 1000);
注意:
主唱需要按固定时间频率(例如1秒)循环向房间内广播合唱信令,以便新进房用户也可中途加入合唱。
7. 加载播放伴奏
// 获取音频特效管理类
TXAudioEffectManager mTXAudioEffectManager = subCloud.getAudioEffectManager();
?
// originMusicId: 自定义原唱标识;originMusicUri: 音速达获取原唱资源
TXAudioEffectManager.AudioMusicParam originMusicParam = new TXAudioEffectManager.AudioMusicParam(originMusicId, originMusicUri);
// 将原唱音乐发布到远端
originMusicParam.publish = true;
// 音乐开始播放的时间点(毫秒)
originMusicParam.startTimeMS = 0;
?
// accompMusicId: 自定义伴奏标识;accompMusicUri: 音速达获取伴奏资源
TXAudioEffectManager.AudioMusicParam accompMusicParam = new TXAudioEffectManager.AudioMusicParam(accompMusicId, accompMusicUri);
// 将伴奏音乐发布到远端
accompMusicParam.publish = true;
// 音乐开始播放的时间点(毫秒)
accompMusicParam.startTimeMS = 0;
?
// 预加载原唱音乐
mTXAudioEffectManager.preloadMusic(originMusicParam);
// 预加载伴奏音乐
mTXAudioEffectManager.preloadMusic(accompMusicParam);
?
// 延迟播放时间(例如3秒)后开始播放原唱音乐
mTXAudioEffectManager.startPlayMusic(originMusicParam);
// 延迟播放时间(例如3秒)后开始播放伴奏音乐
mTXAudioEffectManager.startPlayMusic(accompMusicParam);
?
// 切换至原唱音乐
mTXAudioEffectManager.setMusicPlayoutVolume(originMusicId, 100);
mTXAudioEffectManager.setMusicPlayoutVolume(accompMusicId, 0);
mTXAudioEffectManager.setMusicPublishVolume(originMusicId, 100);
mTXAudioEffectManager.setMusicPublishVolume(accompMusicId, 0);
?
// 切换至伴奏音乐
mTXAudioEffectManager.setMusicPlayoutVolume(originMusicId, 0);
mTXAudioEffectManager.setMusicPlayoutVolume(accompMusicId, 100);
mTXAudioEffectManager.setMusicPublishVolume(originMusicId, 0);
mTXAudioEffectManager.setMusicPublishVolume(accompMusicId, 100);
注意:
建议开始播放音乐前先进行预加载,提前将音乐资源载入到内存当中,可有效降低音乐播放的加载延迟。
K 歌场景下需要同时播放原唱和伴奏(使用 MusicID 区分),通过调整本地和远端播放音量来实现原唱和伴奏的切换。
如果播放的是双音轨(包含原唱和伴奏)音乐,可通过 setMusicTrack 指定音乐的播放音轨来实现原唱和伴奏的切换。
8. 伴奏同步
// 约定的合唱开始时间
long mStartPlayMusicTs = jsonObject.getLong("startPlayMusicTS");
// 当前伴奏音乐的实际播放进度
long currentProgress = subCloud.getAudioEffectManager().getMusicCurrentPosInMS(musicId);
// 当前伴奏音乐的理想播放进度
long estimatedProgress = TXLiveBase.getNetworkTimestamp() - mStartPlayMusicTs;
// 当进度差超过50ms,进行修正
if (estimatedProgress >= 0 && Math.abs(currentProgress - estimatedProgress) > 50) {
subCloud.getAudioEffectManager().seekMusicToPosInMS(musicId, (int) estimatedProgress);
}
9. 歌词同步
下载歌词
通过批量获取歌曲详情,即调用 REST API BatchDescribeKTVMusicDetails,获取歌词下载链接 LyricsUrl,将歌词缓存到本地。
本地歌词同步,以及 SEI 传递歌曲进度
mTXAudioEffectManager.setMusicObserver(musicId, new TXAudioEffectManager.TXMusicPlayObserver() {
@Override
public void onStart(int id, int errCode) {
// 音乐开始播放
}
@Override
public void onPlayProgress(int id, long curPtsMs, long durationMs) {
// 根据最新进度和本地歌词进度误差,判断是否需要 seek
// 通过发送 SEI 消息传递歌曲进度
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("musicId", id);
jsonObject.put("progress", curPtsMs);
jsonObject.put("duration", durationMs);
mTRTCCloud.sendSEIMsg(jsonObject.toString().getBytes(), 1);
} catch (JSONException e) {
e.printStackTrace();
}
}
@Override
public void onComplete(int id, int errCode) {
// 音乐播放完成
}
});
注意:
请在播放背景音乐之前使用该接口设置播放事件回调,以便感知背景音乐的播放进度。
演唱者发送 SEI 消息频率由事件回调频率决定,这里也可通过 getMusicCurrentPosInMS 主动获取播放进度定时同步。
10. 下麦退房
// 子实例关闭合唱模式的实验性接口
subCloud.callExperimentalAPI(
"{\\"api\\":\\"enableChorus\\",\\"params\\":{\\"enable\\":false,\\"audioSource\\":1}}");
// 子实例关闭低延时模式的实验性接口
subCloud.callExperimentalAPI(
"{\\"api\\":\\"setLowLatencyModeEnabled\\",\\"params\\":{\\"enable\\":false}}");
// 子实例切换为观众角色
subCloud.switchRole(TRTCCloudDef.TRTCRoleAudience);
// 子实例停止播放伴奏音乐
subCloud.getAudioEffectManager().stopPlayMusic(musicId);
// 子实例退出房间
subCloud.exitRoom();
?
// 主实例关闭补黑帧的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"enableBlackStream\\",\\"params\\": {\\"enable\\":false}}");
// 主实例关闭合唱模式的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"enableChorus\\",\\"params\\":{\\"enable\\":false,\\"audioSource\\":0}}");
// 主实例关闭低延时模式的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"setLowLatencyModeEnabled\\",\\"params\\":{\\"enable\\":false}}");
// 主实例切换为观众角色
mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAudience);
// 主实例停止本地音频采集和发布
mTRTCCloud.stopLocalAudio();
// 主实例退出房间
mTRTCCloud.exitRoom();

视角二:合唱动作

时序图

?
?
?
1. 进入房间
public void enterRoom(String roomId, String userId) {
TRTCCloudDef.TRTCParams params = new TRTCCloudDef.TRTCParams();
// 以字符串房间号为例
params.strRoomId = roomId;
params.userId = userId;
// 从业务后台获取到的 UserSig
params.userSig = getUserSig(userId);
// 替换成您的 SDKAppID
params.sdkAppId = SDKAppID;
// 示例以观众角色进房
params.role = TRTCCloudDef.TRTCRoleAudience;
// 进房场景须选择 LIVE
mTRTCCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
}
?
// 进房结果事件回调
@Override
public void onEnterRoom(long result) {
if (result > 0) {
// result 代表加入房间所消耗的时间(毫秒)
Log.d(TAG, "Enter room succeed");
} else {
// result 代表进房失败的错误码
Log.d(TAG, "Enter room failed");
}
}
2. 上麦推流
// 切换为主播角色
mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAnchor);
?
// 切换角色事件回调
@Override
public void onSwitchRole(int errCode, String errMsg) {
if (errCode == TXLiteAVCode.ERR_NULL) {
// 取消订阅主唱子实例发布的音乐流
mTRTCCloud.muteRemoteAudio(mBgmUserId, true);
// 开启合唱模式的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"enableChorus\\",\\"params\\":{\\"enable\\":true,\\"audioSource\\":0}}");
// 开启低延时模式的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"setLowLatencyModeEnabled\\",\\"params\\":{\\"enable\\":true}}");
// 设置媒体音量类型
mTRTCCloud.setSystemVolumeType(TRTCCloudDef.TRTCSystemVolumeTypeMedia);
// 上行本地音频流,设置音质
mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_MUSIC);
}
}
注意:
为了尽可能降低延迟,合唱者均本地播放伴奏音乐,因此需要取消订阅主唱发布的音乐流。
合唱者也需要使用实验性接口开启合唱模式和低延时模式,以优化合唱体验。
K 歌场景下建议设置全程媒体音量、Music 音质,以获得高保真听感体验。
3. NTP 校时
TXLiveBase.setListener(new TXLiveBaseListener() {
@Override
public void onUpdateNetworkTime(int errCode, String errMsg) {
super.onUpdateNetworkTime(errCode, errMsg);
// errCode 0: 校时成功且偏差在30ms以内;1: 校时成功但偏差可能在30ms以上;-1: 校时失败
if (errCode == 0) {
// 校时成功,获取 NTP 时间戳
long ntpTime = TXLiveBase.getNetworkTimestamp();
} else {
// 校时失败,可尝试重新校时
TXLiveBase.updateNetworkTime();
}
}
});
?
TXLiveBase.updateNetworkTime();
注意:
NTP 校时结果能够反应用户当前的网络质量,为了保证良好的合唱体验,建议在校时失败时不允许用户参与合唱。
4. 接收合唱信令
@Override
public void onRecvCustomCmdMsg(String userId, int cmdID, int seq, byte[] message) {
try {
JSONObject json = new JSONObject(new String(message, "UTF-8"));
// 匹配合唱信令
if (json.getString("cmd").equals("startChorus")) {
long startPlayMusicTs = json.getLong("startPlayMusicTS");
int musicId = json.getInt("musicId");
long musicDuration = json.getLong("musicDuration");
// 约定合唱时间和当前时间差值
long delayMs = startPlayMusicTs - TXLiveBase.getNetworkTimestamp();
}
} catch (JSONException e) {
e.printStackTrace();
}
}
注意:
合唱者接收到合唱信令并参与合唱后,状态应转为“合唱中”,本轮合唱结束前不再重复响应合唱信令。
5. 播放伴奏,开始合唱
if (delayMs > 0) { // 合唱未开始
// 开始预加载音乐
preloadMusic(musicId, 0L);
// 延迟 delayMs 后开始播放音乐
startPlayMusic(musicId, 0L);
} else if (Math.abs(delayMs) < musicDuration) { // 合唱进行中
// 开始播放时间: 时间差值绝对值 + 预加载延迟(例如400ms)
long startTimeMS = Math.abs(delayMs) + 400;
// 开始预加载音乐
preloadMusic(musicId, startTimeMS);
// 预加载延迟(例如400ms)后开始播放音乐
startPlayMusic(musicId, startTimeMS);
} else { // 合唱已结束
// 不允许加入合唱
}
?
// 预加载音乐
public void preloadMusic(int musicId, long startTimeMS) {
// musicId: 从合唱信令获取;musicUrl: 对应的音乐资源地址
TXAudioEffectManager.AudioMusicParam musicParam = new
TXAudioEffectManager.AudioMusicParam(musicId, musicUrl);
// 仅本地播放音乐
musicParam.publish = false;
// 音乐开始播放的时间点(毫秒)
musicParam.startTimeMS = startTimeMS;
mTRTCCloud.getAudioEffectManager().preloadMusic(musicParam);
}
?
// 开始播放音乐
public void startPlayMusic(int musicId, long startTimeMS) {
// musicId: 从合唱信令获取;musicUrl: 对应的音乐资源地址
TXAudioEffectManager.AudioMusicParam musicParam = new
TXAudioEffectManager.AudioMusicParam(musicId, musicUrl);
// 仅本地播放音乐
musicParam.publish = false;
// 音乐开始播放的时间点(毫秒)
musicParam.startTimeMS = startTimeMS;
mTRTCCloud.getAudioEffectManager().startPlayMusic(musicParam);
}
注意:
为了尽可能降低传输延迟,合唱者跟随本地播放的伴奏音乐演唱,无需发布和接收远端音乐。
根据 delayMs 可判断当前合唱状态,不同状态下的 startPlayMusic 延迟调用需要开发者自行实现。
6. 伴奏同步
// 约定的合唱开始时间
long mStartPlayMusicTs = jsonObject.getLong("startPlayMusicTS");
// 当前伴奏音乐的实际播放进度
long currentProgress = mTRTCCloud.getAudioEffectManager().getMusicCurrentPosInMS(musicId);
// 当前伴奏音乐的理想播放进度
long estimatedProgress = TXLiveBase.getNetworkTimestamp() - mStartPlayMusicTs;
// 当进度差超过50ms,进行修正
if (estimatedProgress >= 0 && Math.abs(currentProgress - estimatedProgress) > 50) {
mTRTCCloud.getAudioEffectManager().seekMusicToPosInMS(musicId, (int) estimatedProgress);
}
7. 歌词同步
下载歌词
通过批量获取歌曲详情,即调用 REST API BatchDescribeKTVMusicDetails,获取歌词下载链接 LyricsUrl,将歌词缓存到本地。
本地歌词同步
mTXAudioEffectManager.setMusicObserver(musicId, new TXAudioEffectManager.TXMusicPlayObserver() {
@Override
public void onStart(int id, int errCode) {
// 音乐开始播放
}
@Override
public void onPlayProgress(int id, long curPtsMs, long durationMs) {
// TODO 更新歌词控件逻辑:
// 根据最新进度和本地歌词进度误差,判断是否需要 seek 歌词控件
?
}
@Override
public void onComplete(int id, int errCode) {
// 音乐播放完成
}
});
注意:
请在播放背景音乐之前使用该接口设置播放事件回调,以便感知背景音乐的播放进度。
8. 下麦退房
// 关闭合唱模式的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"enableChorus\\",\\"params\\":{\\"enable\\":false,\\"audioSource\\":0}}");
// 关闭低延时模式的实验性接口
mTRTCCloud.callExperimentalAPI(
"{\\"api\\":\\"setLowLatencyModeEnabled\\",\\"params\\":{\\"enable\\":false}}");
// 切换为观众角色
mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAudience);
// 停止播放伴奏音乐
mTRTCCloud.getAudioEffectManager().stopPlayMusic(musicId);
// 停止本地音频采集和发布
mTRTCCloud.stopLocalAudio();
// 退出房间
mTRTCCloud.exitRoom();

视角三:听众动作

时序图

?
?
?
1. 进入房间
public void enterRoom(String roomId, String userId) {
TRTCCloudDef.TRTCParams params = new TRTCCloudDef.TRTCParams();
// 以字符串房间号为例
params.strRoomId = roomId;
params.userId = userId;
// 从业务后台获取到的 UserSig
params.userSig = getUserSig(userId);
// 替换成您的 SDKAppID
params.sdkAppId = SDKAppID;
// 建议均以观众角色进房
params.role = TRTCCloudDef.TRTCRoleAudience;
// 进房场景须选择 LIVE
mTRTCCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
}
?
// 进房结果事件回调
@Override
public void onEnterRoom(long result) {
if (result > 0) {
// result 代表加入房间所消耗的时间(毫秒)
Log.d(TAG, "Enter room succeed");
} else {
// result 代表进房失败的错误码
Log.d(TAG, "Enter room failed");
}
}
注意:
为了更好地透传 SEI 消息用于歌词同步,建议进房场景选用 TRTC_APP_SCENE_LIVE
自动订阅模式下(默认)观众进房会自动订阅并播放麦上主播音频流。
2. 歌词同步
下载歌词
通过批量获取歌曲详情,即调用 REST API BatchDescribeKTVMusicDetails,获取歌词下载链接 LyricsUrl,将歌词缓存到本地。
听众端歌词同步
@Override
public void onUserVideoAvailable(String userId, boolean available) {
if (available) {
mTRTCCloud.startRemoteView(userId, null);
} else {
mTRTCCloud.stopRemoteView(userId);
}
}
?
@Override
public void onRecvSEIMsg(String userId, byte[] data) {
String result = new String(data);
try {
JSONObject jsonObject = new JSONObject(result);
int musicId = jsonObject.getInt("musicId");
long progress = jsonObject.getLong("progress");
long duration = jsonObject.getLong("duration");
} catch (JSONException e) {
e.printStackTrace();
}
...
// TODO 更新歌词控件逻辑:
// 根据接收到的最新进度和本地歌词进度误差,判断是否需要 seek 歌词控件
...
}
注意:
听众需要主动订阅主唱者的视频流,以便接收黑帧携带的 SEI 消息。
如果主唱混流同时混入黑帧,这里则只需订阅混流机器人的视频流即可。
3. 退出房间
// 退出房间
mTRTCCloud.exitRoom();
?
// 退出房间事件回调
@Override
public void onExitRoom(int reason) {
if (reason == 0) {
Log.d(TAG, "主动调用 exitRoom 退出房间");
} else if (reason == 1) {
Log.d(TAG, "被服务器踢出当前房间");
} else if (reason == 2) {
Log.d(TAG, "当前房间整个被解散");
}
}

高级功能

音速达打分模块接入

1. 下载歌词和音高文件
通过批量获取歌曲详情,即调用 REST API BatchDescribeKTVMusicDetails,获取歌词下载链接 LyricsUrl 和音高数据下载链接 MidiUrl,然后将歌词及音高文件缓存到本地并记录文件路径。
2. 配置音速达打分模块
AtomicBoolean isKSongPrepared = new AtomicBoolean(false);
ITXSongScore score = null;
?
private void songScore(String noteFilePath, String lyricFilePath) {
TXSongScoreConfig config = new TXSongScoreConfig();
// TRTC 采集音频采样率
config.sampleRate = 48000;
// TRTC 采集音频声道数
config.channel = 2;
// 歌词文件路径,xxx_vtt
config.lyricFilePath = lyricFilePath;
// 音高文件路径,xxx_midi
config.noteFilePath = noteFilePath;
score = copyrightedMedia.createSongScore(config);
score.setSongScoreCallback(new ITXSongScoreCallback() {
@Override
public void onMIDISCoreUpdate(int currentScore, int totalScore, int curIndex) {
/**
* 输出每句歌词的分数
* @param currentScore 这一句的分数[-1,100]
* @param totalScore 目前总分数
* @param curIndex 歌词索引下标
*/
}
@Override
public void onMIDIGroveAndHint(boolean isHit, float timeStamp, float pitch, int viewValue) {
/**
* 输出当前实时音高命中
* @param isHit 是否命中
* @param timeStamp 当前时间
* @param pitch 用户音高(数值可与note音高对比)
* @param viewValue UI展示的音高
*/
}
@Override
public void onMIDIScoreFinish(int[] scoreArray, int totalScore) {
/**
* 打分回调结果
* @param scoreArray 每句歌词的分数集合
* @param totalScore 总分
*/
score.destroy();
isKSongPrepared.set(false);
}
@Override
public void onMIDIScorePrepared() {
/**
* prepare后回调,表明当前打分模块准备就绪,可以执行process等操作
*/
isKSongPrepared.set(true);
}
@Override
public void onMIDIScoreError(int errCode, String errMsg) {
/**
* 打分模块错误回调
* @param errCode 见{@link ErrorCode}
* @param errMsg 提示信息
*/
score.destroy();
isKSongPrepared.set(false);
}
});
// 初始化打分模块
score.prepare();
}
3. TRTC 回调采集人声 PCM 数据给音速达打分模块
private void initTRTCAudioCapture() {
TRTCCloud mTRTCCloud = TRTCCloud.sharedInstance(context);
TRTCCloudDef.TRTCAudioFrameCallbackFormat format = new TRTCCloudDef.TRTCAudioFrameCallbackFormat();
format.channel = 2;
format.sampleRate = 48000;
format.samplesPerCall = 960;
mTRTCCloud.setCapturedAudioFrameCallbackFormat(format);
mTRTCCloud.setAudioFrameListener(new TRTCCloudListener.TRTCAudioFrameListener() {
@Override
public void onCapturedAudioFrame(TRTCCloudDef.TRTCAudioFrame trtcAudioFrame) {
// 音速达打分模块准备就绪
if (isKSongPrepared.get() && score != null) {
// 演唱中,开始打分
if (mTRTCCloud.getAudioEffectManager().getMusicCurrentPosInMS(musicId) <
mTRTCCloud.getAudioEffectManager().getMusicDurationInMS(musicUri)) {
// 获取所有音高,可用于 UI 绘制
TXSongScoreNoteItem[] noteItems = score.getAllGrove();
for (int i = 0; i < noteItems.length; i++) {
Log.d(TAG, "noteItem[" + i + "]: " + noteItems[i].toString());
}
// 处理采集的人声
score.process(trtcAudioFrame.data, trtcAudioFrame.data.length,
mTRTCCloud.getAudioEffectManager().getMusicCurrentPosInMS(musicId));
} else {
// 演唱完毕,结束打分
score.finish();
}
}
}
...
});
}
注意:
音频帧回调格式中的采样率、声道数、采样点数必须和音速达评分模块中配置的采集音频数据格式完全一致。
TRTC SDK 11.1版本前,设置本地麦克风采集出的音频帧回调格式 API 为 setCapturedRawAudioFrameCallbackFormat,本地采集并经过音频模块前处理后的音频数据回调为 onCapturedRawAudioFrame

混流透传单流音量大小

开启混流后,听众无法直接获取麦上主播单流音量。此时,可采取房主将所有麦上主播的回调音量值通过 SEI 发送出去的方式透传单流音量。
@Override
public void onUserVoiceVolume(ArrayList<TRTCCloudDef.TRTCVolumeInfo> userVolumes, int totalVolume) {
super.onUserVoiceVolume(userVolumes, totalVolume);
if (userVolumes != null && userVolumes.size() > 0) {
// 用于保存麦上用户对应的音量值
HashMap<String, Integer> volumesMap = new HashMap<>();
for (TRTCCloudDef.TRTCVolumeInfo user : userVolumes) {
// 可以设置适当的音量阈值
if (user.volume > 10) {
volumesMap.put(user.userId, user.volume);
}
}
Gson gson = new Gson();
String body = gson.toJson(volumesMap);
// 通过 SEI 消息发送麦上用户音量集合
mTRTCCloud.sendSEIMsg(body.getBytes(), 1);
}
}
?
@Override
public void onRecvSEIMsg(String userId, byte[] data) {
Gson gson = new Gson();
HashMap<String, Integer> volumesMap = new HashMap<>();
try {
String message = new String(data, "UTF-8");
volumesMap = gson.fromJson(message, volumesMap.getClass());
for (String userId : volumesMap.keySet()) {
// 打印所有麦上用户单流的音量大小
Log.i(userId, String.valueOf(volumesMap.get(userId)));
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
注意:
采用 SEI 消息从混流透传单流音量的前提是房主有视频推流或已开启补黑帧,同时听众需要主动订阅房主视频流。

网络质量实时回调

可以通过监听 onNetworkQuality 来实时统计本地及远端用户的网络质量,该回调每隔2秒抛出一次。
private class TRTCCloudImplListener extends TRTCCloudListener {
@Override
public void onNetworkQuality(TRTCCloudDef.TRTCQuality localQuality,
ArrayList<TRTCCloudDef.TRTCQuality> remoteQuality) {
// localQuality userId 为空,代表本地用户网络质量评估结果
// remoteQuality 代表远端用户网络质量评估结果,其结果受远端和本地共同影响
switch (localQuality.quality) {
case TRTCCloudDef.TRTC_QUALITY_Excellent:
Log.i(TAG, "当前网络非常好");
break;
case TRTCCloudDef.TRTC_QUALITY_Good:
Log.i(TAG, "当前网络比较好");
break;
case TRTCCloudDef.TRTC_QUALITY_Poor:
Log.i(TAG, "当前网络一般");
break;
case TRTCCloudDef.TRTC_QUALITY_Bad:
Log.i(TAG, "当前网络较差");
break;
case TRTCCloudDef.TRTC_QUALITY_Vbad:
Log.i(TAG, "当前网络很差");
break;
case TRTCCloudDef.TRTC_QUALITY_Down:
Log.i(TAG, "当前网络不满足 TRTC 最低要求");
break;
default:
Log.i(TAG, "未定义");
break;
}
}
}

高级权限控制

TRTC 高级权限控制可用于对不同房间设置不同进入权限,例如高级 VIP 房;也可用于控制听众上麦权限,例如处理幽灵麦。
步骤一:在 TRTC 控制台 应用的功能配置页面打开高级权限控制开关。
?
?
注意:
当某个 SDKAppID 开启高级权限控制后,使用该 SDKAppID 的所有用户都需要在 TRTCParams 中传入?privateMapKey?参数才可以成功进房。因此如果您线上有使用此 SDKAppID 的用户,请不要轻易开启此功能。
步骤二:在业务后台生成 PrivateMapKey,代码示例参考 PrivateMapKey 计算源码
步骤三:进房校验&上麦校验 PrivateMapKey。
进房校验
TRTCCloudDef.TRTCParams mTRTCParams = new TRTCCloudDef.TRTCParams();
mTRTCParams.sdkAppId = SDKAPPID;
mTRTCParams.userId = mUserId;
mTRTCParams.strRoomId = mRoomId;
// 从业务后台获取到的 UserSig
mTRTCParams.userSig = getUserSig();
// 从业务后台获取到的 PrivateMapKey
mTRTCParams.privateMapKey = getPrivateMapKey();
mTRTCParams.role = TRTCCloudDef.TRTCRoleAudience;
mTRTCCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
上麦校验
// 从业务后台获取到最新的 PrivateMapKey 传入切换角色接口
mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAnchor, getPrivateMapKey());

异常处理

异常错误处理

TRTC SDK 遇到不可恢复的错误会在 onError 回调中抛出,详见 TRTC 错误码表
1. UserSig 相关
UserSig 校验失败会导致进房失败,您可参考 UserSig 生成与校验 进行校验。
枚举
取值
描述
ERR_TRTC_INVALID_USER_SIG
-3320
进房参数 userSig 不正确,请检查 TRTCParams.userSig 是否为空。
ERR_TRTC_USER_SIG_CHECK_FAILED
-100018
UserSig 校验失败,请检查参数 TRTCParams.userSig 是否填写正确或已经过期。
2. 进退房相关
进房失败请先检查进房参数是否正确,且进退房接口必须成对调用,即便进房失败也需要调用退房接口。
枚举
取值
描述
ERR_TRTC_CONNECT_SERVER_TIMEOUT
-3308
请求进房超时,请检查是否断网或者是否开启 VPN,您也可以切换4G进行测试。
ERR_TRTC_INVALID_SDK_APPID
-3317
进房参数 sdkAppId 错误,请检查 TRTCParams.sdkAppId 是否为空
ERR_TRTC_INVALID_ROOM_ID
-3318
进房参数 roomId 错误,请检查 TRTCParams.roomIdTRTCParams.strRoomId 是否为空,注意 roomId 和 strRoomId 不可混用。
ERR_TRTC_INVALID_USER_ID
-3319
进房参数 userId 不正确,请检查 TRTCParams.userId 是否为空。
ERR_TRTC_ENTER_ROOM_REFUSED
-3340
进房请求被拒绝,请检查是否连续调用 enterRoom 进入相同 Id 的房间。
3. 设备相关
可监听设备相关错误,在出现相关错误时 UI 提示用户。
枚举
取值
描述
ERR_MIC_START_FAIL
-1302
打开麦克风失败,例如在 Windows 或 Mac 设备,麦克风的配置程序(驱动程序)异常,禁用后重新启用设备,或者重启机器,或者更新配置程序。
ERR_SPEAKER_START_FAIL
-1321
打开扬声器失败,例如在 Windows 或 Mac 设备,扬声器的配置程序(驱动程序)异常,禁用后重新启用设备,或者重启机器,或者更新配置程序。
ERR_MIC_OCCUPY
-1319
麦克风正在被占用中,例如移动设备正在通话时,打开麦克风会失败。

K 歌打分过低问题

1. 实际采集的音频数据格式与初始化打分模块不一致
对比 setCapturedAudioFrameCallbackFormat 设置的采样率、声道数和 onCapturedAudioFrame 回调的采样率、声道数是否一致;
对比 onCapturedAudioFrame 回调的采样率、声道数和音速达 TXSongScoreConfig 配置的采样率、声道数是否一致。
2. 打分模块传入的时间戳非伴奏当前播放的准确时间
检查通过 score.process 传入的时间戳是否是伴奏当前播放的准确时间。时间戳单位为毫秒,一般都是从0开始递增,建议使用 AudioEffectManager.getMusicCurrentPosInMS 获取准确的播放时间,不建议使用 onPlayProgress 回调进度。
3. 音高文件路径和歌词路径不正确
检查音高文件路径 noteFilePath 和歌词路径 lyricFilePath 是否正确。音高文件和歌词通常需要通过 REST API BatchDescribeKTVMusicDetails 获取对应的下载链接,并缓存到本地。
4. 人声采集音调与音速达打分音调不一致
音乐乐理中,通常将一个八度分为12个半音,每个半音之间的音高差为半个音阶。音速达 SDK 打分接口升降调 setKeyShift 支持范围 [-12, 12],而 TRTC 设置语音音调 setVoicePitch 支持范围 [-1, 1],所以 TRTC 音调升一个半音对应值为0.08,相应地音速达音调值为1;TRTC 降一个半音对应值为-0.08,相应地音速达音调值为-1。

耳返相关问题

1. 如何开启耳返功能及设置耳返音量
// 开启耳返
mTRTCCloud.getAudioEffectManager().enableVoiceEarMonitor(true);
// 设置耳返音量
mTRTCCloud.getAudioEffectManager().setVoiceEarMonitorVolume(int volume);
注意:
可以提前设置开启耳返,无需监听音频路由变化,接入耳机后耳返功能会自动生效。
2. 开启耳返功能后没有生效
由于蓝牙耳机的硬件延迟非常高,请尽量在用户界面上提示主播佩戴有线耳机。 同时也需要注意,并非所有的手机开启此特性后都能达到优秀的耳返效果,TRTC SDK 已经对部分耳返效果不佳的手机屏蔽了该特性。
3. 耳返延迟过高
请检查是否使用的是蓝牙耳机,由于蓝牙耳机的硬件延迟非常高,请尽量使用有线耳机。另外,可以尝试通过实验性接口 setSystemAudioKitEnabled 开启硬件耳返来改善耳返延迟过高的问题。硬件耳返性能较好,且延迟较低;软件耳返延迟较高,但兼容性较好。目前,对于华为和 VIVO 设备,SDK 默认使用硬件耳返,其他设备默认使用软件耳返。如果硬件耳返存在兼容性问题,可以 联系我们 配置强制使用软件耳返。

NTP 校时问题

1. NTP time sync finished, but result maybe inaccurate
NTP 校时成功,但偏差可能在30ms以上,反应客户端网络环境差,rtt 持续抖动。
2. Error in AddressResolver: No address associated with hostname
NTP 校时失败,可能是当前网络环境下本地运营商 DNS 解析暂时异常,请稍后再试。
3. NTP 服务重试处理逻辑
?
?
?

音乐播放资源路径问题

K 歌场景下使用 TRTC SDK 播放伴奏音乐建议搭配音速达引擎使用,当然您也可以选择播放本地或网络音乐资源。其中播放路径目前只支持传入网络资源 URL、设备外部存储及应用私有目录下音乐文件的绝对路径,不支持传入 Android 开发中的 assets 等目录下的文件路径。
您可以通过将 assets 目录下的资源文件提前拷贝到设备外部存储或应用私有目录下的方法规避这一问题,示例代码如下:
public static void copyAssetsToFile(Context context, String name) {
// 应用程序自身目录下的 files 目录
String savePath = ContextCompat.getExternalFilesDirs(context, null)[0].getAbsolutePath();
// 应用程序自身目录下的 cache 目录
// String savePath = getApplication().getExternalCacheDir().getAbsolutePath();
// 应用程序私有存储目录下的 files 目录
// String savePath = getApplication().getFilesDir().getAbsolutePath();
String filename = savePath + "/" + name;
File dir = new File(savePath);
// 如果目录不存在,创建这个目录
if (!dir.exists()) {
dir.mkdir();
}
try {
if (!(new File(filename)).exists()) {
InputStream is = context.getResources().getAssets().open(name);
FileOutputStream fos = new FileOutputStream(filename);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.close();
is.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
应用外部存储 files 目录路径:/storage/emulated/0/Android/data/<package_name>/files/<file_name>
应用外部存储 cache 目录路径:/storage/emulated/0/Android/data/<package_name>/cache/file_name>
应用私有存储 files 目录路径:/data/user/0/<package_name>/files/<file_name>
注意:
如果您传入的路径为非应用程序自身特定目录下的其他外部存储路径,在 Android 10及以上设备上可能面临拒绝访问资源,这是因为 Google 引入了新的存储管理系统,分区存储。可以通过在 AndroidManifest.xml 文件中的 <application> 标签内添加以下代码暂时规避:android:requestLegacyExternalStorage="true"。该属性只在 targetSdkVersion 为29(Android 10)的应用上生效,更高版本 targetSdkVersion 的应用仍建议您使用应用的私有或外部存储路径。
TRTC SDK 11.5 及以上版本支持传入 Content Provider 组件的 Content URI 来播放 Android 设备上的本地音乐资源。
Android 11 及 HarmonyOS 3.0 以上系统,如果无法访问外部存储目录下的资源文件,需要申请 MANAGE_EXTERNAL_STORAGE 权限:
首先,需要在您的应用的 AndroidManifest 文件中添加以下条目。
<manifest ...>
<!-- This is the permission itself -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
?
<application ...>
...
</application>
</manifest>
然后,在您的应用需要使用到这个权限的地方引导用户手动授权。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
}
} else {
// For Android versions less than Android 11, you can use the old permissions model
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
}

实时合唱相关用法问题

1. 实时合唱场景下主唱为什么要双实例推流?
实时合唱场景下为了尽量降低端到端延迟,从而达到人声和伴奏同步,通常采用主唱端双实例分别上行人声和伴奏,其他合唱端只上行人声,本地播放伴奏的方案。此时合唱端需要订阅主唱人声流,同时不订阅主唱音乐流,这样只有双实例分离推流才能实现。
2. 实时合唱场景下为什么建议开启混流回推?
听众端同时拉取多路单流极有可能导致多路人声流和伴奏流不对齐,而拉取混流则能保证各路流的绝对对齐,同时可以降低下行带宽。
3. SEI 在实时合唱场景中的用途有哪些方面?
传递伴奏音乐进度,用于听众端歌词同步。
混流透传单流音量,用于听众端展示音浪。
4. 伴奏音乐加载耗时长,存在较大播放延迟?
SDK 加载网络音乐资源需要一定耗时,建议在开始播放前提前开启音乐预加载。
mTRTCCloud.getAudioEffectManager().preloadMusic(musicParam);
5. 随伴奏演唱时听不清人声,音乐压制人声?
如果采用默认音量存在伴奏音乐压制人声的情况,建议适当调整音乐及人声音量占比。
// 设置某一首背景音乐的本地播放音量的大小
mTRTCCloud.getAudioEffectManager().setMusicPlayoutVolume(musicID, volume);
// 设置某一首背景音乐的远端播放音量的大小
mTRTCCloud.getAudioEffectManager().setMusicPublishVolume(musicID, volume);
// 设置所有背景音乐的本地音量和远端音量的大小
mTRTCCloud.getAudioEffectManager().setAllMusicVolume(volume);
// 设置人声采集音量的大小
mTRTCCloud.getAudioEffectManager().setVoiceCaptureVolume(volume);
?


http://www.vxiaotou.com