diff --git a/KTVAPI/Android/KTVApi.kt b/KTVAPI/Android/KTVApi.kt new file mode 100644 index 0000000..7caa881 --- /dev/null +++ b/KTVAPI/Android/KTVApi.kt @@ -0,0 +1,443 @@ +package io.agora.ktvapi + +import io.agora.mediaplayer.Constants +import io.agora.mediaplayer.IMediaPlayer +import io.agora.musiccontentcenter.IAgoraMusicContentCenter +import io.agora.musiccontentcenter.Music +import io.agora.musiccontentcenter.MusicChartInfo +import io.agora.rtc2.IRtcEngineEventHandler +import io.agora.rtc2.RtcEngine + +/** + * KTV场景类型 + * @param Normal 普通独唱或多人合唱 + * @param SingBattle 嗨歌抢唱 + */ +enum class KTVType(val value: Int) { + Normal(0), + SingBattle(1) +} + +/** + * 在KTVApi中的身份 + * @param SoloSinger 独唱者: 当前只有自己在唱歌 + * @param CoSinger 合唱者: 加入合唱需要通过调用switchSingerRole将切换身份成合唱 + * @param LeadSinger 主唱: 有合唱者加入后,需要通过调用switchSingerRole切换身份成主唱 + * @param Audience 观众: 默认状态 + */ +enum class KTVSingRole(val value: Int) { + SoloSinger(0), + CoSinger(1), + LeadSinger(2), + Audience(3) +} + +/** + * loadSong失败的原因 + * @param NO_LYRIC_URL 没有歌词,不影响音乐正常播放 + * @param MUSIC_PRELOAD_FAIL 音乐加载失败 + * @param CANCELED 本次加载已终止 + */ +enum class KTVLoadSongFailReason(val value: Int) { + NO_LYRIC_URL(0), + MUSIC_PRELOAD_FAIL(1), + CANCELED(2) +} + +/** + * switchSingerRole的失败的原因 + * @param JOIN_CHANNEL_FAIL 加入channel2失败 + * @param NO_PERMISSION switchSingerRole传入了错误的目标角色(不能从当前角色切换到目标角色) + */ +enum class SwitchRoleFailReason(val value: Int) { + JOIN_CHANNEL_FAIL(0), + NO_PERMISSION(1) +} + +/** + * 加载音乐的模式 + * @param LOAD_MUSIC_ONLY 只加载音乐(通常加入合唱前使用此模式) + * @param LOAD_LRC_ONLY 只加载歌词(通常歌曲开始播放时观众使用此模式) + * @param LOAD_MUSIC_AND_LRC 默认模式,加载歌词和音乐(通常歌曲开始播放时主唱使用此模式) + */ +enum class KTVLoadMusicMode(val value: Int) { + LOAD_NONE(-1), + LOAD_MUSIC_ONLY(0), + LOAD_LRC_ONLY(1), + LOAD_MUSIC_AND_LRC(2) +} + +/** + * 加载音乐的状态 + * @param COMPLETED 加载完成, 进度为100 + * @param FAILED 加载失败 + * @param INPROGRESS 加载中 + */ +enum class MusicLoadStatus(val value: Int) { + COMPLETED(0), + FAILED(1), + INPROGRESS(2), +} + +/** + * 歌词组件接口,您setLrcView传入的歌词组件需要继承此接口类,并实现以下三个方法 + */ +interface ILrcView { + /** + * ktvApi内部更新音高pitch时会主动调用此方法将pitch值传给你的歌词组件 + * @param pitch 音高值 + */ + fun onUpdatePitch(pitch: Float?) + + /** + * ktvApi内部更新音乐播放进度progress时会主动调用此方法将进度值progress传给你的歌词组件,50ms回调一次 + * @param progress 歌曲播放的真实进度 20ms回调一次 + */ + fun onUpdateProgress(progress: Long?) + + /** + * ktvApi获取到歌词地址时会主动调用此方法将歌词地址url传给你的歌词组件,您需要在这个回调内完成歌词的下载 + */ + fun onDownloadLrcData(url: String?) + + /** + * ktvApi获取到抢唱切片歌曲副歌片段时间时,会调用此方法回调给歌词组件 + */ + fun onHighPartTime(highStartTime: Long, highEndTime: Long) +} + +/** + * 音乐加载状态接口 + */ +interface IMusicLoadStateListener { + /** + * 音乐加载成功 + * @param songCode 歌曲编码, 和你loadMusic传入的songCode一致 + * @param lyricUrl 歌词地址 + */ + fun onMusicLoadSuccess(songCode: Long, lyricUrl: String) + + /** + * 音乐加载失败 + * @param reason 歌曲加载失败的原因 + */ + fun onMusicLoadFail(songCode: Long, reason: KTVLoadSongFailReason) + + /** + * 音乐加载进度 + * @param songCode 歌曲编码 + * @param percent 歌曲加载进度 + * @param status 歌曲加载的状态 + * @param msg + * @param lyricUrl + */ + fun onMusicLoadProgress(songCode: Long, percent: Int, status: MusicLoadStatus, msg: String?, lyricUrl: String?) +} + +/** + * 切换演唱角色状态接口 + */ +interface ISwitchRoleStateListener { + /** + * 切换演唱角色成功 + */ + fun onSwitchRoleSuccess() + + /** + * 切换演唱角色失败 + * @param reason 切换演唱角色失败的原因 + */ + fun onSwitchRoleFail(reason: SwitchRoleFailReason) +} + +/** + * KTVApi事件回调 + */ +abstract class IKTVApiEventHandler { + /** + * 播放器状态变化 + * @param state MediaPlayer 播放状态 + * @param error MediaPlayer Error 信息 + * @param isLocal 本地还是主唱端的 Player 信息 + */ + open fun onMusicPlayerStateChanged( + state: Constants.MediaPlayerState, error: Constants.MediaPlayerError, isLocal: Boolean + ) { + } + + /** + * ktvApi内部角色切换 + * @param oldRole 老角色 + * @param newRole 新角色 + */ + open fun onSingerRoleChanged(oldRole: KTVSingRole, newRole: KTVSingRole) {} + + /** + * rtm或合唱频道token将要过期回调,需要renew这个token + */ + open fun onTokenPrivilegeWillExpire() {} + + /** + * 合唱频道人声音量提示 + * @param speakers 不同用户音量信息 + * @param totalVolume 总音量 + */ + open fun onChorusChannelAudioVolumeIndication( + speakers: Array?, + totalVolume: Int) {} +} + +/** + * 初始化KTVApi的配置 + * @param appId 用来初始化 Mcc Engine + * @param rtmToken 创建 Mcc Engine 需要 + * @param engine RTC engine 对象 + * @param channelName 频道号,子频道名以基于主频道名 + "_ex" 固定规则生成频道号 + * @param localUid 创建 Mcc engine 和 加入子频道需要用到 + * @param chorusChannelName 子频道名 加入子频道需要用到 + * @param chorusChannelToken 子频道token 加入子频道需要用到 + * @param maxCacheSize 最大缓存歌曲数 + * @param type KTV场景 + */ +data class KTVApiConfig( + val appId: String, + val rtmToken: String, + val engine: RtcEngine, + val channelName: String, + val localUid: Int, + val chorusChannelName: String, + val chorusChannelToken: String, + val maxCacheSize: Int = 10, + val type: KTVType = KTVType.Normal +) + +/** + * 加载歌曲的配置,不允许在一首歌没有load完成前(成功/失败均算完成)进行下一首歌的加载 + * @param autoPlay 是否自动播放歌曲(通常主唱选择true)默认为false + * @param mode 歌曲加载的模式, 默认为音乐和歌词均加载 + * @param songCode 歌曲 id + * @param mainSingerUid 主唱的 Uid,如果是伴唱,伴唱需要根据这个信息 mute 主频道主唱的声音 + */ +data class KTVLoadMusicConfiguration( + val songIdentifier: String, + val autoPlay: Boolean = false, + val mainSingerUid: Int, + val mode: KTVLoadMusicMode = KTVLoadMusicMode.LOAD_MUSIC_AND_LRC +) + +interface KTVApi { + /** + * 初始化内部变量/缓存数据,并注册相应的监听,必须在其他KTVApi调用前调用initialize初始化KTVApi + * @param config 初始化KTVApi的配置 + */ + fun initialize(config: KTVApiConfig) + + /** + * 更新ktvapi内部使用的streamId,每次加入频道需要更新内部streamId + */ + fun renewInnerDataStreamId() + + /** + * 订阅KTVApi事件, 支持多注册 + * @param ktvApiEventHandler KTVApi事件接口实例 + */ + fun addEventHandler(ktvApiEventHandler: IKTVApiEventHandler) + + /** + * 取消订阅KTVApi事件 + * @param ktvApiEventHandler KTVApi事件接口实例 + */ + fun removeEventHandler(ktvApiEventHandler: IKTVApiEventHandler) + + /** + * 清空内部变量/缓存,取消在initWithRtcEngine时的监听,以及取消网络请求等 + */ + fun release() + + /** + * 收到 IKTVApiEventHandler.onTokenPrivilegeWillExpire 回调时需要主动调用方法更新Token + * @param rtmToken musicContentCenter模块需要的rtm token + * @param chorusChannelRtcToken 合唱需要的频道rtc token + */ + fun renewToken( + rtmToken: String, + chorusChannelRtcToken: String + ) + + /** + * 获取歌曲榜单 + * @param onMusicChartResultListener 榜单列表回调 + */ + fun fetchMusicCharts( + onMusicChartResultListener: ( + requestId: String?, // TODO 不需要? + status: Int, // status=2 时token过期 + list: Array? + ) -> Unit + ) + + /** + * 根据歌曲榜单类型获取歌单 + * @param musicChartId 榜单id + * @param page 歌曲列表回调 + * @param pageSize 歌曲列表回调 + * @param jsonOption 自定义过滤模式 + * @param onMusicCollectionResultListener 歌曲列表回调 + */ + fun searchMusicByMusicChartId( + musicChartId: Int, + page: Int, + pageSize: Int, + jsonOption: String, + onMusicCollectionResultListener: ( + requestId: String?, // TODO 不需要? + status: Int, // status=2 时token过期 + page: Int, + pageSize: Int, + total: Int, + list: Array? + ) -> Unit + ) + + /** + * 根据关键字搜索歌曲 + * @param keyword 关键字 + * @param page 歌曲列表回调 + * @param jsonOption 自定义过滤模式 + * @param onMusicCollectionResultListener 歌曲列表回调 + */ + fun searchMusicByKeyword( + keyword: String, + page: Int, pageSize: Int, + jsonOption: String, + onMusicCollectionResultListener: ( + requestId: String?, // TODO 不需要? + status: Int, // status=2 时token过期 + page: Int, + pageSize: Int, + total: Int, + list: Array? + ) -> Unit + ) + + /** + * 异步加载歌曲,同时只能为一首歌loadSong,loadSong结果会通过回调通知业务层 + * @param songCode 歌曲唯一编码 + * @param config 加载歌曲配置 + * @param musicLoadStateListener 加载歌曲结果回调 + * + * 推荐调用: + * 歌曲开始时: + * 主唱 loadMusic(KTVLoadMusicConfiguration(autoPlay=true, mode=LOAD_MUSIC_AND_LRC, songCode, mainSingerUid)) switchSingerRole(SoloSinger) + * 观众 loadMusic(KTVLoadMusicConfiguration(autoPlay=false, mode=LOAD_LRC_ONLY, songCode, mainSingerUid)) + * 加入合唱时: + * 准备加入合唱者:loadMusic(KTVLoadMusicConfiguration(autoPlay=false, mode=LOAD_MUSIC_ONLY, songCode, mainSingerUid)) + * loadMusic成功后switchSingerRole(CoSinger) + */ + fun loadMusic( + songCode: Long, + config: KTVLoadMusicConfiguration, + musicLoadStateListener: IMusicLoadStateListener + ) + + /** + * 加载歌曲,同时只能为一首歌loadSong,同步调用, 一般使用此loadSong是歌曲已经preload成功(url为本地文件地址) + * @param config 加载歌曲配置 + * @param url 歌曲地址 + * + * 推荐调用: + * 歌曲开始时: + * 主唱 loadMusic(KTVLoadMusicConfiguration(autoPlay=true, mode=LOAD_MUSIC_AND_LRC, url, mainSingerUid)) switchSingerRole(SoloSinger) + * 观众 loadMusic(KTVLoadMusicConfiguration(autoPlay=false, mode=LOAD_LRC_ONLY, url, mainSingerUid)) + * 加入合唱时: + * 准备加入合唱者:loadMusic(KTVLoadMusicConfiguration(autoPlay=false, mode=LOAD_MUSIC_ONLY, url, mainSingerUid)) + * loadMusic成功后switchSingerRole(CoSinger) + */ + fun loadMusic( + url: String, + config: KTVLoadMusicConfiguration + ) + + /** + * 异步切换演唱身份,结果会通过回调通知业务层 + * @param newRole 新演唱身份 + * @param switchRoleStateListener 切换演唱身份结果 + * + * 允许的调用路径: + * 1、Audience -》SoloSinger 自己点的歌播放时 + * 2、Audience -》LeadSinger 自己点的歌播放时, 且歌曲开始时就有合唱者加入 + * 3、SoloSinger -》Audience 独唱结束时 + * 4、Audience -》CoSinger 加入合唱时 + * 5、CoSinger -》Audience 退出合唱时 + * 6、SoloSinger -》LeadSinger 当前第一个合唱者加入合唱时,主唱由独唱切换成领唱 + * 7、LeadSinger -》SoloSinger 最后一个合唱者退出合唱时,主唱由领唱切换成独唱 + * 8、LeadSinger -》Audience 以领唱的身份结束歌曲时 + */ + fun switchSingerRole( + newRole: KTVSingRole, + switchRoleStateListener: ISwitchRoleStateListener? + ) + + /** + * 播放歌曲 + * @param songCode 歌曲唯一编码 + * @param startPos 开始播放的位置 + * 对于主唱: + * 如果loadMusic时你选择了autoPlay = true 则不需要主动调用startSing + * 如果loadMusic时你选择了autoPlay = false 则需要在loadMusic成功后调用startSing + */ + fun startSing(songCode: Long, startPos: Long) + + /** + * 播放歌曲 + * @param url 歌曲地址 + * @param startPos 开始播放的位置 + * 对于主唱: + * 如果loadMusic时你选择了autoPlay = true 则不需要主动调用startSing + * 如果loadMusic时你选择了autoPlay = false 则需要在loadMusic成功后调用startSing + */ + fun startSing(url: String, startPos: Long) + + /** + * 恢复播放 + */ + fun resumeSing() + + /** + * 暂停播放 + */ + fun pauseSing() + + /** + * 调整进度 + */ + fun seekSing(time: Long) + + /** + * 设置歌词组件,在任意时机设置都可以生效 + * @param view 传入的歌词组件view, 需要继承ILrcView并实现ILrcView的三个接口 + */ + fun setLrcView(view: ILrcView) + + /** + * 设置当前mic开关状态 + * 目前关麦调用 adjustRecordSignalVolume(0) 后 onAudioVolumeIndication 仍然会执行, ktvApi需要增加一个变量判断当前是否关麦, 如果关麦把设置给歌词组件的pitch改为0 + */ + fun setMicStatus(isOnMicOpen: Boolean) + + /** + * 设置当前音频播放delay, 适用于音频自采集的情况 + * @param audioPlayoutDelay 音频帧处理和播放的时间差 + */ + fun setAudioPlayoutDelay(audioPlayoutDelay: Int) + + /** + * 获取mpk实例 + */ + fun getMediaPlayer() : IMediaPlayer + + /** + * 获取mcc实例 + */ + fun getMusicContentCenter() : IAgoraMusicContentCenter +} \ No newline at end of file diff --git a/KTVAPI/Android/KTVApiImpl.kt b/KTVAPI/Android/KTVApiImpl.kt new file mode 100644 index 0000000..7862488 --- /dev/null +++ b/KTVAPI/Android/KTVApiImpl.kt @@ -0,0 +1,1223 @@ +package io.agora.ktvapi + +import android.os.Handler +import android.os.Looper +import android.util.Log +import io.agora.mediaplayer.Constants +import io.agora.mediaplayer.Constants.MediaPlayerState +import io.agora.mediaplayer.IMediaPlayer +import io.agora.mediaplayer.IMediaPlayerObserver +import io.agora.mediaplayer.data.PlayerUpdatedInfo +import io.agora.mediaplayer.data.SrcInfo +import io.agora.musiccontentcenter.* +import io.agora.rtc2.* +import io.agora.rtc2.Constants.* +import org.json.JSONException +import org.json.JSONObject +import java.util.concurrent.* + +enum class KTVSongMode(val value: Int) { + SONG_CODE(0), + SONG_URL(1) +} + +/** + * 加入合唱错误原因 + */ +enum class KTVJoinChorusFailReason(val value: Int) { + JOIN_CHANNEL_FAIL(0), // 加入channel2失败 + MUSIC_OPEN_FAIL(1) // 歌曲open失败 +} + +interface OnJoinChorusStateListener { + fun onJoinChorusSuccess() + fun onJoinChorusFail(reason: KTVJoinChorusFailReason) +} + +class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver, + IRtcEngineEventHandler() { + private val TAG: String = "KTV_API_LOG" + + // 外部可修改 + var songMode:KTVSongMode = KTVSongMode.SONG_CODE + var useCustomAudioSource:Boolean = false + + // 音频最佳实践 + var remoteVolume: Int = 40 // 远端音频 + var mpkPlayoutVolume: Int = 50 + var mpkPublishVolume: Int = 50 + + private val mainHandler by lazy { Handler(Looper.getMainLooper()) } + private lateinit var mRtcEngine: RtcEngineEx + private lateinit var mMusicCenter: IAgoraMusicContentCenter + private lateinit var mPlayer: IAgoraMusicPlayer + + private lateinit var ktvApiConfig: KTVApiConfig + private var innerDataStreamId: Int = 0 + private var subChorusConnection: RtcConnection? = null + + private var mainSingerUid: Int = 0 + private var songCode: Long = 0 + private var songUrl: String = "" + private var songIdentifier: String = "" + + private val lyricCallbackMap = + mutableMapOf Unit>() // (requestId, callback) + private val lyricSongCodeMap = mutableMapOf() // (requestId, songCode) + private val loadMusicCallbackMap = + mutableMapOf Unit>() // (songNo, callback) + private val musicChartsCallbackMap = + mutableMapOf?) -> Unit>() + private val musicCollectionCallbackMap = + mutableMapOf?) -> Unit>() + + private var lrcView: ILrcView? = null + + private var localPlayerPosition: Long = 0 + private var localPlayerSystemTime: Long = 0 + + //歌词实时刷新 + private var mStopDisplayLrc = true + private var mReceivedPlayPosition: Long = 0 //播放器播放position,ms + private var mLastReceivedPlayPosTime: Long? = null + + // event + private var ktvApiEventHandlerList = mutableListOf() + private var mainSingerHasJoinChannelEx: Boolean = false + + // 合唱校准 + private var audioPlayoutDelay = 0 + + // 音高 + private var pitch = 0.0 + + // 是否在麦上 + private var isOnMicOpen = false + private var isRelease = false + + // mpk状态 + private var mediaPlayerState: MediaPlayerState = MediaPlayerState.PLAYER_STATE_IDLE + + companion object{ + private val scheduledThreadPool: ScheduledExecutorService = Executors.newScheduledThreadPool(5) + } + + override fun initialize( + config: KTVApiConfig + ) { + this.mRtcEngine = config.engine as RtcEngineEx + this.ktvApiConfig = config + + // ------------------ 初始化内容中心 ------------------ + val contentCenterConfiguration = MusicContentCenterConfiguration() + contentCenterConfiguration.appId = config.appId + contentCenterConfiguration.mccUid = ktvApiConfig.localUid.toLong() + contentCenterConfiguration.token = config.rtmToken + contentCenterConfiguration.maxCacheSize = config.maxCacheSize + mMusicCenter = IAgoraMusicContentCenter.create(mRtcEngine) + mMusicCenter.initialize(contentCenterConfiguration) + + // ------------------ 初始化音乐播放器实例 ------------------ + mPlayer = mMusicCenter.createMusicPlayer() + mPlayer.adjustPublishSignalVolume(mpkPublishVolume) + mPlayer.adjustPlayoutVolume(mpkPlayoutVolume) + + // 注册回调 + mRtcEngine.addHandler(this) + mPlayer.registerPlayerObserver(this) + mMusicCenter.registerEventHandler(this) + + setKTVParameters() + startDisplayLrc() + startSyncPitch() + isRelease = false + } + + override fun renewInnerDataStreamId() { + val innerCfg = DataStreamConfig() + innerCfg.syncWithAudio = true + innerCfg.ordered = false + this.innerDataStreamId = mRtcEngine.createDataStream(innerCfg) + } + + private fun setKTVParameters() { + mRtcEngine.setParameters("{\"rtc.enable_nasa2\": false}") + mRtcEngine.setParameters("{\"rtc.ntp_delay_drop_threshold\":1000}") + mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp\": true}") + mRtcEngine.setParameters("{\"rtc.net.maxS2LDelay\": 800}") + mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":true}") + + mRtcEngine.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}") + mRtcEngine.setParameters("{\"che.audio.neteq.targetlevel_offset\": 20}") + + mRtcEngine.setParameters("{\"rtc.net.maxS2LDelayBroadcast\":400}") + mRtcEngine.setParameters("{\"che.audio.neteq.prebuffer\":true}") + mRtcEngine.setParameters("{\"che.audio.neteq.prebuffer_max_delay\":600}") + mRtcEngine.setParameters("{\"che.audio.max_mixed_participants\": 8}") + mRtcEngine.setParameters("{\"che.audio.custom_bitrate\": 48000}") + mRtcEngine.setParameters("{\"che.audio.direct.uplink_process\": false}") + mRtcEngine.setParameters("{\"che.audio.uplink_apm_async_process\": true}") + + // Android Only + mRtcEngine.setParameters("{\"che.audio.enable_estimated_device_delay\":false}") + } + + override fun addEventHandler(ktvApiEventHandler: IKTVApiEventHandler) { + ktvApiEventHandlerList.add(ktvApiEventHandler) + } + + override fun removeEventHandler(ktvApiEventHandler: IKTVApiEventHandler) { + ktvApiEventHandlerList.remove(ktvApiEventHandler) + } + + override fun release() { + if (isRelease) return + isRelease = true + singerRole = KTVSingRole.Audience + + stopSyncPitch() + stopDisplayLrc() + this.mLastReceivedPlayPosTime = null + this.mReceivedPlayPosition = 0 + this.innerDataStreamId = 0 + + lyricCallbackMap.clear() + loadMusicCallbackMap.clear() + musicChartsCallbackMap.clear() + musicCollectionCallbackMap.clear() + lrcView = null + + mRtcEngine.removeHandler(this) + mPlayer.unRegisterPlayerObserver(this) + mMusicCenter.unregisterEventHandler() + + mPlayer.stop() + mPlayer.destroy() + IAgoraMusicContentCenter.destroy() + + mainSingerHasJoinChannelEx = false + } + + override fun renewToken(rtmToken: String, chorusChannelRtcToken: String) { + // 更新RtmToken + mMusicCenter.renewToken(rtmToken) + // 更新合唱频道RtcToken + if (subChorusConnection != null) { + val channelMediaOption = ChannelMediaOptions() + channelMediaOption.token = chorusChannelRtcToken + mRtcEngine.updateChannelMediaOptionsEx(channelMediaOption, subChorusConnection) + } + } + + // 1、Audience -》SoloSinger + // 2、Audience -》LeadSinger + // 3、SoloSinger -》Audience + // 4、Audience -》CoSinger + // 5、CoSinger -》Audience + // 6、SoloSinger -》LeadSinger + // 7、LeadSinger -》SoloSinger + // 8、LeadSinger -》Audience + var singerRole: KTVSingRole = KTVSingRole.Audience + override fun switchSingerRole( + newRole: KTVSingRole, + switchRoleStateListener: ISwitchRoleStateListener? + ) { + Log.d(TAG, "switchSingerRole oldRole: $singerRole, newRole: $newRole") + val oldRole = singerRole + if (this.singerRole == KTVSingRole.Audience && newRole == KTVSingRole.SoloSinger) { + // 1、Audience -》SoloSinger + this.singerRole = newRole + becomeSoloSinger() + ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) } + switchRoleStateListener?.onSwitchRoleSuccess() + } else if (this.singerRole == KTVSingRole.Audience && newRole == KTVSingRole.LeadSinger) { + // 2、Audience -》LeadSinger + becomeSoloSinger() + joinChorus(newRole, ktvApiConfig.chorusChannelToken, object : OnJoinChorusStateListener { + override fun onJoinChorusSuccess() { + Log.d(TAG, "onJoinChorusSuccess") + singerRole = newRole + ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) } + switchRoleStateListener?.onSwitchRoleSuccess() + } + + override fun onJoinChorusFail(reason: KTVJoinChorusFailReason) { + Log.d(TAG, "onJoinChorusFail reason:$reason") + leaveChorus(newRole) + switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.JOIN_CHANNEL_FAIL) + } + }) + } else if (this.singerRole == KTVSingRole.SoloSinger && newRole == KTVSingRole.Audience) { + // 3、SoloSinger -》Audience + + stopSing() + this.singerRole = newRole + ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) } + switchRoleStateListener?.onSwitchRoleSuccess() + + } else if (this.singerRole == KTVSingRole.Audience && newRole == KTVSingRole.CoSinger) { + // 4、Audience -》CoSinger + joinChorus(newRole, ktvApiConfig.chorusChannelToken, object : OnJoinChorusStateListener { + override fun onJoinChorusSuccess() { + Log.d(TAG, "onJoinChorusSuccess") + singerRole = newRole + switchRoleStateListener?.onSwitchRoleSuccess() + ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) } + } + + override fun onJoinChorusFail(reason: KTVJoinChorusFailReason) { + Log.d(TAG, "onJoinChorusFail reason:$reason") + leaveChorus(newRole) + switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.JOIN_CHANNEL_FAIL) + } + }) + + } else if (this.singerRole == KTVSingRole.CoSinger && newRole == KTVSingRole.Audience) { + // 5、CoSinger -》Audience + leaveChorus(singerRole) + + this.singerRole = newRole + ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) } + switchRoleStateListener?.onSwitchRoleSuccess() + + } else if (this.singerRole == KTVSingRole.SoloSinger && newRole == KTVSingRole.LeadSinger) { + // 6、SoloSinger -》LeadSinger + + joinChorus(newRole, ktvApiConfig.chorusChannelToken, object : OnJoinChorusStateListener { + override fun onJoinChorusSuccess() { + Log.d(TAG, "onJoinChorusSuccess") + singerRole = newRole + switchRoleStateListener?.onSwitchRoleSuccess() + ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) } + } + + override fun onJoinChorusFail(reason: KTVJoinChorusFailReason) { + Log.d(TAG, "onJoinChorusFail reason:$reason") + leaveChorus(newRole) + switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.JOIN_CHANNEL_FAIL) + } + }) + } else if (this.singerRole == KTVSingRole.LeadSinger && newRole == KTVSingRole.SoloSinger) { + // 7、LeadSinger -》SoloSinger + leaveChorus(singerRole) + + this.singerRole = newRole + ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) } + switchRoleStateListener?.onSwitchRoleSuccess() + } else if (this.singerRole == KTVSingRole.LeadSinger && newRole == KTVSingRole.Audience) { + // 8、LeadSinger -》Audience + leaveChorus(singerRole) + stopSing() + + this.singerRole = newRole + ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) } + switchRoleStateListener?.onSwitchRoleSuccess() + } else { + switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.NO_PERMISSION) + Log.e(TAG, "Error!You can not switch role from $singerRole to $newRole!") + } + } + + override fun fetchMusicCharts(onMusicChartResultListener: (requestId: String?, status: Int, list: Array?) -> Unit) { + val requestId = mMusicCenter.musicCharts + musicChartsCallbackMap[requestId] = onMusicChartResultListener + } + + override fun searchMusicByMusicChartId( + musicChartId: Int, + page: Int, + pageSize: Int, + jsonOption: String, + onMusicCollectionResultListener: (requestId: String?, status: Int, page: Int, pageSize: Int, total: Int, list: Array?) -> Unit + ) { + val requestId = + mMusicCenter.getMusicCollectionByMusicChartId(musicChartId, page, pageSize, jsonOption) + musicCollectionCallbackMap[requestId] = onMusicCollectionResultListener + } + + override fun searchMusicByKeyword( + keyword: String, + page: Int, + pageSize: Int, + jsonOption: String, + onMusicCollectionResultListener: (requestId: String?, status: Int, page: Int, pageSize: Int, total: Int, list: Array?) -> Unit + ) { + val requestId = mMusicCenter.searchMusic(keyword, page, pageSize, jsonOption) + musicCollectionCallbackMap[requestId] = onMusicCollectionResultListener + } + + override fun loadMusic( + songCode: Long, + config: KTVLoadMusicConfiguration, + musicLoadStateListener: IMusicLoadStateListener + ) { + Log.d(TAG, "loadMusic called: songCode $songCode") + if (this.ktvApiConfig.type == KTVType.SingBattle) { + mMusicCenter.getSongSimpleInfo(songCode); + } + // 设置到全局, 连续调用以最新的为准 + this.songMode = KTVSongMode.SONG_CODE + this.songCode = songCode + this.songIdentifier = config.songIdentifier + this.mainSingerUid = config.mainSingerUid + mLastReceivedPlayPosTime = null + mReceivedPlayPosition = 0 + + if (config.mode == KTVLoadMusicMode.LOAD_NONE) { + return + } + + if (config.mode == KTVLoadMusicMode.LOAD_LRC_ONLY) { + // 只加载歌词 + loadLyric(songCode) { song, lyricUrl -> + if (this.songCode != song) { + // 当前歌曲已发生变化,以最新load歌曲为准 + Log.e(TAG, "loadMusic failed: CANCELED") + musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.CANCELED) + return@loadLyric + } + + if (lyricUrl == null) { + // 加载歌词失败 + Log.e(TAG, "loadMusic failed: NO_LYRIC_URL") + musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.NO_LYRIC_URL) + } else { + // 加载歌词成功 + Log.d(TAG, "loadMusic success") + lrcView?.onDownloadLrcData(lyricUrl) + musicLoadStateListener.onMusicLoadSuccess(song, lyricUrl) + } + } + return + } + + // 预加载歌曲 + preLoadMusic(songCode) { song, percent, status, msg, lrcUrl -> + if (status == 0) { + // 预加载歌曲成功 + if (this.songCode != song) { + // 当前歌曲已发生变化,以最新load歌曲为准 + Log.e(TAG, "loadMusic failed: CANCELED") + musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.CANCELED) + return@preLoadMusic + } + if (config.mode == KTVLoadMusicMode.LOAD_MUSIC_AND_LRC) { + // 需要加载歌词 + loadLyric(song) { _, lyricUrl -> + if (this.songCode != song) { + // 当前歌曲已发生变化,以最新load歌曲为准 + Log.e(TAG, "loadMusic failed: CANCELED") + musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.CANCELED) + return@loadLyric + } + + if (lyricUrl == null) { + // 加载歌词失败 + Log.e(TAG, "loadMusic failed: NO_LYRIC_URL") + musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.NO_LYRIC_URL) + } else { + // 加载歌词成功 + Log.d(TAG, "loadMusic success") + lrcView?.onDownloadLrcData(lyricUrl) + musicLoadStateListener.onMusicLoadProgress(song, 100, MusicLoadStatus.COMPLETED, msg, lrcUrl) + musicLoadStateListener.onMusicLoadSuccess(song, lyricUrl) + } + + if (config.autoPlay) { + // 主唱自动播放歌曲 + if (this.singerRole != KTVSingRole.LeadSinger) { + switchSingerRole(KTVSingRole.SoloSinger, null) + } + startSing(song, 0) + } + } + } else if (config.mode == KTVLoadMusicMode.LOAD_MUSIC_ONLY) { + // 不需要加载歌词 + Log.d(TAG, "loadMusic success") + if (config.autoPlay) { + // 主唱自动播放歌曲 + if (this.singerRole != KTVSingRole.LeadSinger) { + switchSingerRole(KTVSingRole.SoloSinger, null) + } + startSing(song, 0) + } + musicLoadStateListener.onMusicLoadProgress(song, 100, MusicLoadStatus.COMPLETED, msg, lrcUrl) + musicLoadStateListener.onMusicLoadSuccess(song, "") + } + } else if (status == 2) { + // 预加载歌曲加载中 + musicLoadStateListener.onMusicLoadProgress(song, percent, MusicLoadStatus.values().firstOrNull { it.value == status } ?: MusicLoadStatus.FAILED, msg, lrcUrl) + } else { + // 预加载歌曲失败 + Log.e(TAG, "loadMusic failed: MUSIC_PRELOAD_FAIL") + musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.MUSIC_PRELOAD_FAIL) + } + } + } + + override fun loadMusic( + url: String, + config: KTVLoadMusicConfiguration + ) { + Log.d(TAG, "loadMusic called: songCode $songCode") + this.songMode = KTVSongMode.SONG_URL + this.songIdentifier = config.songIdentifier + this.songUrl = url + this.mainSingerUid = config.mainSingerUid + + if (config.autoPlay) { + // 主唱自动播放歌曲 + if (this.singerRole != KTVSingRole.LeadSinger) { + switchSingerRole(KTVSingRole.SoloSinger, null) + } + startSing(url, 0) + } + } + + override fun startSing(songCode: Long, startPos: Long) { + Log.d(TAG, "playSong called: $singerRole") + if (this.songCode != songCode) { + Log.e(TAG, "startSing failed: canceled") + return + } + mRtcEngine.adjustPlaybackSignalVolume(remoteVolume) + mPlayer.open(songCode, startPos) + } + + override fun startSing(url: String, startPos: Long) { + Log.d(TAG, "playSong called: $singerRole") + if (this.songUrl != url) { + Log.e(TAG, "startSing failed: canceled") + return + } + mRtcEngine.adjustPlaybackSignalVolume(remoteVolume) + mPlayer.open(url, startPos) + } + + override fun resumeSing() { + Log.d(TAG, "resumePlay called") + mPlayer.resume() + } + + override fun pauseSing() { + Log.d(TAG, "pausePlay called") + mPlayer.pause() + } + + override fun seekSing(time: Long) { + Log.d(TAG, "seek called") + mPlayer.seek(time) + syncPlayProgress(time) + } + + override fun setLrcView(view: ILrcView) { + Log.d(TAG, "setLrcView called") + this.lrcView = view + } + + override fun setMicStatus(isOnMicOpen: Boolean) { + this.isOnMicOpen = isOnMicOpen + } + + override fun setAudioPlayoutDelay(audioPlayoutDelay: Int) { + this.audioPlayoutDelay = audioPlayoutDelay + } + + override fun getMediaPlayer(): IMediaPlayer { + return mPlayer + } + + override fun getMusicContentCenter(): IAgoraMusicContentCenter { + return mMusicCenter + } + + // ------------------ inner KTVApi -------------------- + private fun becomeSoloSinger() { + Log.d(TAG, "becomeSoloSinger called") + // 主唱进入合唱模式 + mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}") + mRtcEngine.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}") + mRtcEngine.setParameters("{\"che.audio.custom_bitrate\": 80000}") + mRtcEngine.setAudioScenario(AUDIO_SCENARIO_CHORUS) + + val channelMediaOption = ChannelMediaOptions() + channelMediaOption.autoSubscribeAudio = true + channelMediaOption.publishMediaPlayerId = mPlayer.mediaPlayerId + channelMediaOption.publishMediaPlayerAudioTrack = true + mRtcEngine.updateChannelMediaOptions(channelMediaOption) + + } + + private fun joinChorus(newRole: KTVSingRole, token: String, onJoinChorusStateListener: OnJoinChorusStateListener) { + Log.d(TAG, "joinChorus: $newRole") + when (newRole) { + KTVSingRole.LeadSinger -> { + joinChorus2ndChannel(newRole, token, mainSingerUid) { joinStatus -> + if (joinStatus == 0) { + onJoinChorusStateListener.onJoinChorusSuccess() + } else { + onJoinChorusStateListener.onJoinChorusFail(KTVJoinChorusFailReason.JOIN_CHANNEL_FAIL) + } + } + } + KTVSingRole.CoSinger -> { + val channelMediaOption = ChannelMediaOptions() + channelMediaOption.autoSubscribeAudio = true + channelMediaOption.publishMediaPlayerAudioTrack = false + mRtcEngine.updateChannelMediaOptions(channelMediaOption) + + // 预加载歌曲成功 + if (songMode == KTVSongMode.SONG_CODE) { + mPlayer.open(songCode, 0) // TODO open failed + } else { + mPlayer.open(songUrl, 0) // TODO open failed + } + + // 预加载成功后加入第二频道:预加载时间>>joinChannel时间 + joinChorus2ndChannel(newRole, token, mainSingerUid) { joinStatus -> + if (joinStatus == 0) { + // 加入第二频道成功 + onJoinChorusStateListener.onJoinChorusSuccess() + } else { + // 加入第二频道失败 + onJoinChorusStateListener.onJoinChorusFail(KTVJoinChorusFailReason.JOIN_CHANNEL_FAIL) + } + } + } + else -> { + Log.e(TAG, "JoinChorus with Wrong role: $singerRole") + } + } + } + + private fun leaveChorus(role: KTVSingRole) { + Log.d(TAG, "leaveChorus: $singerRole") + when (role) { + KTVSingRole.LeadSinger -> { + mainSingerHasJoinChannelEx = false + leaveChorus2ndChannel(role) + } + KTVSingRole.CoSinger -> { + mPlayer.stop() + val channelMediaOption = ChannelMediaOptions() + channelMediaOption.autoSubscribeAudio = true + channelMediaOption.publishMediaPlayerAudioTrack = false + mRtcEngine.updateChannelMediaOptions(channelMediaOption) + leaveChorus2ndChannel(role) + mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":true}") + mRtcEngine.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}") + mRtcEngine.setParameters("{\"che.audio.custom_bitrate\": 48000}") + mRtcEngine.setAudioScenario(AUDIO_SCENARIO_GAME_STREAMING) + } + else -> { + Log.e(TAG, "JoinChorus with wrong role: $singerRole") + } + } + } + + private fun stopSing() { + Log.d(TAG, "stopSong called") + + val channelMediaOption = ChannelMediaOptions() + channelMediaOption.autoSubscribeAudio = true + channelMediaOption.publishMediaPlayerAudioTrack = false + mRtcEngine.updateChannelMediaOptions(channelMediaOption) + + mPlayer.stop() + mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":true}") + mRtcEngine.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}") + mRtcEngine.setParameters("{\"che.audio.custom_bitrate\": 48000}") + mRtcEngine.setAudioScenario(AUDIO_SCENARIO_GAME_STREAMING) + } + + // ------------------ inner -------------------- + + private fun isChorusCoSinger(): Boolean { + return singerRole == KTVSingRole.CoSinger + } + + private fun sendStreamMessageWithJsonObject( + obj: JSONObject, + success: (isSendSuccess: Boolean) -> Unit + ) { + val ret = mRtcEngine.sendStreamMessage(innerDataStreamId, obj.toString().toByteArray()) + if (ret == 0) { + success.invoke(true) + } else { + Log.e(TAG, "sendStreamMessageWithJsonObject failed: $ret") + } + } + + private fun syncPlayState( + state: Constants.MediaPlayerState, + error: Constants.MediaPlayerError + ) { + val msg: MutableMap = HashMap() + msg["cmd"] = "PlayerState" + msg["state"] = Constants.MediaPlayerState.getValue(state) + msg["error"] = Constants.MediaPlayerError.getValue(error) + val jsonMsg = JSONObject(msg) + sendStreamMessageWithJsonObject(jsonMsg) {} + } + + private fun syncPlayProgress(time: Long) { + val msg: MutableMap = HashMap() + msg["cmd"] = "Seek" + msg["position"] = time + val jsonMsg = JSONObject(msg) + sendStreamMessageWithJsonObject(jsonMsg) {} + } + + // 合唱 + private fun joinChorus2ndChannel( + newRole: KTVSingRole, + token: String, + mainSingerUid: Int, + onJoinChorus2ndChannelCallback: (status: Int?) -> Unit + ) { + Log.d(TAG, "joinChorus2ndChannel: token:$token") + if (newRole == KTVSingRole.SoloSinger || newRole == KTVSingRole.Audience) { + Log.e(TAG, "joinChorus2ndChannel with wrong role: $newRole") + return + } + + if (newRole == KTVSingRole.CoSinger) { + mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}") + mRtcEngine.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}") + mRtcEngine.setParameters("{\"che.audio.custom_bitrate\": 48000}") + mRtcEngine.setAudioScenario(AUDIO_SCENARIO_CHORUS) + } + + // main singer do not subscribe 2nd channel + // co singer auto sub + val channelMediaOption = ChannelMediaOptions() + channelMediaOption.autoSubscribeAudio = + newRole != KTVSingRole.LeadSinger + channelMediaOption.autoSubscribeVideo = false + channelMediaOption.publishMicrophoneTrack = newRole == KTVSingRole.LeadSinger + channelMediaOption.enableAudioRecordingOrPlayout = + newRole != KTVSingRole.LeadSinger + channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER + + val rtcConnection = RtcConnection() + rtcConnection.channelId = ktvApiConfig.chorusChannelName + rtcConnection.localUid = ktvApiConfig.localUid + subChorusConnection = rtcConnection + + val ret = mRtcEngine.joinChannelEx( + token, + rtcConnection, + channelMediaOption, + object : IRtcEngineEventHandler() { + override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { + Log.d(TAG, "onJoinChannel2Success: channel:$channel, uid:$uid") + if (isRelease) return + super.onJoinChannelSuccess(channel, uid, elapsed) + if (newRole == KTVSingRole.LeadSinger) { + mainSingerHasJoinChannelEx = true + } + onJoinChorus2ndChannelCallback(0) + mRtcEngine.enableAudioVolumeIndicationEx(50, 10, true, rtcConnection) + } + + override fun onLeaveChannel(stats: RtcStats?) { + Log.d(TAG, "onLeaveChannel2") + if (isRelease) return + super.onLeaveChannel(stats) + if (newRole == KTVSingRole.LeadSinger) { + mainSingerHasJoinChannelEx = false + } + } + + override fun onError(err: Int) { + super.onError(err) + if (isRelease) return + if (err == ERR_JOIN_CHANNEL_REJECTED) { + Log.e(TAG, "joinChorus2ndChannel failed: ERR_JOIN_CHANNEL_REJECTED") + onJoinChorus2ndChannelCallback(ERR_JOIN_CHANNEL_REJECTED) + } else if (err == ERR_LEAVE_CHANNEL_REJECTED) { + Log.e(TAG, "leaveChorus2ndChannel failed: ERR_LEAVE_CHANNEL_REJECTED") + } + } + + override fun onTokenPrivilegeWillExpire(token: String?) { + super.onTokenPrivilegeWillExpire(token) + ktvApiEventHandlerList.forEach { it.onTokenPrivilegeWillExpire() } + } + + override fun onAudioVolumeIndication( + speakers: Array?, + totalVolume: Int + ) { + super.onAudioVolumeIndication(speakers, totalVolume) + ktvApiEventHandlerList.forEach { it.onChorusChannelAudioVolumeIndication(speakers, totalVolume) } + } + } + ) + + if (ret != 0) { + Log.e(TAG, "joinChorus2ndChannel failed: $ret") + } + + if (newRole == KTVSingRole.CoSinger) { + mRtcEngine.muteRemoteAudioStream(mainSingerUid, true) + Log.d(TAG, "muteRemoteAudioStream$mainSingerUid") + } + } + + private fun leaveChorus2ndChannel(role: KTVSingRole) { + if (role == KTVSingRole.LeadSinger) { + mRtcEngine.leaveChannelEx(subChorusConnection) + } else if (role == KTVSingRole.CoSinger) { + mRtcEngine.leaveChannelEx(subChorusConnection) + mRtcEngine.muteRemoteAudioStream(mainSingerUid, false) + } + } + + // ------------------ 歌词播放、同步 ------------------ + // 开始播放歌词 + + private val displayLrcTask = object : Runnable { + override fun run() { + if (!mStopDisplayLrc){ + val lastReceivedTime = mLastReceivedPlayPosTime ?: return + val curTime = System.currentTimeMillis() + val offset = curTime - lastReceivedTime + if (offset <= 1000) { + val curTs = mReceivedPlayPosition + offset + highStartTime + runOnMainThread { + lrcView?.onUpdatePitch(pitch.toFloat()) + // (fix ENT-489)Make lyrics delay for 200ms + // Per suggestion from Bob, it has a intrinsic buffer/delay between sound and `onPositionChanged(Player)`, + // such as AEC/Player/Device buffer. + // We choose the estimated 200ms. + lrcView?.onUpdateProgress(if (curTs > 200) (curTs - 200) else curTs) // The delay here will impact both singer and audience side + } + } + } + } + } + + private var displayLrcFuture: ScheduledFuture<*>? = null + private fun startDisplayLrc() { + Log.d(TAG, "startDisplayLrc called") + mStopDisplayLrc = false + displayLrcFuture = scheduledThreadPool.scheduleAtFixedRate(displayLrcTask, 0,20, TimeUnit.MILLISECONDS) + } + + // 停止播放歌词 + private fun stopDisplayLrc() { + Log.d(TAG, "stopDisplayLrc called") + mStopDisplayLrc = true + displayLrcFuture?.cancel(true) + displayLrcFuture = null + if (scheduledThreadPool is ScheduledThreadPoolExecutor) { + scheduledThreadPool.remove(displayLrcTask) + } + } + + // ------------------ 音高pitch同步 ------------------ +// private var mSyncPitchThread: Thread? = null + private var mStopSyncPitch = true + + private val mSyncPitchTask = Runnable { + if (!mStopSyncPitch) { + if (mediaPlayerState == MediaPlayerState.PLAYER_STATE_PLAYING && + (singerRole == KTVSingRole.LeadSinger || singerRole == KTVSingRole.SoloSinger)) { + sendSyncPitch(pitch) + } + } + } + + private fun sendSyncPitch(pitch: Double) { + val msg: MutableMap = java.util.HashMap() + msg["cmd"] = "setVoicePitch" + msg["pitch"] = pitch + val jsonMsg = JSONObject(msg) + sendStreamMessageWithJsonObject(jsonMsg) {} + } + + // 开始同步音高 + private var mSyncPitchFuture :ScheduledFuture<*>? = null + private fun startSyncPitch() { + mStopSyncPitch = false + mSyncPitchFuture = scheduledThreadPool.scheduleAtFixedRate(mSyncPitchTask,0,50,TimeUnit.MILLISECONDS) + } + + // 停止同步音高 + private fun stopSyncPitch() { + mStopSyncPitch = true + pitch = 0.0 + + mSyncPitchFuture?.cancel(true) + mSyncPitchFuture = null + if (scheduledThreadPool is ScheduledThreadPoolExecutor) { + scheduledThreadPool.remove(mSyncPitchTask) + } + } + + private fun loadLyric(songNo: Long, onLoadLyricCallback: (songNo: Long, lyricUrl: String?) -> Unit) { + Log.d(TAG, "loadLyric: $songNo") + val requestId = mMusicCenter.getLyric(songNo, 0) + if (requestId.isEmpty()) { + onLoadLyricCallback.invoke(songNo, null) + return + } + lyricSongCodeMap[requestId] = songNo + lyricCallbackMap[requestId] = onLoadLyricCallback + } + + private fun preLoadMusic(songNo: Long, onLoadMusicCallback: (songCode: Long, + percent: Int, + status: Int, + msg: String?, + lyricUrl: String?) -> Unit) { + Log.d(TAG, "loadMusic: $songNo") + val ret = mMusicCenter.isPreloaded(songNo) + if (ret == 0) { + loadMusicCallbackMap.remove(songNo.toString()) + onLoadMusicCallback(songNo, 100, 0, null, null) + return + } + + val retPreload = mMusicCenter.preload(songNo, null) + if (retPreload != 0) { + Log.e(TAG, "preLoadMusic failed: $retPreload") + loadMusicCallbackMap.remove(songNo.toString()) + onLoadMusicCallback(songNo, 100, 1, null, null) + return + } + loadMusicCallbackMap[songNo.toString()] = onLoadMusicCallback + } + + private fun getNtpTimeInMs(): Long { + val currentNtpTime = mRtcEngine.ntpWallTimeInMs + return if (currentNtpTime != 0L) { + currentNtpTime + 2208988800L * 1000 + } else { + Log.e(TAG, "getNtpTimeInMs DeviceDelay is zero!!!") + System.currentTimeMillis() + } + } + + private fun runOnMainThread(r: Runnable) { + if (Thread.currentThread() == mainHandler.looper.thread) { + r.run() + } else { + mainHandler.post(r) + } + } + + // ------------------------ AgoraRtcEvent ------------------------ + override fun onStreamMessage(uid: Int, streamId: Int, data: ByteArray?) { + super.onStreamMessage(uid, streamId, data) + val jsonMsg: JSONObject + val messageData = data ?: return + try { + val strMsg = String(messageData) + jsonMsg = JSONObject(strMsg) + if (jsonMsg.getString("cmd") == "setLrcTime") { //同步歌词 + val position = jsonMsg.getLong("time") + val realPosition = jsonMsg.getLong("realTime") + val duration = jsonMsg.getLong("duration") + val remoteNtp = jsonMsg.getLong("ntp") + val songId = jsonMsg.getString("songIdentifier") + val mpkState = jsonMsg.getInt("playerState") + + if (isChorusCoSinger()) { + // 本地BGM校准逻辑 + if (this.mediaPlayerState == MediaPlayerState.PLAYER_STATE_OPEN_COMPLETED) { + // 合唱者开始播放音乐前调小远端人声 + mRtcEngine.adjustPlaybackSignalVolume(remoteVolume) + // 收到leadSinger第一次播放位置消息时开启本地播放(先通过seek校准) + val delta = getNtpTimeInMs() - remoteNtp + val expectPosition = position + delta + audioPlayoutDelay + if (expectPosition in 1 until duration) { + mPlayer.seek(expectPosition) + } + mPlayer.play() + } else if (this.mediaPlayerState == MediaPlayerState.PLAYER_STATE_PLAYING) { + val localNtpTime = getNtpTimeInMs() + val localPosition = + localNtpTime - this.localPlayerSystemTime + this.localPlayerPosition // 当前副唱的播放时间 + val expectPosition = + localNtpTime - remoteNtp + position + audioPlayoutDelay // 实际主唱的播放时间 + val diff = expectPosition - localPosition + Log.d(TAG,"play_status_seek: " + diff + " localNtpTime: " + localNtpTime + " expectPosition: " + expectPosition + + " localPosition: " + localPosition + " ntp diff: " + (localNtpTime - remoteNtp)) + if ((diff > 50 || diff < -50) && expectPosition < duration) { //设置阈值为50ms,避免频繁seek + mPlayer.seek(expectPosition) + } + } else { + mLastReceivedPlayPosTime = System.currentTimeMillis() + mReceivedPlayPosition = realPosition + } + + if (MediaPlayerState.getStateByValue(mpkState) != this.mediaPlayerState) { + when (MediaPlayerState.getStateByValue(mpkState)) { + MediaPlayerState.PLAYER_STATE_PAUSED -> { + mPlayer.pause() + } + MediaPlayerState.PLAYER_STATE_PLAYING -> { + mPlayer.resume() + } + else -> {} + } + } + } else { + // 独唱观众 + if (this.songIdentifier == songId) { + mLastReceivedPlayPosTime = System.currentTimeMillis() + mReceivedPlayPosition = realPosition + } else { + mLastReceivedPlayPosTime = null + mReceivedPlayPosition = 0 + } + } + } else if (jsonMsg.getString("cmd") == "Seek") { + // 伴唱收到原唱seek指令 + if (isChorusCoSinger()) { + val position = jsonMsg.getLong("position") + mPlayer.seek(position) + } + } else if (jsonMsg.getString("cmd") == "PlayerState") { + // 其他端收到原唱seek指令 + val state = jsonMsg.getInt("state") + val error = jsonMsg.getInt("error") + Log.d(TAG, "onStreamMessage PlayerState: $state") + if (isChorusCoSinger()) { + when (MediaPlayerState.getStateByValue(state)) { + MediaPlayerState.PLAYER_STATE_PAUSED -> { + mPlayer.pause() + } + MediaPlayerState.PLAYER_STATE_PLAYING -> { + mPlayer.resume() + } + else -> {} + } + } else if (this.singerRole == KTVSingRole.Audience) { + this.mediaPlayerState = MediaPlayerState.getStateByValue(state) + } + ktvApiEventHandlerList.forEach { it.onMusicPlayerStateChanged( + MediaPlayerState.getStateByValue(state), + Constants.MediaPlayerError.getErrorByValue(error), + false + ) } + } else if (jsonMsg.getString("cmd") == "setVoicePitch") { + val pitch = jsonMsg.getDouble("pitch") + if (this.singerRole == KTVSingRole.Audience) { + this.pitch = pitch + } + } + } catch (exp: JSONException) { + Log.e(TAG, "onStreamMessage:$exp") + } + } + + override fun onAudioVolumeIndication(speakers: Array?, totalVolume: Int) { + super.onAudioVolumeIndication(speakers, totalVolume) + val allSpeakers = speakers ?: return + // VideoPitch 回调, 用于同步各端音准 + if (this.singerRole != KTVSingRole.Audience) { + for (info in allSpeakers) { + if (info.uid == 0) { + pitch = + if (this.mediaPlayerState == MediaPlayerState.PLAYER_STATE_PLAYING && isOnMicOpen) { + info.voicePitch + } else { + 0.0 + } + } + } + } + } + + // 用于合唱校准 + override fun onLocalAudioStats(stats: LocalAudioStats?) { + super.onLocalAudioStats(stats) + if (useCustomAudioSource) return + val audioState = stats ?: return + audioPlayoutDelay = audioState.audioPlayoutDelay + } + + // ------------------------ AgoraMusicContentCenterEventDelegate ------------------------ + override fun onPreLoadEvent( + requestId: String?, + songCode: Long, + percent: Int, + lyricUrl: String?, + status: Int, + errorCode: Int + ) { + val callback = loadMusicCallbackMap[songCode.toString()] ?: return + if (status == 0 || status == 1) { + loadMusicCallbackMap.remove(songCode.toString()) + } + if (errorCode == 2) { + // Token过期 + ktvApiEventHandlerList.forEach { it.onTokenPrivilegeWillExpire() } + } + callback.invoke(songCode, percent, status, RtcEngine.getErrorDescription(errorCode), lyricUrl) + } + + override fun onMusicCollectionResult( + requestId: String?, + page: Int, + pageSize: Int, + total: Int, + list: Array?, + errorCode: Int + ) { + val id = requestId ?: return + val callback = musicCollectionCallbackMap[id] ?: return + musicCollectionCallbackMap.remove(id) + if (errorCode == 2) { + // Token过期 + ktvApiEventHandlerList.forEach { it.onTokenPrivilegeWillExpire() } + } + callback.invoke(requestId, errorCode, page, pageSize, total, list) + } + + override fun onMusicChartsResult(requestId: String?, list: Array?, errorCode: Int) { + val id = requestId ?: return + val callback = musicChartsCallbackMap[id] ?: return + musicChartsCallbackMap.remove(id) + if (errorCode == 2) { + // Token过期 + ktvApiEventHandlerList.forEach { it.onTokenPrivilegeWillExpire() } + } + callback.invoke(requestId, errorCode, list) + } + + override fun onLyricResult( + requestId: String?, + songCode: Long, + lyricUrl: String?, + errorCode: Int + ) { + val callback = lyricCallbackMap[requestId] ?: return + val songCode = lyricSongCodeMap[requestId] ?: return + lyricCallbackMap.remove(lyricUrl) + if (errorCode == 2) { + // Token过期 + ktvApiEventHandlerList.forEach { it.onTokenPrivilegeWillExpire() } + } + if (lyricUrl == null || lyricUrl.isEmpty()) { + callback(songCode, null) + return + } + callback(songCode, lyricUrl) + } + + private var highStartTime = 0L; + override fun onSongSimpleInfoResult( + requestId: String?, + songCode: Long, + simpleInfo: String, + errorCode: Int + ) { + if (this.ktvApiConfig.type == KTVType.Normal) return + val jsonMsg = JSONObject(simpleInfo) + val format = jsonMsg.getJSONObject("format") + val highPart = format.getJSONArray("highPart") + val highStartTime = JSONObject(highPart[0].toString()) + val time = highStartTime.getLong("highStartTime") + val endTime = highStartTime.getLong("highEndTime") + this.highStartTime = time + lrcView?.onHighPartTime(time, endTime) + } + + // ------------------------ AgoraRtcMediaPlayerDelegate ------------------------ + private var duration: Long = 0 + override fun onPlayerStateChanged( + state: Constants.MediaPlayerState?, + error: Constants.MediaPlayerError? + ) { + val mediaPlayerState = state ?: return + val mediaPlayerError = error ?: return + Log.d(TAG, "onPlayerStateChanged called, state: $mediaPlayerState, error: $error") + this.mediaPlayerState = mediaPlayerState + when (mediaPlayerState) { + MediaPlayerState.PLAYER_STATE_OPEN_COMPLETED -> { + duration = mPlayer.duration + this.localPlayerPosition = 0 + mPlayer.selectAudioTrack(1) + if (this.singerRole == KTVSingRole.SoloSinger || + this.singerRole == KTVSingRole.LeadSinger + ) { + mPlayer.play() + } + } + MediaPlayerState.PLAYER_STATE_PLAYING -> { + mRtcEngine.adjustPlaybackSignalVolume(remoteVolume) + } + MediaPlayerState.PLAYER_STATE_PAUSED -> { + mRtcEngine.adjustPlaybackSignalVolume(100) + } + MediaPlayerState.PLAYER_STATE_STOPPED -> { + mRtcEngine.adjustPlaybackSignalVolume(100) + duration = 0 + } + else -> {} + } + + if (this.singerRole == KTVSingRole.SoloSinger || this.singerRole == KTVSingRole.LeadSinger) { + syncPlayState(mediaPlayerState, mediaPlayerError) + } + ktvApiEventHandlerList.forEach { it.onMusicPlayerStateChanged(mediaPlayerState, mediaPlayerError, true) } + } + + // 同步播放进度 + override fun onPositionChanged(position_ms: Long, timestamp_ms: Long) { + localPlayerPosition = position_ms + localPlayerSystemTime = timestamp_ms + + if ((this.singerRole == KTVSingRole.SoloSinger || this.singerRole == KTVSingRole.LeadSinger) && position_ms > audioPlayoutDelay) { + val msg: MutableMap = HashMap() + msg["cmd"] = "setLrcTime" + msg["ntp"] = timestamp_ms + msg["duration"] = duration + msg["time"] = + position_ms - audioPlayoutDelay // "position-audioDeviceDelay" 是计算出当前播放的真实进度 + msg["realTime"] = position_ms + msg["playerState"] = MediaPlayerState.getValue(this.mediaPlayerState) + msg["pitch"] = pitch + msg["songIdentifier"] = songIdentifier + val jsonMsg = JSONObject(msg) + sendStreamMessageWithJsonObject(jsonMsg) {} + } + + if (this.singerRole != KTVSingRole.Audience) { + mLastReceivedPlayPosTime = System.currentTimeMillis() + mReceivedPlayPosition = position_ms + } else { + mLastReceivedPlayPosTime = null + mReceivedPlayPosition = 0 + } + } + + override fun onPlayerEvent( + eventCode: Constants.MediaPlayerEvent?, + elapsedTime: Long, + message: String? + ) { + } + + override fun onMetaData(type: Constants.MediaPlayerMetadataType?, data: ByteArray?) {} + + override fun onPlayBufferUpdated(playCachedBuffer: Long) {} + + override fun onPreloadEvent(src: String?, event: Constants.MediaPlayerPreloadEvent?) {} + + override fun onAgoraCDNTokenWillExpire() {} + + override fun onPlayerSrcInfoChanged(from: SrcInfo?, to: SrcInfo?) {} + + override fun onPlayerInfoUpdated(info: PlayerUpdatedInfo?) {} + + override fun onAudioVolumeIndication(volume: Int) {} +} \ No newline at end of file