三、步骤一:开通服务
在线 K 歌场景通常需要依赖腾讯云 实时音视频 TRTC 和曲库类产品两项付费 PaaS 服务构建。其中 TRTC 负责提供实时音频互动能力,曲库类产品负责提供正版音乐资源。开通 TRTC 服务的步骤如下:
1. 首先,你需要登录 实时音视频 TRTC 控制台 创建应用,你可根据需要选择升级 TRTC 应用版本,例如旗舰版可解锁更多增值功能服务。
注意:说明:
2. 应用创建完毕之后,你可以在应用管理 > 应用概览栏目看到该应用的基本信息,其中需要你保管好 SDKAppID、SDKSecretKey 便于后续使用,同时应避免密钥泄露造成流量盗刷。
四、步骤二:导入 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'}
注意:说明:
2. 在 defaultConfig 中,指定 App 使用的 CPU 架构。
defaultConfig { ndk { abiFilters "armeabi-v7a", "arm64-v8a" }}
注意:说明:
注意:TRTC SDK 支持 armeabi-v7a/arm64-v8a/x86/x86_64架构。
五、步骤三:工程配置
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" />
2. 混淆配置。
由于我们在 SDK 内部使用了 Java 的反射特性,需要你在 proguard-rules.pro 文件中将 SDK 相关类加入不混淆名单:
-keep class com.tencent.** { *; }
六、步骤四:鉴权与许可
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 提供实时音视频服务。
七、步骤五:初始化 SDK
初始化 TRTC 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 事件通知,对一些常见错误进行日志打印和处理,详见 错误码表。
十、时序图
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。
// 进房结果事件回调@Overridepublic 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);// 切换角色事件回调@Overridepublic 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. 点歌演唱。
·搜索歌曲,获取音乐资源。
通过向业务后台搜索目标歌曲,获取歌曲标识 MusicId、歌曲地址 MusicUrl、歌词地址 LyricsUrl 等音乐资源。
建议业务侧自行选择合适的音乐曲库类产品来提供正版音乐资源。
·播放伴奏,开始演唱。
// 获取音频特效管理类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);
4. 歌词同步。
·下载歌词。
通过向业务后台获取目标歌词下载链接 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) { // 音乐播放完成 }});
5. 下麦退房。
// 切换为观众角色mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAudience);// 切换角色事件回调@Overridepublic void onSwitchRole(int errCode, String errMsg) { if (errCode == TXLiteAVCode.ERR_NULL) { // 停止播放伴奏音乐 mTRTCCloud.getAudioEffectManager().stopPlayMusic(musicId); // 停止本地音频的采集和发布 mTRTCCloud.stopLocalAudio(); }}// 退出房间mTRTCCloud.exitRoom();// 退出房间事件回调@Overridepublic 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. 进入房间。
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);}// 进房结果事件回调@Overridepublic void onEnterRoom(long result) { if (result > 0) { // result 代表加入房间所消耗的时间(毫秒) Log.d(TAG, "Enter room succeed"); } else { // result 代表进房失败的错误码 Log.d(TAG, "Enter room failed"); }}
2. 歌词同步。
·下载歌词。
通过向业务后台获取目标歌词下载链接 LyricsUrl,将目标歌词缓存到本地。
·听众端歌词同步。
@Overridepublic void onUserVideoAvailable(String userId, boolean available) { if (available) { mTRTCCloud.startRemoteView(userId, null); } else { mTRTCCloud.stopRemoteView(userId); }}@Overridepublic 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();// 退出房间事件回调@Overridepublic 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. 进房后设置。
// 主实例进房结果事件回调@Overridepublic 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"); }}// 子实例进房结果事件回调@Overridepublic 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. 搜索与点歌。
通过向业务后台搜索目标歌曲,获取歌曲标识 MusicId、歌曲地址 MusicUrl、歌词地址 LyricsUrl 等音乐资源。
建议业务侧自行选择合适的音乐曲库类产品来提供正版音乐资源。
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);
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. 歌词同步。
·下载歌词。
通过向业务后台获取目标歌词下载链接 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) { // 音乐播放完成 }});
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);}// 进房结果事件回调@Overridepublic 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);// 切换角色事件回调@Overridepublic 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); }}
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. 接收合唱信令。
@Overridepublic 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);}
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. 歌词同步。
·下载歌词。
通过向业务后台获取目标歌词下载链接 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);}// 进房结果事件回调@Overridepublic void onEnterRoom(long result) { if (result > 0) { // result 代表加入房间所消耗的时间(毫秒) Log.d(TAG, "Enter room succeed"); } else { // result 代表进房失败的错误码 Log.d(TAG, "Enter room failed"); }}
2. 歌词同步。
·下载歌词。
通过向业务后台获取目标歌词下载链接 LyricsUrl,将目标歌词缓存到本地。
·听众端歌词同步。
@Overridepublic void onUserVideoAvailable(String userId, boolean available) { if (available) { mTRTCCloud.startRemoteView(userId, null); } else { mTRTCCloud.stopRemoteView(userId); }}@Overridepublic 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 歌词控件 …}
3. 退出房间。
// 退出房间mTRTCCloud.exitRoom();// 退出房间事件回调@Overridepublic 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, "当前房间整个被解散"); }}
21、SEI 替代方案
在线 K 歌场景通常需要使用 SEI 来传递一些额外的信息,例如伴奏音乐进度、单流音量大小、演唱评分结果等。但是 SEI 需要依赖视频帧来传输,这在纯音频业务场景下会产生不必要的额外费用。因此,如果你只需要在 TRTC 房间内传输一些信令消息,推荐使用 TRTC 音频帧的 extraData 字段替代 SEI 方案。
22、发送端
mTRTCCloud.setAudioFrameListener(new TRTCCloudListener.TRTCAudioFrameListener() { @Override public void onLocalProcessedAudioFrame(TRTCCloudDef.TRTCAudioFrame trtcAudioFrame) { try { JSONObject musicInfo = new JSONObject(); musicInfo.put("musicId", musicId); musicInfo.put("progress", progress); musicInfo.put("duration", duration); trtcAudioFrame.extraData = musicInfo.toString().getBytes(); } catch (JSONException e) { e.printStackTrace(); } }});
23、接收端
mTRTCCloud.setAudioFrameListener(new TRTCCloudListener.TRTCAudioFrameListener() { @Override public void onRemoteUserAudioFrame(TRTCCloudDef.TRTCAudioFrame trtcAudioFrame, String userId) { String extraDataStr = new String(trtcAudioFrame.extraData); try { JSONObject musicInfo = new JSONObject(extraDataStr); int musicId = musicInfo.getInt("musicId"); long progress = musicInfo.getLong("progress"); long duration = musicInfo.getLong("duration"); Log.d("extraData", userId + ": " + musicId + " " + progress + " " + duration); } catch (JSONException e) { e.printStackTrace(); } }});
24、混响和变声特效
为了丰富在线 K 歌场景的玩法,提升趣味性,我们可以使用混响特效和变声特效对人声进行特殊处理,从而呈现出不一样的演唱效果。
25、混响特效
混响特效可作用于人声之上,通过声学算法对声音进行叠加处理,模拟出各种不同环境下的临场感受,目前支持 KTV、小房间、大会堂、录音棚等混响效果。
mTRTCCloud.getAudioEffectManager().setVoiceReverbType(TXAudioEffectManager.TXVoiceReverbType.TXLiveVoiceReverbType_1);
26、变声特效
变声特效可作用于人声之上,通过声学算法对人声进行二次处理,以获得与原始声音不同的音色,目前支持熊孩子、萝莉、大叔、重金属、空灵等变声效果。
mTRTCCloud.getAudioEffectManager().setVoiceChangerType(TXAudioEffectManager.TXVoiceChangerType.TXLiveVoiceChangerType_1);
27、曲库打分模块接入
1. 下载歌词和音高文件。
提前准备好待打分的演唱录制文件、原始音乐标准文件、MIDI 音高文件等。
2. 配置曲库打分模块。
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 绘制, 待业务补充 // 处理采集的人声, 待业务补充 } else { // 演唱完毕,结束打分, 待业务补充 } } } … });}
28、混流透传单流音量大小
开启混流后,听众无法直接获取麦上主播单流音量。此时,可采取房主将所有麦上主播的回调音量值通过 SEI 发送出去的方式透传单流音量。
@Overridepublic 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); }}@Overridepublic 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 消息从混流透传单流音量的前提是房主有视频推流或已开启补黑帧,同时听众需要主动订阅房主视频流。
29、网络质量实时回调
可以通过监听 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; } }}
30、高级权限控制
TRTC 高级权限控制可用于对不同房间设置不同进入权限,例如高级 VIP 房;也可用于控制听众上麦权限,例如处理幽灵麦。
1. 在 TRTC 控制台 应用的功能配置页面打开高级权限控制开关。
注意:当某个 SDKAppID 开启高级权限控制后,使用该 SDKAppID 的所有用户都需要在 TRTCParams 中传入 privateMapKey 参数才可以成功进房。因此如果你线上有使用此 SDKAppID 的用户,请不要轻易开启此功能。
2. 在业务后台生成 PrivateMapKey,代码示例参考 PrivateMapKey 计算源码。
3. 进房校验&上麦校验 PrivateMapKey。
·进房校验。
TRTCCloudDef.TRTCParams mTRTCParams = new TRTCCloudDef.TRTCParams();mTRTCParams.sdkAppId = SDKAPPID;mTRTCParams.userId = mUserId;mTRTCParams.strRoomId = mRoomId;// 从业务后台获取到的 UserSigmTRTCParams.userSig = getUserSig();// 从业务后台获取到的 PrivateMapKeymTRTCParams.privateMapKey = getPrivateMapKey();mTRTCParams.role = TRTCCloudDef.TRTCRoleAudience;mTRTCCloud.enterRoom(mTRTCParams, TRTCCloudDef.TRTC_APP_SCENE_LIVE);
·上麦校验。
// 从业务后台获取到最新的 PrivateMapKey 传入切换角色接口mTRTCCloud.switchRole(TRTCCloudDef.TRTCRoleAnchor, getPrivateMapKey());
32、异常错误处理
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.roomId 或 TRTCParams.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 | 麦克风正在被占用中,例如移动设备正在通话时,打开麦克风会失败。 |
33、K 歌打分过低问题
1. 实际采集的音频数据格式与初始化打分模块不一致。
·对比 setCapturedAudioFrameCallbackFormat 设置的采样率、声道数和 onCapturedAudioFrame 回调的采样率、声道数是否一致;
·对比 onCapturedAudioFrame 回调的采样率、声道数和曲库配置的采样率、声道数是否一致。
2. 打分模块传入的时间戳非伴奏当前播放的准确时间。
检查通过曲库打分模块传入的时间戳是否是伴奏当前播放的准确时间。时间戳单位为毫秒,一般都是从0开始递增,建议使用 AudioEffectManager.getMusicCurrentPosInMS 获取准确的播放时间,不建议使用 onPlayProgress 回调进度。
3. 音高文件路径和歌词路径不正确。
检查音高文件路径 noteFilePath 和歌词路径 lyricFilePath 是否正确。
4. 人声采集音调与曲库打分音调不一致。
音乐乐理中,通常将一个八度分为12个半音,每个半音之间的音高差为半个音阶。曲库 SDK 打分接口升降调 setKeyShift 支持范围 [-12, 12],而 TRTC 设置语音音调 setVoicePitch 支持范围 [-1, 1],所以 TRTC 音调升一个半音对应值为0.08,相应地曲库音调值为1;TRTC 降一个半音对应值为-0.08,相应地曲库音调值为-1。
34、耳返相关问题
1. 如何开启耳返功能及设置耳返音量。
// 开启耳返mTRTCCloud.getAudioEffectManager().enableVoiceEarMonitor(true);// 设置耳返音量mTRTCCloud.getAudioEffectManager().setVoiceEarMonitorVolume(int volume);
注意:可以提前设置开启耳返,无需监听音频路由变化,接入耳机后耳返功能会自动生效。
2. 开启耳返功能后没有生效。
由于蓝牙耳机的硬件延迟非常高,请尽量在用户界面上提示主播佩戴有线耳机。 同时也需要注意,并非所有的手机开启此特性后都能达到优秀的耳返效果,TRTC SDK 已经对部分耳返效果不佳的手机屏蔽了该特性。
3. 耳返延迟过高。
请检查是否使用的是蓝牙耳机,由于蓝牙耳机的硬件延迟非常高,请尽量使用有线耳机。另外,硬件耳返性能较好,且延迟较低;软件耳返延迟较高,但兼容性较好。目前,对于华为、 VIVO、小米、OPPO、一加和荣耀设备,SDK 默认策略开启硬件耳返,其他设备默认使用软件耳返,并且 SDK 目前有兜底策略,不需要主动配置。
35、NTP 校时问题
1. NTP time sync finished, but result may be inaccurate。
NTP 校时成功,但偏差可能在30ms以上,反映客户端网络环境差,rtt 持续抖动。
2. Error in AddressResolver: No address associated with hostname。
NTP 校时失败,可能是当前网络环境下本地运营商 DNS 解析暂时异常,请稍后再试。
3. NTP 服务重试处理逻辑。
。
36、音乐播放资源路径问题
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>
注意:<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);}
37、实时合唱相关用法问题
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);