实现一起听(基于 NERoom)
更新时间: 2024/11/26 15:44:05
本文介绍如何基于 NERoom 房间组件,在您的 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 及以上版本的真机。  | 
前提条件
示例项目源码
一起听的示例项目源码地址请参见 语聊房 NEChatroom 的示例项目源码。
示例项目的目录结构和跑通示例项目的步骤请参见跑通示例项目。
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)。 - 
房主和连麦观众分别调用
setLocalAudioProfile接口,设置音频profile类型为HighQualityStereo,设置scenario为MUSIC。 - 
连麦观众调用
leaveRoom接口离开一起听房间。 - 
房主调用
endRoom接口结束一起听房间。 
示例代码
// 房主调用自己的业务服务器restful api创建房间。
// 房主或者观众加入房间:
         String roomUuid="123";
            String userName="userName";
            String avatar="your avatar";
            String role="host";// host为房主,audience为观众
            NERoomKit.getInstance().getRoomService().joinRoom(new NEJoinRoomParams(roomUuid,userName,avatar,role,null,new HashMap<>()), new NEJoinRoomOptions(), new NECallback2<NERoomContext>() {
              @Override
              public void onSuccess(@Nullable NERoomContext roomContext) {
                // 设置为直播场景
                  roomContext.getRtcController().setChannelProfile(
                        NERoomRtcChannelProfile.liveBroadcasting
                );
                // 设置音频类型
                roomContext.getRtcController().setLocalAudioProfile(
                        NERoomRtcAudioProfile.HIGH_QUALITY_STEREO,
                        NERoomRtcAudioScenario.MUSIC
                );
                // 加入rtc
                roomContext.getRtcController().joinRtcChannel(null);
                // 加入聊天室
                roomContext.getChatController().joinChatroom(null);
                
              }
              @Override
              public void onError(int code, @Nullable String message) {
                super.onError(code, message);
              }
            });
 // 观众离开房间
 roomContext.leaveRoom(null);
 // 房主结束房间
 roomContext.endRoom(false,null);
实现一起听
步骤1 初始化曲库 SDK
- 
调用
NECopyrightedMedia.getInstance()接口创建版权音乐对象。 示例代码如下:NECopyrightedMedia copyRight = NECopyrightedMedia.getInstance(); - 
调用
NECopyrightedMedia.initialize接口初始化组件。在调用 SDK 初始化接口之前,需要先为应用下的每一位用户生成user账号(account),同时获取对应的 Token。Token 鉴权相关步骤,请参考 曲库动态 Token 鉴权 即可,下文不再赘述。示例代码如下:
NECopyrightedMedia copyRight = NECopyrightedMedia.getInstance(); HashMap<String, Object> extras = new HashMap<>(); copyRight.initialize( context, appkey, token, user, //请传入您的应用账号体系中的账号ID,用于数据统计 extras, new NECopyrightedMedia.Callback<Unit>() { @Override public void success(@Nullable Unit info) { Toast.makeText( context, "init CopyrightedMedia success", Toast.LENGTH_LONG) .show(); } @Override public void error(int code, @Nullable String msg) { Toast.makeText( context, "init CopyrightedMedia fail", Toast.LENGTH_LONG) .show(); } } ); - 
调用
NECopyrightedMedia.setSongScene接口,指定音乐场景为听歌的场景。版权曲库支持听歌场景和 K 歌场景,您需要在初始化曲库 SDK 后指定对应的场景。
// 设置听歌场景 NECopyrightedMedia.getInstance().setSongScene(SongScene.TYPE_LISTENING_TO_MUSIC); 
4.调用 NECopyrightedMedia.setEventHandler  接口注册事件通知回调。
当您的曲库 Token 过期时,会触发 onTokenExpired 回调。此时,您需要参见曲库动态 Token 鉴权生成新的 Token,并调用 renewToken 更新 Token 后才能继续调用 NECopyrightedMedia SDK 的API。示例代码如下:
NECopyrightedMedia.getInstance().setEventHandler(new NECopyrightedEventHandler() {
    @Override
    public void onTokenExpired() {
        // token过期监听回调
    }
});
- 
调用
NECopyrightedMedia.renewToken接口更新Token。示例代码如下:
NECopyrightedMedia.getInstance().renewToken(token);初始化操作完毕,如果能正常调用以下接口,表示初始化成功。
 
步骤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", null, 1, 20, new NECopyrightedMedia.Callback<List<NECopyrightedSong>>() {
@Override
public void success(@Nullable List<NECopyrightedSong> info) {
}
@Override
public void error(int code, @Nullable String msg) {
}
});
// 获取歌曲列表
NECopyrightedMedia.getInstance().getSongList(null, null, 1, 20, new NECopyrightedMedia.Callback<List<NECopyrightedSong>>() {
@Override
public void success(@Nullable List<NECopyrightedSong> info) {
}
@Override
public void error(int code, @Nullable String msg) {
}
});
// 获取热门歌曲列表
NECopyrightedMedia.getInstance().getHotSongList(NECopyrightedHotType.HOTTYPE_DEFAULT, NECopyrightedHotDimension.HOTDIMENSION_APPLICATION, null, 1, 20, new NECopyrightedMedia.Callback<List<NECopyrightedHotSong>>() {
@Override
public void success(@Nullable List<NECopyrightedHotSong> info) {
}
@Override
public void error(int code, @Nullable String msg) {
}
});
步骤3 点歌并下载歌曲
调用  NECopyrightedMedia.preloadSong 接口预加载歌曲,包括原唱、歌词和MIDI。
示例代码如下:
 NECopyrightedMedia.getInstance().preloadSong(songId, channel, new NESongPreloadCallback() {
      @Override
      public void onPreloadStart(String songId, int channel) {
        
      }
      @Override
      public void onPreloadProgress(String songId, int channel, float progress) {
      }
      @Override
      public void onPreloadComplete(String songId, int channel, int errorCode, String msg) {
      }
    });
步骤4 房主播放歌曲
调用  NERoomRtcController.playEffect 播放歌曲。
示例代码如下:
 String path="";// 音乐文件路径
int loopCount=1;
boolean sendEnabled=false;
int sendVolume=100;
boolean playbackEnabled=true;
int playbackVolume=100;
long startTimestamp= 0;
long progressInterval=100;
NERoomRtcAudioStreamType sendWithAudioType=NERoomRtcAudioStreamType.NERtcAudioStreamTypeSub;
roomContext.getRtcController().playEffect(effectId,new NERoomCreateAudioEffectOption(path,loopCount,sendEnabled,sendVolume,playbackEnabled,playbackVolume,startTimestamp,progressInterval,sendWithAudioType));
步骤5 观众上麦
观众加入房间后,自动上麦并获取歌曲播放信息。
- 观众加入房间后,主播调用 
NESeatController.sendSeatInvitation接口邀请观众上麦。上麦成功后,房间内所有成员收到onSeatInvitationAccepted的回调和onSeatListChanged的回调。 - 观众调用业务服务器接口获取当前播放歌曲列表。
 - 观众调用 
NECopyrightedMedia.preloadSong接口预加载歌曲。 
// 观众进入房间,主播抱麦,观众自动上麦
NERoomContext roomContext = NERoomKit.getInstance().getRoomService().getRoomContext(roomUuid);
if (roomContext!=null){
  roomContext.getSeatController().sendSeatInvitation(index, "account", new NECallback2<Unit>() {
    @Override
    public void onSuccess(@Nullable Unit data) {
      super.onSuccess(data);
    }
    @Override
    public void onError(int code, @Nullable String message) {
      super.onError(code, message);
    }
  });
}
// 调用业务服务器获取当前播放歌曲和歌曲列表(需自行实现)
// 预加载当前播放歌曲,在加载完后通过点对点自定义消息向主播询问当前播放进度
String songId="your songId";
int channel=1;
if (NECopyrightedMedia.getInstance().isSongPreloaded(songId,channel)){
  // 向主播询问当前歌曲播放进度
  NERoomKit.instance.getService(NEMessageChannelService.class).sendCustomMessage(roomUuid,anchorUserUuid,10001,"",null);
}else {
  NECopyrightedMedia.getInstance().preloadSong(songId, channel, new NESongPreloadCallback() {
    @Override
    public void onPreloadStart(String songId, int channel) {
    }
    @Override
    public void onPreloadProgress(String songId, int channel, float progress) {
    }
    @Override
    public void onPreloadComplete(String songId, int channel, int errorCode, String msg) {
        // 向主播询问当前歌曲播放进度
  NERoomKit.instance.getService(NEMessageChannelService.class).sendCustomMessage(roomUuid,anchorUserUuid,10001,"",null);
    }
  });
    
步骤6 同步一起听的歌曲状态和进度
- 房主通过 
NEMessageChannelService.sendCustomMessage发送定向消息,将播放进度同步给观众。 - 观众调用 
NEMessageChannelService.sendCustomMessage接口获取歌曲播放进度。 
    
// 监听来自主播的自定义消息
NERoomKit.instance.getService(NEMessageChannelService.class).addMessageChannelListener(new NEMessageChannelListener() {
@Override
public void onReceiveCustomMessage(@NonNull NECustomMessage message) {
if (message.getCommandId()==10002){
  // 接收到来自主播播放进度消息,假设commandId=10002
String data = message.getData(); // 可定义为json格式
// 自定义消息内容,通过解析获取主播的播放进度
String path="";// 音乐文件路径
int loopCount=1;
boolean sendEnabled=false;
int sendVolume=100;
boolean playbackEnabled=true;
int playbackVolume=100;
long startTimestamp= 30;// 主播播放进度
long progressInterval=100;
NERoomRtcAudioStreamType sendWithAudioType=NERoomRtcAudioStreamType.NERtcAudioStreamTypeSub;
roomContext.getRtcController().playEffect(effectId,new NERoomCreateAudioEffectOption(path,loopCount,sendEnabled,sendVolume,playbackEnabled,playbackVolume,startTimestamp,progressInterval,sendWithAudioType));
}else if(commandId=10001){
   // 回复当前播放进度给对方
   String data="d";
NERoomKit.instance.getService(NEMessageChannelService.class).sendCustomMessage(roomUuid,anotherUserUuid,10002,data,null);   
}
}
});
  // 向主播询问当前歌曲播放进度
NERoomKit.instance.getService(NEMessageChannelService.class).sendCustomMessage(roomUuid,anchorUserUuid,10001,"",null);
进阶功能
暂停或恢复歌曲播放
// 暂停歌曲播放
roomContext.getRtcController().pauseEffect(effectId);
// 恢复歌曲播放
roomContext.getRtcController().resumeEffect(effectId);
切歌
// 停止播放上一首歌
roomContext.getRtcController().stopEffect(effectId);
// 播放下一首歌
String path="";// 音乐文件路径
int loopCount=1;
boolean sendEnabled=false;
int sendVolume=100;
boolean playbackEnabled=true;
int playbackVolume=100;
long startTimestamp= 0;
long progressInterval=100;
NERoomRtcAudioStreamType sendWithAudioType=NERoomRtcAudioStreamType.NERtcAudioStreamTypeSub;
roomContext.getRtcController().playEffect(effectId,new NERoomCreateAudioEffectOption(path,loopCount,sendEnabled,sendVolume,playbackEnabled,playbackVolume,startTimestamp,progressInterval,sendWithAudioType));  





