实现一起听(基于NERoom 房间组件)
更新时间: 2023/08/30 06:04:31
本文介绍如何基于 NERoom 房间组件,在您的 App 中添加一起听场景,实现房主和观众一起听音乐。
方案架构
开发环境要求
开发环境要求如下:
环境要求 | 说明 |
---|---|
iOS 版本 | 9.0 及以上 |
CPU 架构 | ARM64、ARMV7 |
IDE | XCode |
其他 | 安装 CocoaPods。 |
前提条件
示例项目源码
一起听的示例项目源码地址请参见 一起听 NEListenTogether 示例项目源码。
示例项目的目录结构和跑通示例项目的步骤请参见跑通示例项目。
API 时序图
sequenceDiagram
participant playerA as NERoom SDK
participant hostClientA as 房主A
participant app_server as 一起听Server
participant neroom_server as NERoom_Server
participant audienceB as 连麦观众B
participant playerB as NERoom 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: 刷新歌词
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 NERoom SDK
participant hostClientA as 房主A
participant app_server as 一起听Server
participant neroom_server as NERoom_Server
participant audienceB as 连麦观众B
participant playerB as NERoom 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: 通知播放进度 <br> (NERoom点对点自定义消息)
neroom_server ->> audienceB: 通知播放进度 <br> (NERoom点对点自定义消息)
audienceB ->> audienceB: 对齐播放进度
audienceB ->> playerB: seekTo
audienceB ->> audienceB:刷新歌词
房间管理
-
房主创建一起听房间。
您可以通过自己的业务服务器调用 NERoom Server 的接口去创建房间,客户端通过业务服务器提供的restful api创建房间。
-
房主和连麦观众分别调用
joinRoom
接口加入一起听房间。加入房间成功后,房间内用户触发onMemberJoinRoom
回调。一起听房间只允许两个用户同时在线,如果房间中已经存在两个用户,则第三个用户无法加入。
-
房主和连麦观众分别调用
setChannelProfile
接口,设置房间场景为直播场景(LIVE_BROADCASTING)。 -
房主和连麦观众分别调用
setAudioProfile
接口,设置音频profile
类型为HighQualityStereo
,设置scenario
为MUSIC
。 -
连麦观众调用
leaveRoom
接口离开一起听房间。 -
房主调用
endRoom
接口结束一起听房间。
示例代码
// 房主调用自己的业务服务器restful api创建房间。
// 房主或者观众加入房间:
// 进入房间
let joinParams = NEJoinRoomParams()
joinParams.roomUuid = "123"
joinParams.userName = "userName"
joinParams.role = "host" // host为房主,audience为观众
let joinOptions = NEJoinRoomOptions()
NERoomKit.shared().roomService.joinRoom(params: joinParams,
options: joinOptions) { [weak self] joinCode, joinMsg, context in
guard let context = context else {
)
callback?(joinCode, joinMsg, nil)
return
}
self.roomContext = context
context.addRoomListener(listener: self)
context.seatController.addSeatListener(self)
// 加入chatroom、rtc
// 加入rtc
context.rtcController.joinRtcChannel { rtcCode, rtcMsg, _ in
guard rtcCode == 0 else {
// 加入rtc 失败,离开房间
context.leaveRoom()
callback?(rtcCode, rtcMsg, nil)
return
}
// 加入聊天室
context.chatController.joinChatroom { chatCode, chatMsg, _ in
guard chatCode == 0 else {
// 加入聊天室失败,离开房间
context.leaveRoom()
callback?(chatCode, chatMsg, nil)
return
}
callback?(chatCode, nil, nil)
}
}
}
// 观众离开房间
self.roomContext.leaveRoom(null);
// 房主结束房间
self.roomContext.endRoom(false,null);
实现一起听
步骤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 | 版权渠道,默认不传则包含所有签约渠道。
|
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 房主播放歌曲
调用 NERoomRtcController.playEffect
播放歌曲。
示例代码如下:
var originPath:String = 本地歌曲路径
//如果使用版权SDK 可通过以下方法获取路径
//NECopyrightedMedia.getInstance()
.getSongURI(songId, channel: channel, songResType: songResType)
if (originPath.length > 0) {
let option = NERoomCreateAudioEffectOption()
option.path = originPath
option.sendEnabled = false
option.playbackEnabled = true
option.sendVolume = 100
option.playbackVolume = 100
option.progressInterval = 100
NSInteger code =
NERoomContext roomContext = NERoomKit.getInstance().getRoomService().getRoomContext(roomUuid);
let code = roomContext.rtcController.playEffect(
effectId: effectId,
option: option
)
步骤5 观众上麦
观众加入房间后,自动上麦并获取歌曲播放信息。
- 观众加入房间后,主播调用
NESeatController.sendSeatInvitation
接口邀请观众上麦。上麦成功后,房间内所有成员收到onSeatInvitationAccepted
的回调和onSeatListChanged
的回调。 - 观众调用业务服务器接口获取当前播放歌曲列表。
- 观众调用
NECopyrightedMedia.preloadSong
接口预加载歌曲。
// 观众进入房间,主播抱麦,观众自动上麦
NERoomContext roomContext = NERoomKit.getInstance().getRoomService().getRoomContext(roomUuid);
if (roomContext!=null){
roomContext!.seatController.sendSeatInvitation(seatIndex,
userUuid: account) { code, msg, _ in
if code == 0 {
} else {
}
callback?(code, msg, nil)
}
}
// 调用业务服务器获取当前播放歌曲和歌曲列表(需自行实现)
// 预加载当前播放歌曲,在加载完后通过点对点自定义消息向主播询问当前播放进度
String songId="your songId";
int channel=1;
if (NECopyrightedMedia.getInstance().isSongPreloaded(songId,channel)){
// 向主播询问当前歌曲播放进度
{
NERoomKit.shared().messageChannelService
.sendCustomMessage(roomUuid: roomUuid,
userUuid: anotherUserUuid,
commandId: 10001,
data: null) { code, msg, _ in
if code == 0 {
} else {
}
callback?(code, msg, nil)
}
}
}else {
NECopyrightedMedia.getInstance().preloadSong(songId, channel: channel, observe: self)
}
//下载完成回调
- (void)onPreloadComplete:(NSString *)songId
channel:(SongChannel)channel
error:(NSError *)error {
// 向主播询问当前歌曲播放进度
[[NEVoiceRoomKit getInstance] sendCustomMessage:anotherUserUuid
commandId:10001
data:""
callback:nil];
}
步骤6 同步一起听的歌曲状态和进度
-
房主通过
NEMessageChannelService.sendCustomMessage
发送定向消息,将播放进度同步给观众。 -
观众调用
NEMessageChannelService.sendCustomMessage
接口获取歌曲播放进度。
// 监听来自主播的自定义消息
NERoomKit.shared().messageChannelService.addMessageChannelListener(listener: self)
/// 接收自定义消息
func onReceiveCustomMessage(message: NECustomMessage) {
guard let dic = message.data.toDictionary() else { return }
guard roomContext != nil else { return }
if message.commandId == 10001 {
//接收到获取进度消息
///发送播放进度
NERoomKit.shared().messageChannelService
.sendCustomMessage(roomUuid: roomUuid,
userUuid: anotherUserUuid,
commandId: 10002,
data: data) { code, msg, _ in
if code == 0 {
//成功
} else {
//失败
}
}
} else if message.commandId == 10002 {
//接受到播放进度消息
//seek
roomContext?.rtcController.setEffectPosition(effectId:EffectId, postion: position)
}
}
进阶功能
暂停或恢复歌曲播放
// 暂停歌曲播放
roomContext.getRtcController().pauseEffect(effectId);
// 恢复歌曲播放
roomContext.getRtcController().resumeEffect(effectId);
切歌
// 停止播放上一首歌
roomContext.getRtcController().stopEffect(effectId);
// 播放下一首歌
let option = NERoomCreateAudioEffectOption()
option.path = "";// 音乐文件路径
option.loopCount = 1
option.sendEnabled = false
option.playbackEnabled = true
option.sendVolume = 100
option.playbackVolume = 100
option.progressInterval = 100
option
.sendWithAudioType = NERoomAudioStreamType(rawValue: sendWithAudioType.rawValue) ?? .main
self.roomContext!.rtcController.playEffect(
effectId: effectId,
option: option
)