实现一起听(基于底层能力)
更新时间: 2024/11/26 15:44:05
本文介绍如何基于 RTC,在您的 App 中添加一起听场景,实现房主和观众一起听音乐。
方案架构
开发环境要求
开发环境要求如下:
环境要求 | 说明 |
---|---|
JDK 版本 | 1.8.0 及以上版 |
Android API 版本 | API 21 及以上版本 |
Android Studio 版本 | 5.0 及以上版本 |
CPU架构 | ARM 64、ARMV7 |
IDE | Android Studio |
其他 | 依赖 Androidx,不支持 support 库。 Android 系统 5.0 及以上版本的真机。 |
前提条件
API 时序图
sequenceDiagram
participant playerA as NERTC SDK
participant hostClientA as 房主A
participant app_server as 一起听Server
participant neroom_server as NIM_Server
participant audienceB as 连麦观众B
participant playerB as NERTC SDK
Note over playerA, playerB: 单人场景
hostClientA ->> app_server: 创建房间、加入房间
hostClientA ->> app_server: 点歌(已下载)
app_server -->> hostClientA: 点歌信息
app_server -->> hostClientA: 歌单变化
hostClientA ->> app_server: 上报歌曲ready
app_server ->> app_server: 判断房间人数和上报ready人数
app_server ->> hostClientA: 下发开始播放
hostClientA ->> playerA: 播放
hostClientA ->> hostClientA: 刷新歌词
audienceB ->> app_server: 加入房间
Note over playerA, playerB: 单人场景暂停、恢复
hostClientA ->> app_server: 暂停
app_server -->> hostClientA: 下发暂停播放
hostClientA ->> playerA: 暂停
hostClientA ->> app_server: 恢复
app_server -->> hostClientA: 下发开始播放
hostClientA ->> playerA: 恢复
Note over playerA, playerB: 单人场景切歌
hostClientA ->> app_server: 切歌
app_server -->> hostClientA: 下发切歌,包含下一首歌信息
hostClientA ->> app_server: 上报下一首歌ready
app_server -->> hostClientA: 下发开始播放
hostClientA ->> playerA: 播放
Note over playerA, playerB: 观众上麦
audienceB ->> app_server: 上麦(主播抱麦)
audienceB ->> app_server: 获取当前播放歌曲
audienceB ->> app_server: 获取已点歌曲列表
app_server -->> audienceB: 当前播放歌曲信息
app_server -->> audienceB: 当前已点歌曲列表
audienceB ->> audienceB: 下载当前播放歌曲
audienceB ->> audienceB: 当前歌曲下载完成
audienceB ->> audienceB: 预下载歌曲列表的其他歌曲
audienceB ->> neroom_server: 获取播放进度
neroom_server ->> hostClientA: 下发消息通知房主需要同步播放进度给连麦观众
hostClientA ->> neroom_server: 通知当前播放进度
neroom_server -->> audienceB: 通知当前播放进度
audienceB ->> audienceB: 对齐播放进度
audienceB ->> playerB: seekTo
audienceB ->> audienceB: 刷新歌词
sequenceDiagram
participant playerA as NERTC SDK
participant hostClientA as 房主A
participant app_server as 一起听Server
participant neroom_server as NIM_Server
participant audienceB as 连麦观众B
participant playerB as NERTC SDK
Note over playerA, playerB: 同步一起听歌曲状态(暂停、恢复)
hostClientA->>app_server: 歌曲播放控制,暂停or恢复
audienceB->>app_server: 歌曲播放控制,暂停or恢复
app_server-->>hostClientA: 下发当前播放歌曲状态
app_server-->>audienceB: 下发当前播放歌曲状态
hostClientA->>playerA: 暂停or恢复
audienceB->>playerB: 暂停or恢复
Note over playerA, playerB: 同步一起听歌曲状态(换歌)
hostClientA->>app_server: 上报换歌动作
app_server-->>hostClientA: 下发换歌消息
app_server-->>audienceB: 下发换歌消息
alt AB均已下载待播放歌曲
hostClientA->>app_server: 上报待播歌曲ready
audienceB->>app_server: 上报待播歌曲ready
app_server->>app_server: 判断房间人数和上报ready人数
app_server-->>hostClientA: 下发开始播放
app_server-->>audienceB: 下发开始播放
else AB至少有一人未下载待播歌曲
audienceB->>audienceB: 先切到待播歌曲状态,同时下载歌曲,状态为下载中
hostClientA->>hostClientA: 先切到待播歌曲状态,同时下载歌曲,状态为下载中
hostClientA->>app_server: 上报待播歌曲ready
audienceB->>app_server: 上报待播歌曲ready
app_server-->>hostClientA: 下发开始播放
app_server-->>audienceB: 下发开始播放
hostClientA->>playerA: 播放新歌
audienceB->>playerB: 播放新歌
end
Note over playerA, playerB: 同步一起听歌曲进度
hostClientA->>hostClientA: 拖动进度条
hostClientA->>playerA: seekTo
hostClientA->>hostClientA: 刷新歌词
hostClientA->>neroom_server: 通知播放进度(NERoom点对点通知自定义消息)
neroom_server-->>audienceB: 通知播放进度 <br> (NERoom点对点通知自定义消息)
audienceB->>audienceB: 对齐播放进度
audienceB->>playerB: seekTo
audienceB->>audienceB: 刷新歌词
房间管理
-
房主创建一起听房间。
客户端通过业务服务器提供的restful api创建房间。
-
房主和连麦观众分别调用
joinChannelWithToken
接口加入RTC房间,分别调用enterChatRoom
接口加入聊天室。一起听房间只允许两个用户同时在线,如果房间中已经存在两个用户,则第三个用户无法加入。
-
房主和连麦观众分别调用
setChannelProfile
接口,设置房间场景为直播场景(LIVE_BROADCASTING)。 -
房主和连麦观众分别调用
setAudioProfile
接口,设置音频profile
类型为HighQualityStereo
,设置scenario
为MUSIC
。 -
连麦观众调用业务服务器接口(需自行实现)离开一起听房间,同时调用
leaveChannel
接口离开RTC房间,调用exitChatRoom
接口离开聊天室。 -
房主调用业务服务器接口(需自行实现)结束一起听房间,同时调用
leaveChannel
接口离开RTC房间,调用exitChatRoom
接口离开聊天室。
示例代码
// 房主调用自己的业务服务器restful api创建房间。
// 房主或者观众加入房间:
主播和观众加入RTC
let ret = NERtcEngine.shared().joinChannel(withToken: rtcToken,
channelName: channel,
myUid: rtcUid) { error, channelId, elapesd, uid in
}
主播和观众加入聊天室
let request = NIMChatroomEnterRequest()
request.roomId = roomId
request.roomNickname = nickName
ChatroomProvider.shared.enterChatroom(request: request) { error, chatroom, member in
}
// 设置房间场景为直播场景
NERtcEngine.shared()
.setChannelProfile(NERtcChannelProfileType(rawValue: liveBroadcasting.rawValue))
// 设置音频类型
NERtcEngine.shared().setAudioProfile(.highQuality, scenario: .music)
// 观众离开房间
观众调用业务服务器接口离开房间,同时离开RTC房间和聊天室
a、离开RTC房间
NERtcEx.getInstance().leaveChannel();
b、离开聊天室
NIMSDK.shared().chatroomManager.exitChatroom(roomId, completion: completion)
// 房主关闭房间
房主调用业务服务器接口关闭房间,同时离开RTC房间和聊天室
a、离开RTC房间
NERtcEx.getInstance().leaveChannel();
b、离开聊天室
NIMSDK.shared().chatroomManager.exitChatroom(roomId, completion: completion)
实现一起听
步骤1 初始化曲库 SDK
-
调用
NECopyrightedMedia getInstance
接口创建版权音乐对象。NECopyrightedMedia * copyRight = [NECopyrightedMedia getInstance];
-
调用
initialize
接口初始化组件。
/// 初始化 NECopyrightedMedia
/// @param appkey appkey
/// @param token token
/// @param userUuid userUuid
/// @param extras 填入Nil
/// @param callback 异步回调 NSError 为Nil 则成功
- (void)initialize:(NSString *_Nonnull)appkey
token:(NSString *_Nonnull)token
userUuid:(NSString *_Nullable)userUuid
extras:(NSDictionary *_Nullable)extras
callback:(void (^)(NSError *_Nullable error))callback;
-
调用
NECopyrightedMedia.setSongScene
接口,指定音乐场景为听歌的场景。版权曲库支持听歌场景和 K 歌场景,您需要在初始化曲库 SDK 后指定对应的场景。
// 设置听歌场景 NECopyrightedMedia.getInstance().setSongScene(TYPE_LISTENING_TO_MUSIC)
-
调用
setEventHandler
接口注册事件通知回调。当您的曲库 Token 过期时,会触发
onTokenExpired
回调。此时,您需要参见曲库动态 Token 鉴权生成新的 Token,并调用renewToken
更新 Token 后才能继续调用 NECopyrightedMedia SDK 的API。//设置动态Token过期代理 [[NECopyrightedMedia getInstance] setEventHandler:self]; //回调如下 - (void)onTokenExpired { // Token过期 //此处需要申请新的realTimeToken }
-
调用
renewToken
接口更新 Token。[[NECopyrightedMedia getInstance] renewToken:copyrightedToken];
初始化操作完毕,如果能正常调用以下接口,表示初始化成功。
步骤2 获取歌曲列表
房主和连麦观众可以通过搜索、请求歌曲列表、榜单三种方式获取歌曲。
-
通过搜索获取歌曲
调用
NECopyrightedMedia.searchSong
接口获取搜索的歌曲列表和歌曲的song ID。参数 类型 描述 keyword String 搜索的关键字。 channel Int 版权渠道,默认不传则包含所有签约渠道。 - 1:网易云音乐
- 2: 咪咕
- 3:HIFIVE
pageNum Int 页码。 默认值为 0。 pageSize Int 每页显示的行数,默认值为 20。 callback Callback 回调 -
通过请求歌曲列表获得歌曲
调用
NECopyrightedMedia.getSongList
接口获取歌曲列表和歌曲的song ID。 -
通过榜单获取歌曲
调用
getHotSongList
接口获取推荐歌单的歌曲 song ID。
示例代码如下:
// 搜索歌曲
NECopyrightedMedia.getInstance()
.searchSong("keyword", channel: 1, pageNum: 1,
pageSize: 20) { songList, error in
}
// 获取歌曲列表
NECopyrightedMedia.getInstance()
.getSongList(nil, channel: 1, pageNum: 1,
pageSize: 20) { songList, error in
}
// 获取热门歌曲列表
[[NECopyrightedMedia getInstance] getHotSongList:HOTTYPE_DEFAULT channel:@1 hotDimension:HOTDIMENSION_PLATFORM pageNum:@0 pageSize:@20 callback:^(NSArray<NECopyrightedHotSong *> * _Nonnull songList, NSError * _Nonnull error) {
if (error) {
NSLog(@"获取歌曲列表失败");
}else{
NSLog(@"获取歌曲列表成功");
for (NECopyrightedSong *songItem in songList) {
//遍历数据
NSLog(@"songDta --- %@",songItem);
}
}
}];
步骤3 点歌并下载歌曲
调用 NECopyrightedMedia.preloadSong
接口预加载歌曲,包括原唱、歌词和MIDI。
示例代码如下:
//遵循协议:
<NESongPreloadProtocol>
//请求示例:
[[NECopyrightedMedia getInstance] preloadSong:songModel.songId channel:CLOUD_MUSIC observe:self];
//请求回调:
//开始下载回调
-(void)onPreloadStart:(NSString *)songId channel:(SongChannel)channel{
NSLog(@"onPreloadStart -- songId = %@",songId);
}
//下载进度回调
-(void)onPreloadProgress:(NSString *)songId channel:(SongChannel)channel progress:(float)progress{
NSLog(@"onPreloadProgress -- songId = %@ ; progress = %.2f",songId,progress);
}
//下载失败/完成回调
-(void)onPreloadComplete:(NSString *)songId channel:(SongChannel)channel error:(NSError * _Nullable)error{
if(error){
NSLog(@"onPreloadComplete error reason:%@",error.description);
}else{
NSLog(@"onPreloadComplete")
}
}
步骤4 房主播放歌曲
调用 playEffectWitdId
播放歌曲。
示例代码如下:
var path:String = "路径"
/// 如果使用版权SDK,可通过以下方法获取
/// NECopyrightedMedia.getInstance().getSongURI(songId, channel: channel, songResType: songResType)
let option = NERtcCreateAudioEffectOption()
option.path = path
option.loopCount = 1
option.sendEnabled = false
option.playbackEnabled = true
option.sendVolume = 100
option.playbackVolume = 100
option.progressInterval = 100
option.sendWithAudioType = .main
option.startTimeStamp = 1000
var effectId = 10001;///自定义播放通道ID,需要rong'yi
NERtcEngine.shared().playEffectWitdId(effectId, effectOption: option.convertToRTC())
步骤5 观众上麦
观众加入房间后,自动上麦并获取歌曲播放信息。
- 观众加入房间后,主播把观众抱上麦位。
- 观众调用业务服务器接口获取当前播放歌曲列表。
- 观众调用
NECopyrightedMedia.preloadSong
接口预加载歌曲。
// 观众进入房间,主播抱麦,观众自动上麦 (具体业务逻辑需自行实现)
// 调用业务服务器获取当前播放歌曲和歌曲列表(需自行实现)
// 预加载当前播放歌曲,在加载完后通过点对点自定义消息向主播询问当前播放进度
String songId="your songId";
int channel=1;
if (NECopyrightedMedia.getInstance().isSongPreloaded(songId,channel)){
// 向主播询问当前歌曲播放进度
let m = NIMMessage()
m.text = "{\"commandId\":10001,\"msg\":\"\"}"// 可以设置自定义消息
m.toAccIds = [String]
// 自定义扩展
m.remoteExt = [fromAccountId: fromId, toAccIds: group]
let session = NIMSession(roomId, type: .chatroom)
let session = NIMSession(roomId, type: .chatroom)
NIMSDK.shared().chatManager.send(message, to: session) { error in
}
}else {
NECopyrightedMedia.getInstance().preloadSong(songId, channel: channel, observe: self)
}
//下载完成回调
- (void)onPreloadComplete:(NSString *)songId
channel:(SongChannel)channel
error:(NSError *)error {
// 向主播询问当前歌曲播放进度
let m = NIMMessage()
m.text = "{\"commandId\":10001,\"msg\":\"\"}"// 可以设置自定义消息
m.toAccIds = [String]
// 自定义扩展
m.remoteExt = [fromAccountId: fromId, toAccIds: group]
let session = NIMSession(roomId, type: .chatroom)
let session = NIMSession(roomId, type: .chatroom)
NIMSDK.shared().chatManager.send(message, to: session) { error in
}
}
步骤6 同步一起听的歌曲状态和进度
- 房主通过
NIMSDK.shared().chatManager.send
发送定向消息,将播放进度同步给观众。 - 观众调用
NIMSDK.shared().chatManager.send
接口获取歌曲播放进度。
NIMSDK.shared().chatManager.add(self)
// 收到消息回调
func onRecvMessages(_ messages: [NIMMessage]) {
print("@@#🏷️ onRecvMessages success:", messages)
if let caseModel = lastCaseModel {
NEHawkManager.shared().sendMessage(caseModel)
}
for message in messages {
if message.messageType == .text{
var string = message.text
///json 转 dic
let draft = string?.data(using: String.Encoding.utf8)
let d = try? JSONSerialization.jsonObject(with: draft!, options: .mutableContainers)
let dic = d as! NSDictionary
guard let commandId = dic["commandId"] else{
return
}
if (commandId as AnyObject).intValue == 10001{
let m = NIMMessage()
m.text = "{\"commandId\": 10002,\"progress\" : 1000}"// 可以设置自定义消息,1000 为播放进度,单位是ms
m.toAccIds = [String]
let session = NIMSession(roomId, type: .chatroom)
let session = NIMSession(roomId, type: .chatroom)
NIMSDK.shared().chatManager.send(message, to: session) { error in
}
}else if (commandId as AnyObject).intValue == 10002{
//seek
NERtcEngine.shared().setEffectPositionWithId(effectId, position: dic["progress"])
}
}
}
}
进阶功能
暂停或恢复歌曲播放
// 暂停歌曲播放
NERtcEx.getInstance().pauseEffect(effectId);
// 恢复歌曲播放
NERtcEx.getInstance().resumeEffect(effectId);
切歌
// 停止播放上一首歌
NERtcEx.getInstance().stopEffect(effectId);
// 播放下一首歌
NERtcCreateAudioEffectOption neRtcCreateAudioEffectOption = new NERtcCreateAudioEffectOption();
neRtcCreateAudioEffectOption.path="";// 音乐文件路径
neRtcCreateAudioEffectOption.loopCount=1;
neRtcCreateAudioEffectOption.sendEnabled=true;
neRtcCreateAudioEffectOption.sendVolume=100;
neRtcCreateAudioEffectOption.playbackEnabled=true;
neRtcCreateAudioEffectOption.playbackVolume=100;
neRtcCreateAudioEffectOption.startTimestamp=0;
neRtcCreateAudioEffectOption.progressInterval=100;
neRtcCreateAudioEffectOption.sendWithAudioType= NERtcAudioStreamType.kNERtcAudioStreamTypeSub;
NERtcEx.getInstance().playEffect(effectId,neRtcCreateAudioEffectOption);