diff --git a/KTVAPI/Android/README.md b/KTVAPI/Android/README.md
index ddc7896..bb25d88 100644
--- a/KTVAPI/Android/README.md
+++ b/KTVAPI/Android/README.md
@@ -1,6 +1,6 @@
# K 歌场景化 API 示例 demo
-> 本文档主要介绍如何快速跑通 K 歌场景化 API 示例工程,支持加载、播放声网内容中心版权音乐和本地音乐文件。
+> 本文档主要介绍如何快速跑通 K 歌场景化 API 示例工程,本 demo 支持普通合唱、大合唱两种模式, 包含加载、播放声网内容中心版权音乐和本地音乐文件等功能
>
> **Demo 效果:**
>
@@ -10,55 +10,61 @@
## 1. 环境准备
- 最低兼容 Android 5.0(SDK API Level 21)
-- Android Studio 3.5及以上版本。
-- Android 5.0 及以上的手机设备。
+- Android Studio 3.5及以上版本
+- Android 5.0 及以上的手机设备
---
## 2. 运行示例
-- 获取声网 App ID -------- [声网Agora - 文档中心 - 如何获取 App ID](https://docs.agora.io/cn/Agora%20Platform/get_appid_token?platform=All%20Platforms#%E8%8E%B7%E5%8F%96-app-id)
- > - 点击创建应用
- >
- > 
- >
- > - 选择你要创建的应用类型
- >
- > 
- >
- > - 得到 App ID 与 App 证书
- >
- > 
+- 2.1 进入声网控制台获取 APP ID 和 APP 证书 [控制台入口](https://console.shengwang.cn/overview)
-- 获取 App 证书 ----- [声网Agora - 文档中心 - 获取 App 证书](https://docs.agora.io/cn/Agora%20Platform/get_appid_token?platform=All%20Platforms#%E8%8E%B7%E5%8F%96-app-%E8%AF%81%E4%B9%A6)
+ - 点击创建项目
-- **联系销售给 AppID 开通 K 歌权限(如果您没有销售人员的联系方式可通过智能客服联系销售人员 [Agora 支持](https://agora-ticket.agora.io/))**
+ 
+
+ - 选择项目基础配置, 鉴权机制需要选择**安全模式**
+
+ 
+
+ - 拿到项目 APP ID 与 APP 证书
+
+ 
+
+ - **Restful API 服务配置(大合唱)**
+ ```json
+ 注: 体验大合唱模式需要填写 Restful API 相关信息
+ ```
+ 
+ 
+ 
+
+ - **联系声网技术支持给 APP ID 开通 K 歌歌单权限和云端转码权限([声网支持](https://ticket.shengwang.cn/form?type_id=&sdk_product=&sdk_platform=&sdk_version=¤t=0&project_id=&call_id=&channel_name=))**
```json
- 注: 拉取声网版权榜单、歌单、歌曲、歌词等功能是需要开通权限的, 仅体验本地音乐文件模式可以不用开通
+ 注: 拉取声网版权榜单、歌单、歌曲、歌词等功能是需要开通歌单权限的, 仅体验本地音乐文件模式可以不用开通
+ 体验大合唱模式需要开通云端转码权限, 仅体验普通合唱可以不用开通
```
-- 在项目的 [**gradle.properties**](gradle.properties) 里填写需要的声网 App ID 和 App 证书
+- 2.2 在项目的 [**gradle.properties**](gradle.properties) 里填写需要的声网 App ID 和 App 证书、RESTFUL KEY 和 SECRET
```
# RTM RTC SDK key Config
- AGORA_APP_ID:声网appid
- AGORA_APP_CERTIFICATE:声网Certificate
+ AGORA_APP_ID:声网 APP ID
+ AGORA_APP_CERTIFICATE:声网 APP 证书
+ RESTFUL_API_KEY:声网RESTful API key
+ RESTFUL_API_SECRET:声网RESTful API secret
```
-- 用 Android Studio 运行项目即可开始您的体验
+- 2.3 用 Android Studio 运行项目即可开始您的体验
---
## 3. 如何集成场景化 API 实现 K 歌场景
详见[**官网文档**](https://doc.shengwang.cn/doc/online-ktv/android/implementation/ktv-scenario/get-music)
-### 集成遇到困难,该如何联系声网获取协助
-
-> 方案1:如果您已经在使用声网服务或者在对接中,可以直接联系对接的销售或服务
->
-> 方案2:发送邮件给 [support@agora.io](mailto:support@agora.io) 咨询
->
-> 方案3:扫码加入我们的微信交流群提问
->
->
----
+## 4. FAQ
+- 集成遇到困难,该如何联系声网获取协助
+ - 方案1:可以从智能客服获取帮助或联系技术支持人员 [声网支持](https://ticket.shengwang.cn/form?type_id=&sdk_product=&sdk_platform=&sdk_version=¤t=0&project_id=&call_id=&channel_name=)
+ - 方案2:加入微信群提问
+
+ 
diff --git a/KTVAPI/Android/app/.gitignore b/KTVAPI/Android/app/.gitignore
index 42afabf..956c004 100644
--- a/KTVAPI/Android/app/.gitignore
+++ b/KTVAPI/Android/app/.gitignore
@@ -1 +1,2 @@
-/build
\ No newline at end of file
+/build
+/release
\ No newline at end of file
diff --git a/KTVAPI/Android/app/build.gradle b/KTVAPI/Android/app/build.gradle
index c4da17d..241ca82 100644
--- a/KTVAPI/Android/app/build.gradle
+++ b/KTVAPI/Android/app/build.gradle
@@ -23,6 +23,8 @@ android {
buildConfigField "String", "TOOLBOX_SERVER_HOST", "\"${properties.getProperty("TOOLBOX_SERVER_HOST", "")}\""
buildConfigField "String", "AGORA_APP_ID", "\"${properties.getProperty("AGORA_APP_ID", "")}\""
buildConfigField "String", "AGORA_APP_CERTIFICATE", "\"${properties.getProperty("AGORA_APP_CERTIFICATE", "")}\""
+ buildConfigField "String", "RESTFUL_API_KEY", "\"${RESTFUL_API_KEY}\""
+ buildConfigField "String", "RESTFUL_API_SECRET", "\"${RESTFUL_API_SECRET}\""
}
buildTypes {
@@ -74,10 +76,13 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.4.1'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
+ implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
implementation 'com.github.mrmike:ok2curl:0.8.0'
// 歌词组件
implementation 'com.github.AgoraIO-Community:LyricsView:1.1.1-beta.8'
+ //
+ implementation 'io.agora:authentication:1.6.1'
// ktvapi
api project(":lib_ktvapi")
}
diff --git a/KTVAPI/Android/app/src/main/AndroidManifest.xml b/KTVAPI/Android/app/src/main/AndroidManifest.xml
index 089cccc..a054703 100644
--- a/KTVAPI/Android/app/src/main/AndroidManifest.xml
+++ b/KTVAPI/Android/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
+
+
+
+ 不如跳舞
+ 陈慧琳
+ 1
+
+
+
+
+
+ 你
+
+
+ 正
+
+
+ 在
+
+
+ 说
+
+
+ 着
+
+
+ 什
+
+
+ 么
+
+
+ 我
+
+
+ 很
+
+
+ 模
+
+
+ 糊
+
+
+
+
+ 只
+
+
+ 有
+
+
+ 好
+
+
+ 音
+
+
+ 乐
+
+
+ 让
+
+
+ 我
+
+
+ 听
+
+
+ 得
+
+
+ 清
+
+
+ 楚
+
+
+
+
+ 言
+
+
+ 语
+
+
+ 从
+
+
+ 这
+
+
+ 里
+
+
+ 开
+
+
+ 始
+
+
+ 失
+
+
+ 去
+
+
+ 作
+
+
+ 用
+
+
+
+
+ 只
+
+
+ 有
+
+
+ 节
+
+
+ 奏
+
+
+ 感
+
+
+ 能
+
+
+ 够
+
+
+ 互
+
+
+ 相
+
+
+ 接
+
+
+ 触
+
+
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+
+
+ 需
+
+
+ 要
+
+
+ 的
+
+
+ 是
+
+
+ 速
+
+
+ 度
+
+
+
+
+ 你
+
+
+ 看
+
+
+ 看
+
+
+ 你
+
+
+ 看
+
+
+ 看
+
+
+
+
+ 是
+
+
+ 什
+
+
+ 么
+
+
+ 在
+
+
+ 作
+
+
+ 主
+
+
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+
+
+ 是
+
+
+ 一
+
+
+ 个
+
+
+ 大
+
+
+ 银
+
+
+ 幕
+
+
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 踏
+
+
+ 着
+
+
+ 一
+
+
+ 样
+
+
+ 的
+
+
+ 脚
+
+
+ 步
+
+
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+ 聊
+
+
+ 天
+
+
+ 倒
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 让
+
+
+ 自
+
+
+ 己
+
+
+ 觉
+
+
+ 得
+
+
+ 舒
+
+
+ 服
+
+
+
+
+ 是
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 的
+
+
+ 天
+
+
+ 赋
+
+
+
+
+ 继
+
+
+ 续
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 谈
+
+
+ 恋
+
+
+ 爱
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 用
+
+
+ 这
+
+
+ 个
+
+
+ 方
+
+
+ 式
+
+
+ 相
+
+
+ 处
+
+
+
+
+ 没
+
+
+ 有
+
+
+ 人
+
+
+ 觉
+
+
+ 得
+
+
+ 孤
+
+
+ 独
+
+
+
+
+ 也
+
+
+ 没
+
+
+ 有
+
+
+ 包
+
+
+ 袱
+
+
+
+
+ 你
+
+
+ 正
+
+
+ 在
+
+
+ 说
+
+
+ 着
+
+
+ 什
+
+
+ 么
+
+
+ 我
+
+
+ 很
+
+
+ 模
+
+
+ 糊
+
+
+
+
+ 只
+
+
+ 有
+
+
+ 好
+
+
+ 音
+
+
+ 乐
+
+
+ 让
+
+
+ 我
+
+
+ 听
+
+
+ 得
+
+
+ 清
+
+
+ 楚
+
+
+
+
+ 言
+
+
+ 语
+
+
+ 从
+
+
+ 这
+
+
+ 里
+
+
+ 开
+
+
+ 始
+
+
+ 失
+
+
+ 去
+
+
+ 作
+
+
+ 用
+
+
+
+
+ 只
+
+
+ 有
+
+
+ 节
+
+
+ 奏
+
+
+ 感
+
+
+ 能
+
+
+ 够
+
+
+ 互
+
+
+ 相
+
+
+ 接
+
+
+ 触
+
+
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+
+
+ 需
+
+
+ 要
+
+
+ 的
+
+
+ 是
+
+
+ 速
+
+
+ 度
+
+
+
+
+ 你
+
+
+ 看
+
+
+ 看
+
+
+ 你
+
+
+ 看
+
+
+ 看
+
+
+
+
+ 是
+
+
+ 什
+
+
+ 么
+
+
+ 在
+
+
+ 作
+
+
+ 主
+
+
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+
+
+ 是
+
+
+ 一
+
+
+ 个
+
+
+ 大
+
+
+ 银
+
+
+ 幕
+
+
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 踏
+
+
+ 着
+
+
+ 一
+
+
+ 样
+
+
+ 的
+
+
+ 脚
+
+
+ 步
+
+
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 聊
+
+
+ 天
+
+
+ 倒
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 让
+
+
+ 自
+
+
+ 己
+
+
+ 觉
+
+
+ 得
+
+
+ 舒
+
+
+ 服
+
+
+
+
+ 是
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 的
+
+
+ 天
+
+
+ 赋
+
+
+
+
+ 继
+
+
+ 续
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 谈
+
+
+ 恋
+
+
+ 爱
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 用
+
+
+ 这
+
+
+ 个
+
+
+ 方
+
+
+ 式
+
+
+ 相
+
+
+ 处
+
+
+
+
+ 没
+
+
+ 有
+
+
+ 人
+
+
+ 觉
+
+
+ 得
+
+
+ 孤
+
+
+ 独
+
+
+
+
+ 也
+
+
+ 没
+
+
+ 有
+
+
+ 包
+
+
+ 袱
+
+
+
+
+ 当
+
+
+ 所
+
+
+ 有
+
+
+ 甜
+
+
+ 言
+
+
+ 蜜
+
+
+ 语
+
+
+ 都
+
+
+ 那
+
+
+ 么
+
+
+ 虚
+
+
+ 无
+
+
+
+
+ 让
+
+
+ 我
+
+
+ 们
+
+
+ 一
+
+
+ 起
+
+
+ 做
+
+
+ 个
+
+
+ 节
+
+
+ 奏
+
+
+ 的
+
+
+ 信
+
+
+ 徒
+
+
+
+
+ 让
+
+
+ 速
+
+
+ 度
+
+
+ 变
+
+
+ 成
+
+
+ 一
+
+
+ 场
+
+
+ 前
+
+
+ 所
+
+
+ 未
+
+
+ 有
+
+
+ 的
+
+
+ 梦
+
+
+
+
+ 你
+
+
+ 看
+
+
+ 你
+
+
+ 看
+
+
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 聊
+
+
+ 天
+
+
+ 倒
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 让
+
+
+ 自
+
+
+ 己
+
+
+ 觉
+
+
+ 得
+
+
+ 舒
+
+
+ 服
+
+
+
+
+ 是
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 的
+
+
+ 天
+
+
+ 赋
+
+
+
+
+ 继
+
+
+ 续
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 谈
+
+
+ 恋
+
+
+ 爱
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 用
+
+
+ 这
+
+
+ 个
+
+
+ 方
+
+
+ 式
+
+
+ 相
+
+
+ 处
+
+
+
+
+ 没
+
+
+ 有
+
+
+ 人
+
+
+ 觉
+
+
+ 得
+
+
+ 孤
+
+
+ 独
+
+
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 聊
+
+
+ 天
+
+
+ 倒
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 让
+
+
+ 自
+
+
+ 己
+
+
+ 觉
+
+
+ 得
+
+
+ 舒
+
+
+ 服
+
+
+
+
+ 是
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 的
+
+
+ 天
+
+
+ 赋
+
+
+
+
+ 继
+
+
+ 续
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 谈
+
+
+ 恋
+
+
+ 爱
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 用
+
+
+ 这
+
+
+ 个
+
+
+ 方
+
+
+ 式
+
+
+ 相
+
+
+ 处
+
+
+
+
+ 没
+
+
+ 有
+
+
+ 人
+
+
+ 觉
+
+
+ 得
+
+
+ 孤
+
+
+ 独
+
+
+
+
+ 也
+
+
+ 没
+
+
+ 有
+
+
+ 包
+
+
+ 袱
+
+
+
+
+
\ No newline at end of file
diff --git "a/KTVAPI/Android/app/src/main/assets/\346\210\220\351\203\275.mp3" "b/KTVAPI/Android/app/src/main/assets/\346\210\220\351\203\275.mp3"
deleted file mode 100644
index da4336d..0000000
Binary files "a/KTVAPI/Android/app/src/main/assets/\346\210\220\351\203\275.mp3" and /dev/null differ
diff --git "a/KTVAPI/Android/app/src/main/assets/\346\210\220\351\203\275.xml" "b/KTVAPI/Android/app/src/main/assets/\346\210\220\351\203\275.xml"
deleted file mode 100644
index 1328c0f..0000000
--- "a/KTVAPI/Android/app/src/main/assets/\346\210\220\351\203\275.xml"
+++ /dev/null
@@ -1,1225 +0,0 @@
-
-
-
- 成都
- 赵雷
- 1
-
-
-
-
-
- 让
-
-
- 我
-
-
- 掉
-
-
- 下
-
-
- 眼
-
-
- 泪
-
-
- 的
-
-
-
-
- 不
-
-
- 止
-
-
- 昨
-
-
- 夜
-
-
- 的
-
-
- 酒
-
-
-
-
- 让
-
-
- 我
-
-
- 依
-
-
- 依
-
-
- 不
-
-
- 舍
-
-
- 的
-
-
-
-
- 不
-
-
- 止
-
-
- 你
-
-
- 的
-
-
- 温
-
-
- 柔
-
-
-
-
- 余
-
-
- 路
-
-
- 还
-
-
- 要
-
-
- 走
-
-
- 多
-
-
- 久
-
-
-
-
- 你
-
-
- 攥
-
-
- 着
-
-
- 我
-
-
- 的
-
-
- 手
-
-
-
-
- 让
-
-
- 我
-
-
- 感
-
-
- 到
-
-
- 为
-
-
- 难
-
-
- 的
-
-
-
-
- 是
-
-
- 挣
-
-
- 扎
-
-
- 的
-
-
- 自
-
-
- 由
-
-
-
-
- 分
-
-
- 别
-
-
- 总
-
-
- 是
-
-
- 在
-
-
- 九
-
-
- 月
-
-
-
-
- 回
-
-
- 忆
-
-
- 是
-
-
- 思
-
-
- 念
-
-
- 的
-
-
- 愁
-
-
-
-
- 深
-
-
- 秋
-
-
- 嫩
-
-
- 绿
-
-
- 的
-
-
- 垂
-
-
- 柳
-
-
-
-
- 亲
-
-
- 吻
-
-
- 着
-
-
- 我
-
-
- 额
-
-
- 头
-
-
-
-
- 在
-
-
- 那
-
-
- 座
-
-
- 阴
-
-
- 雨
-
-
- 的
-
-
- 小
-
-
- 城
-
-
- 里
-
-
-
-
- 我
-
-
- 从
-
-
- 未
-
-
- 忘
-
-
- 记
-
-
- 你
-
-
-
-
- 成
-
-
- 都
-
-
- 带
-
-
- 不
-
-
- 走
-
-
- 的
-
-
- 只
-
-
- 有
-
-
- 你
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
- 你
-
-
- 会
-
-
- 挽
-
-
- 着
-
-
- 我
-
-
- 的
-
-
- 衣
-
-
- 袖
-
-
-
-
- 我
-
-
- 会
-
-
- 把
-
-
- 手
-
-
- 揣
-
-
- 进
-
-
- 裤
-
-
- 兜
-
-
-
-
- 走
-
-
- 到
-
-
- 玉
-
-
- 林
-
-
- 路
-
-
- 的
-
-
- 尽
-
-
- 头
-
-
-
-
- 坐
-
-
- 在
-
-
- 小
-
-
- 酒
-
-
- 馆
-
-
- 的
-
-
- 门
-
-
- 口
-
-
-
-
- 分
-
-
- 别
-
-
- 总
-
-
- 是
-
-
- 在
-
-
- 九
-
-
- 月
-
-
-
-
- 回
-
-
- 忆
-
-
- 是
-
-
- 思
-
-
- 念
-
-
- 的
-
-
- 愁
-
-
-
-
- 深
-
-
- 秋
-
-
- 嫩
-
-
- 绿
-
-
- 的
-
-
- 垂
-
-
- 柳
-
-
-
-
- 亲
-
-
- 吻
-
-
- 着
-
-
- 我
-
-
- 额
-
-
- 头
-
-
-
-
- 在
-
-
- 那
-
-
- 座
-
-
- 阴
-
-
- 雨
-
-
- 的
-
-
- 小
-
-
- 城
-
-
- 里
-
-
-
-
- 我
-
-
- 从
-
-
- 未
-
-
- 忘
-
-
- 记
-
-
- 你
-
-
-
-
- 成
-
-
- 都
-
-
- 带
-
-
- 不
-
-
- 走
-
-
- 的
-
-
- 只
-
-
- 有
-
-
- 你
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
- 你
-
-
- 会
-
-
- 挽
-
-
- 着
-
-
- 我
-
-
- 的
-
-
- 衣
-
-
- 袖
-
-
-
-
- 我
-
-
- 会
-
-
- 把
-
-
- 手
-
-
- 揣
-
-
- 进
-
-
- 裤
-
-
- 兜
-
-
-
-
- 走
-
-
- 到
-
-
- 玉
-
-
- 林
-
-
- 路
-
-
- 的
-
-
- 尽
-
-
- 头
-
-
-
-
- 坐
-
-
- 在
-
-
- 小
-
-
- 酒
-
-
- 馆
-
-
- 的
-
-
- 门
-
-
- 口
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
- 你
-
-
- 会
-
-
- 挽
-
-
- 着
-
-
- 我
-
-
- 的
-
-
- 衣
-
-
- 袖
-
-
-
-
- 我
-
-
- 会
-
-
- 把
-
-
- 手
-
-
- 揣
-
-
- 进
-
-
- 裤
-
-
- 兜
-
-
-
-
- 走
-
-
- 到
-
-
- 玉
-
-
- 林
-
-
- 路
-
-
- 的
-
-
- 尽
-
-
- 头
-
-
-
-
- 走
-
-
- 过
-
-
- 小
-
-
- 酒
-
-
- 馆
-
-
- 的
-
-
- 门
-
-
- 口
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
-
\ No newline at end of file
diff --git a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/MyApplication.kt b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/MyApplication.kt
index f8e4dd8..e190ed6 100644
--- a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/MyApplication.kt
+++ b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/MyApplication.kt
@@ -19,8 +19,8 @@ class MyApplication : Application() {
super.onCreate()
sApp = this
try {
- initFile("成都.mp3")
- initFile("成都.xml")
+ initFile("不如跳舞.mp4")
+ initFile("不如跳舞.xml")
}catch (e:Exception){
e.printStackTrace()
}
diff --git a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/api/CloudApiManager.kt b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/api/CloudApiManager.kt
new file mode 100644
index 0000000..2d7b2a0
--- /dev/null
+++ b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/api/CloudApiManager.kt
@@ -0,0 +1,201 @@
+package io.agora.ktvdemo.api
+
+import android.util.Log
+import com.moczul.ok2curl.CurlInterceptor
+import com.moczul.ok2curl.logger.Logger
+import io.agora.ktvdemo.BuildConfig
+import io.agora.ktvdemo.MyApplication
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Request.Builder
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.logging.HttpLoggingInterceptor
+import org.json.JSONArray
+import org.json.JSONObject
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+/**
+ * 云端合流请求
+ */
+class CloudApiManager private constructor() {
+
+ companion object {
+ fun getInstance(): CloudApiManager {
+ return InstanceHolder.apiManager
+ }
+
+ //private const val testIp = "218.205.37.50"
+ private const val domain = "https://api.sd-rtn.com"
+ private const val TAG = "ApiManager"
+ const val outputUid = 20232023
+ }
+
+ internal object InstanceHolder {
+ val apiManager = CloudApiManager()
+ }
+
+ private var tokenName = ""
+ private var taskId = ""
+ private val okHttpClient: OkHttpClient = OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
+ .addInterceptor(CurlInterceptor(object : Logger {
+ override fun log(message: String) {
+ Log.d(TAG, message)
+ }
+ }))
+ .build()
+
+ private fun fetchCloudToken(): String {
+ var token = ""
+ try {
+ val acquireOjb = JSONObject()
+ acquireOjb.put("instanceId", System.currentTimeMillis().toString() + "")
+ //acquireOjb.put("testIp", testIp)
+ val request: Request = Builder()
+ .url(getTokenUrl(domain, BuildConfig.AGORA_APP_ID))
+ .addHeader("Content-Type", "application/json")
+ .addHeader("Authorization", basicAuth)
+ .post(acquireOjb.toString().toRequestBody())
+ .build()
+
+ val responseToken = okHttpClient.newCall(request).execute()
+ if (responseToken.isSuccessful) {
+ val body = responseToken.body!!
+ val bodyString = body.string()
+ val jsonToken = JSONObject(bodyString)
+ if (jsonToken.has("tokenName")) {
+ token = jsonToken.getString("tokenName")
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "getToken error " + e.message)
+ }
+ return token
+ }
+
+ fun fetchStartCloud(mainChannel: String, inputToken: String, outputToken: String) {
+ val token = fetchCloudToken()
+ tokenName = token.ifEmpty {
+ Log.e(TAG, "云端合流uid 请求报错 token is null")
+ return
+ }
+ var taskId = ""
+ try {
+ val transcoderObj = JSONObject()
+ val inputRetObj = JSONObject()
+ .put("rtcUid", 0)
+ .put("rtcToken", inputToken)
+ .put("rtcChannel", mainChannel)
+ val intObj = JSONObject()
+ .put("rtc", inputRetObj)
+ transcoderObj.put("audioInputs", JSONArray().put(intObj))
+ transcoderObj.put("idleTimeout", 300)
+ val audioOptionObj = JSONObject()
+ .put("profileType", "AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO")
+ .put("fullChannelMixer", "native-mixer-weighted")
+ val outputRetObj = JSONObject()
+ .put("rtcUid", outputUid)
+ .put("rtcToken", outputToken)
+ .put("rtcChannel", mainChannel + "_ad")
+ val dataStreamObj = JSONObject()
+ .put("source", JSONObject().put("audioMetaData", true))
+ .put("sink", JSONObject())
+ val outputsObj = JSONObject()
+ .put("audioOption", audioOptionObj)
+ .put("rtc", outputRetObj)
+ .put("metaDataOption", dataStreamObj)
+ transcoderObj.put("outputs", JSONArray().put(outputsObj))
+ val postBody = JSONObject()
+ .put(
+ "services", JSONObject()
+ .put(
+ "cloudTranscoder", JSONObject()
+ .put("serviceType", "cloudTranscoderV2")
+ .put(
+ "config", JSONObject()
+ .put("transcoder", transcoderObj)
+ )
+ )
+ )
+ val request: Request = Builder()
+ .url(startTaskUrl(domain, BuildConfig.AGORA_APP_ID, tokenName))
+ .addHeader("Content-Type", "application/json")
+ .addHeader("Authorization", basicAuth)
+ .post(postBody.toString().toRequestBody())
+ .build()
+
+ val responseStart = okHttpClient.newCall(request).execute()
+ if (responseStart.isSuccessful) {
+ val body = responseStart.body!!
+ val bodyString = body.string()
+ val jsonUid = JSONObject(bodyString)
+ if (jsonUid.has("taskId")) {
+ taskId = jsonUid.getString("taskId")
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "云端合流uid 请求报错 " + e.message)
+ }
+ if (taskId.isNotEmpty()) {
+ this.taskId = taskId
+ }
+ }
+
+ fun fetchStopCloud() {
+ if (taskId.isEmpty() || tokenName.isEmpty()) {
+ Log.e(TAG, "云端合流任务停止失败 taskId || tokenName is null")
+ return
+ }
+ try {
+ val request: Request = Builder()
+ .url(deleteTaskUrl(domain, BuildConfig.AGORA_APP_ID, taskId, tokenName))
+ .addHeader("Content-Type", "application/json")
+ .addHeader("Authorization", basicAuth)
+ .delete()
+ .build()
+ val response = okHttpClient.newCall(request).execute()
+ if (response.isSuccessful) {
+ val body = response.body!!
+ val bodyString = body.string()
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "云端合流任务停止失败 " + e.message)
+ }
+ }
+
+ private fun getTokenUrl(domain: String, appId: String): String {
+ return String.format("%s/v1/projects/%s/rtsc/cloud-transcoder/builderTokens", domain, appId)
+ }
+
+ private fun startTaskUrl(domain: String, appId: String, tokenName: String): String {
+ return String.format("%s/v1/projects/%s/rtsc/cloud-transcoder/tasks?builderToken=%s", domain, appId, tokenName)
+ }
+
+ private fun deleteTaskUrl(domain: String, appid: String, taskid: String, tokenName: String): String {
+ return String.format(
+ "%s/v1/projects/%s/rtsc/cloud-transcoder/tasks/%s?builderToken=%s",
+ domain,
+ appid,
+ taskid,
+ tokenName
+ )
+ }
+
+ private val basicAuth: String
+ private get() {
+ // 拼接客户 ID 和客户密钥并使用 base64 编码
+ val plainCredentials = BuildConfig.RESTFUL_API_KEY + ":" + BuildConfig.RESTFUL_API_SECRET
+ var base64Credentials: String? = null
+ base64Credentials = String(Base64.getEncoder().encode(plainCredentials.toByteArray()))
+ // 创建 authorization header
+ return "Basic $base64Credentials"
+ }
+
+ private fun getString(resId:Int):String{
+ return MyApplication().getString(resId)
+ }
+}
\ No newline at end of file
diff --git a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/rtc/RtcEngineController.kt b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/rtc/RtcEngineController.kt
index 50bf199..79bf301 100644
--- a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/rtc/RtcEngineController.kt
+++ b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/rtc/RtcEngineController.kt
@@ -50,7 +50,8 @@ object RtcEngineController {
return innerRtcEngine!!
}
- var rtcToken: String = ""
var chorusChannelRtcToken = ""
+ var audienceChannelToken = ""
+ var musicStreamToken = ""
var rtmToken = ""
}
\ No newline at end of file
diff --git a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/ui/LivingFragment.kt b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/ui/LivingFragment.kt
index 6b6344b..70c7608 100644
--- a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/ui/LivingFragment.kt
+++ b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/ui/LivingFragment.kt
@@ -1,28 +1,28 @@
package io.agora.ktvdemo.ui
import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.widget.Toast
import androidx.navigation.fragment.findNavController
import io.agora.karaoke_view.v11.KaraokeView
import io.agora.ktvapi.*
import io.agora.ktvdemo.BuildConfig
-import io.agora.ktvdemo.MyApplication
import io.agora.ktvdemo.R
+import io.agora.ktvdemo.api.CloudApiManager
import io.agora.ktvdemo.databinding.FragmentLivingBinding
import io.agora.ktvdemo.rtc.RtcEngineController
import io.agora.ktvdemo.utils.DownloadUtils
import io.agora.ktvdemo.utils.KeyCenter
+import io.agora.ktvdemo.utils.TokenGenerator
import io.agora.ktvdemo.utils.ZipUtils
import io.agora.rtc2.ChannelMediaOptions
+import io.agora.rtc2.IRtcEngineEventHandler
+import io.agora.rtc2.RtcConnection
import java.io.File
-import kotlin.random.Random
+import java.util.concurrent.Executors
/*
* K 歌体验页面
@@ -37,15 +37,15 @@ class LivingFragment : BaseFragment() {
/*
* KTVAPI 实例
*/
- private val ktvApi: KTVApi by lazy {
- createKTVApi()
- }
+ private lateinit var ktvApi: KTVApi
/*
* KTVAPI 事件
*/
private val ktvApiEventHandler = object : IKTVApiEventHandler() {}
+ private val scheduledThreadPool = Executors.newScheduledThreadPool(5)
+
override fun getViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLivingBinding {
return FragmentLivingBinding.inflate(inflater)
}
@@ -53,15 +53,43 @@ class LivingFragment : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ // 大合唱模式下主唱需要启动云端合流
+ if (KeyCenter.role == KTVSingRole.LeadSinger && !KeyCenter.isNormalChorus) {
+
+ TokenGenerator.generateToken("${KeyCenter.channelId}_ad", CloudApiManager.outputUid.toString(),
+ TokenGenerator.TokenGeneratorType.token007, TokenGenerator.AgoraTokenType.rtc,
+ success = { outputToken ->
+ TokenGenerator.generateToken(KeyCenter.channelId, "0",
+ TokenGenerator.TokenGeneratorType.token007, TokenGenerator.AgoraTokenType.rtc,
+ success = { inputToken ->
+ scheduledThreadPool.execute {
+ CloudApiManager.getInstance().fetchStartCloud(
+ KeyCenter.channelId,
+ inputToken,
+ outputToken
+ )
+ }
+ },
+ failure = {
+ toast("云端合流启动失败, token获取失败")
+ }
+ )
+ },
+ failure = {
+ toast("云端合流启动失败, token获取失败")
+ }
+ )
+ }
+
initView()
initKTVApi()
joinChannel()
- loadMusic()
// 设置麦克风初始状态,主唱默认开麦
if (KeyCenter.role == KTVSingRole.LeadSinger) {
ktvApi.muteMic(false)
}
+ loadMusic()
}
override fun onDestroy() {
@@ -82,6 +110,9 @@ class LivingFragment : BaseFragment() {
ktvApi.removeEventHandler(ktvApiEventHandler)
ktvApi.release()
RtcEngineController.rtcEngine.leaveChannel()
+ scheduledThreadPool.execute {
+ CloudApiManager.getInstance().fetchStopCloud()
+ }
findNavController().popBackStack()
}
if (KeyCenter.role == KTVSingRole.LeadSinger) {
@@ -95,21 +126,86 @@ class LivingFragment : BaseFragment() {
if (KeyCenter.role == KTVSingRole.LeadSinger) {
toast(getString(R.string.app_no_premission))
} else {
- ktvApi.switchSingerRole(KTVSingRole.CoSinger, object : ISwitchRoleStateListener {
- override fun onSwitchRoleSuccess() {
- mainHandler.post {
- toast("加入合唱成功")
- tvSinger.text = getString(R.string.app_co_singer)
- KeyCenter.role = KTVSingRole.CoSinger
+ if (KeyCenter.isMcc) {
+ // 使用声网版权中心歌单
+ val musicConfiguration = KTVLoadMusicConfiguration(
+ KeyCenter.songCode.toString(), // 需要传入唯一的歌曲id,demo 简化逻辑传了songCode
+ KeyCenter.LeadSingerUid,
+ KTVLoadMusicMode.LOAD_MUSIC_ONLY
+ )
+ ktvApi.loadMusic(KeyCenter.songCode, musicConfiguration, object : IMusicLoadStateListener {
+ override fun onMusicLoadSuccess(songCode: Long, lyricUrl: String) {
+ Log.d("Music", "onMusicLoadSuccess, songCode: $songCode, lyricUrl: $lyricUrl")
+ // 切换身份为合唱者
+ ktvApi.switchSingerRole(KTVSingRole.CoSinger, object : ISwitchRoleStateListener {
+ override fun onSwitchRoleSuccess() {
+ mainHandler.post {
+ toast("加入合唱成功,自动开麦")
+ ktvApi.muteMic(false)
+ btMicStatus.text = "麦克风开"
+ tvSinger.text = getString(R.string.app_co_singer)
+ KeyCenter.role = KTVSingRole.CoSinger
+ }
+ }
+
+ override fun onSwitchRoleFail(reason: SwitchRoleFailReason) {
+ mainHandler.post {
+ toast("加入合唱失败")
+ }
+ }
+ })
}
- }
- override fun onSwitchRoleFail(reason: SwitchRoleFailReason) {
- mainHandler.post {
- toast("加入合唱失败")
+ override fun onMusicLoadFail(songCode: Long, reason: KTVLoadMusicFailReason) {
+ Log.d("Music", "onMusicLoadFail, songCode: $songCode, reason: $reason")
}
- }
- })
+
+ override fun onMusicLoadProgress(
+ songCode: Long,
+ percent: Int,
+ status: MusicLoadStatus,
+ msg: String?,
+ lyricUrl: String?
+ ) {
+ Log.d("Music", "onMusicLoadProgress, songCode: $songCode, percent: $percent")
+ mainHandler.post {
+ binding?.btLoadProgress?.text = "下载进度:$percent%"
+ }
+ }
+ })
+ } else {
+ // 使用本地音乐文件
+ val musicConfiguration = KTVLoadMusicConfiguration(
+ KeyCenter.songCode.toString(), // 需要传入唯一的歌曲id,demo 简化逻辑传了songCode
+ KeyCenter.LeadSingerUid,
+ KTVLoadMusicMode.LOAD_NONE
+ )
+ val songPath = requireActivity().filesDir.absolutePath + File.separator
+ val songName = "不如跳舞"
+ ktvApi.loadMusic("$songPath$songName.mp4", musicConfiguration)
+ val fileLrc = File("$songPath$songName.xml")
+ val lyricsModel = KaraokeView.parseLyricsData(fileLrc)
+ karaokeView?.lyricsData = lyricsModel
+
+ // 切换身份为合唱者
+ ktvApi.switchSingerRole(KTVSingRole.CoSinger, object : ISwitchRoleStateListener {
+ override fun onSwitchRoleSuccess() {
+ mainHandler.post {
+ toast("加入合唱成功,自动开麦")
+ ktvApi.muteMic(false)
+ btMicStatus.text = "麦克风开"
+ tvSinger.text = getString(R.string.app_co_singer)
+ KeyCenter.role = KTVSingRole.CoSinger
+ }
+ }
+
+ override fun onSwitchRoleFail(reason: SwitchRoleFailReason) {
+ mainHandler.post {
+ toast("加入合唱失败")
+ }
+ }
+ })
+ }
}
}
@@ -127,47 +223,17 @@ class LivingFragment : BaseFragment() {
// 开原唱:仅领唱和合唱者可以做这项操作
btOriginal.setOnClickListener {
- when (KeyCenter.role) {
- KTVSingRole.LeadSinger -> {
- ktvApi.getMediaPlayer().selectMultiAudioTrack(0, 0)
- }
- KTVSingRole.CoSinger -> {
- ktvApi.getMediaPlayer().selectAudioTrack(0)
- }
- else -> {
- toast(getString(R.string.app_no_premission))
- }
- }
+ ktvApi.switchAudioTrack(AudioTrackMode.YUAN_CHANG)
}
// 开伴奏:仅领唱和合唱者可以做这项操作
btAcc.setOnClickListener {
- when (KeyCenter.role) {
- KTVSingRole.LeadSinger -> {
- ktvApi.getMediaPlayer().selectMultiAudioTrack(1, 1)
- }
- KTVSingRole.CoSinger -> {
- ktvApi.getMediaPlayer().selectAudioTrack(1)
- }
- else -> {
- toast(getString(R.string.app_no_premission))
- }
- }
+ ktvApi.switchAudioTrack(AudioTrackMode.BAN_ZOU)
}
// 开导唱:仅领唱可以做这项操作,开启后领唱本地听到歌曲原唱,但观众听到仍为伴奏
btDaoChang.setOnClickListener {
- when (KeyCenter.role) {
- KTVSingRole.LeadSinger -> {
- ktvApi.getMediaPlayer().selectMultiAudioTrack(0, 1)
- }
- KTVSingRole.CoSinger -> {
- toast(getString(R.string.app_no_premission))
- }
- else -> {
- toast(getString(R.string.app_no_premission))
- }
- }
+ ktvApi.switchAudioTrack(AudioTrackMode.DAO_CHANG)
}
// 加载音乐
@@ -175,7 +241,8 @@ class LivingFragment : BaseFragment() {
if (KeyCenter.isMcc) {
// 使用声网版权中心歌单
val musicConfiguration = KTVLoadMusicConfiguration(
- KeyCenter.songCode.toString(), false, KeyCenter.LeadSingerUid,
+ KeyCenter.songCode.toString(), // 需要传入唯一的歌曲id,demo 简化逻辑传了songCode
+ KeyCenter.LeadSingerUid,
if (KeyCenter.role == KTVSingRole.Audience) KTVLoadMusicMode.LOAD_LRC_ONLY else KTVLoadMusicMode.LOAD_MUSIC_AND_LRC
)
ktvApi.loadMusic(KeyCenter.songCode, musicConfiguration, object : IMusicLoadStateListener {
@@ -206,7 +273,7 @@ class LivingFragment : BaseFragment() {
}
}
- override fun onMusicLoadFail(songCode: Long, reason: KTVLoadSongFailReason) {
+ override fun onMusicLoadFail(songCode: Long, reason: KTVLoadMusicFailReason) {
Log.d("Music", "onMusicLoadFail, songCode: $songCode, reason: $reason")
}
@@ -226,18 +293,20 @@ class LivingFragment : BaseFragment() {
} else {
// 使用本地音乐文件
val musicConfiguration = KTVLoadMusicConfiguration(
- KeyCenter.songCode.toString(), false, KeyCenter.LeadSingerUid, KTVLoadMusicMode.LOAD_NONE
+ KeyCenter.songCode.toString(), // 需要传入唯一的歌曲id,demo 简化逻辑传了songCode
+ KeyCenter.LeadSingerUid,
+ KTVLoadMusicMode.LOAD_NONE
)
val songPath = requireActivity().filesDir.absolutePath + File.separator
- val songName = "成都"
- ktvApi.loadMusic("$songPath$songName.mp3", musicConfiguration)
+ val songName = "不如跳舞"
+ ktvApi.loadMusic("$songPath$songName.mp4", musicConfiguration)
val fileLrc = File("$songPath$songName.xml")
val lyricsModel = KaraokeView.parseLyricsData(fileLrc)
karaokeView?.lyricsData = lyricsModel
if (KeyCenter.role == KTVSingRole.LeadSinger) {
ktvApi.switchSingerRole(KTVSingRole.LeadSinger, object : ISwitchRoleStateListener {
override fun onSwitchRoleSuccess() {
- ktvApi.startSing("$songPath$songName.mp3", 0)
+ ktvApi.startSing("$songPath$songName.mp4", 0)
}
override fun onSwitchRoleFail(reason: SwitchRoleFailReason) {
@@ -293,20 +362,41 @@ class LivingFragment : BaseFragment() {
* 初始化 KTVAPI
*/
private fun initKTVApi() {
- val ktvApiConfig = KTVApiConfig(
- BuildConfig.AGORA_APP_ID,
- RtcEngineController.rtmToken,
- RtcEngineController.rtcEngine,
- KeyCenter.channelId,
- KeyCenter.localUid,
- "${KeyCenter.channelId}_ex",
- RtcEngineController.chorusChannelRtcToken,
- 10,
- KTVType.Normal,
- if (KeyCenter.isMcc) KTVMusicType.SONG_CODE else KTVMusicType.SONG_URL
- )
- // 初始化 ktvapi 模块
- ktvApi.initialize(ktvApiConfig)
+ if (KeyCenter.isNormalChorus) {
+ // 创建普通合唱ktvapi实例
+ ktvApi = createKTVApi(
+ KTVApiConfig(
+ appId = BuildConfig.AGORA_APP_ID,
+ rtmToken = RtcEngineController.rtmToken,
+ engine = RtcEngineController.rtcEngine,
+ channelName = KeyCenter.channelId,
+ localUid = KeyCenter.localUid,
+ chorusChannelName = "${KeyCenter.channelId}_ex",
+ chorusChannelToken = RtcEngineController.chorusChannelRtcToken,
+ maxCacheSize = 10,
+ type = KTVType.Normal,
+ musicType = if (KeyCenter.isMcc) KTVMusicType.SONG_CODE else KTVMusicType.SONG_URL
+ )
+ )
+ } else {
+ // 创建大合唱ktvapi实例
+ ktvApi = createKTVGiantChorusApi(
+ KTVGiantChorusApiConfig(
+ appId = BuildConfig.AGORA_APP_ID,
+ rtmToken = RtcEngineController.rtmToken,
+ engine = RtcEngineController.rtcEngine,
+ localUid = KeyCenter.localUid,
+ audienceChannelName = KeyCenter.channelId + "_ad",
+ audienceChannelToken = RtcEngineController.audienceChannelToken,
+ chorusChannelName = KeyCenter.channelId,
+ chorusChannelToken = RtcEngineController.chorusChannelRtcToken,
+ musicStreamUid = 2023,
+ musicStreamToken = RtcEngineController.musicStreamToken,
+ maxCacheSize = 10,
+ musicType = if (KeyCenter.isMcc) KTVMusicType.SONG_CODE else KTVMusicType.SONG_URL
+ )
+ )
+ }
// 注册 ktvapi 事件
ktvApi.addEventHandler(ktvApiEventHandler)
// 设置歌词组件
@@ -343,12 +433,34 @@ class LivingFragment : BaseFragment() {
publishMicrophoneTrack = KeyCenter.role != KTVSingRole.Audience
clientRoleType = if (KeyCenter.role == KTVSingRole.Audience) io.agora.rtc2.Constants.CLIENT_ROLE_AUDIENCE else io.agora.rtc2.Constants.CLIENT_ROLE_BROADCASTER
}
- RtcEngineController.rtcEngine.joinChannel(
- RtcEngineController.rtcToken,
- KeyCenter.channelId,
- KeyCenter.localUid,
- channelMediaOptions
- )
+
+ if (KeyCenter.isNormalChorus) {
+ // 普通合唱或独唱加入频道
+ RtcEngineController.rtcEngine.joinChannel(
+ RtcEngineController.audienceChannelToken,
+ KeyCenter.channelId,
+ KeyCenter.localUid,
+ channelMediaOptions
+ )
+ } else {
+ // 大合唱加入频道
+ RtcEngineController.rtcEngine.joinChannelEx(
+ RtcEngineController.audienceChannelToken,
+ RtcConnection(KeyCenter.channelId + "_ad", KeyCenter.localUid),
+ channelMediaOptions,
+ object : IRtcEngineEventHandler() {
+ override fun onStreamMessage(uid: Int, streamId: Int, data: ByteArray?) {
+ (ktvApi as KTVGiantChorusApiImpl).setAudienceStreamMessage(uid, streamId, data)
+ }
+
+ override fun onAudioMetadataReceived(uid: Int, data: ByteArray?) {
+ super.onAudioMetadataReceived(uid, data)
+ (ktvApi as KTVGiantChorusApiImpl).setAudienceAudioMetadataReceived(uid, data)
+ }
+ }
+ )
+ RtcEngineController.rtcEngine.setParametersEx("{\"rtc.use_audio4\": true}", RtcConnection(KeyCenter.channelId + "_ad", KeyCenter.localUid))
+ }
// 加入频道后需要更新数据传输通道
ktvApi.renewInnerDataStreamId()
@@ -361,7 +473,8 @@ class LivingFragment : BaseFragment() {
if (KeyCenter.isMcc) {
// 使用声网版权中心歌单
val musicConfiguration = KTVLoadMusicConfiguration(
- KeyCenter.songCode.toString(), false, KeyCenter.LeadSingerUid,
+ KeyCenter.songCode.toString(), // 需要传入唯一的歌曲id,demo 简化逻辑传了songCode
+ KeyCenter.LeadSingerUid,
if (KeyCenter.role == KTVSingRole.Audience) KTVLoadMusicMode.LOAD_LRC_ONLY else KTVLoadMusicMode.LOAD_MUSIC_AND_LRC
)
ktvApi.loadMusic(KeyCenter.songCode, musicConfiguration, object : IMusicLoadStateListener {
@@ -382,7 +495,7 @@ class LivingFragment : BaseFragment() {
}
}
- override fun onMusicLoadFail(songCode: Long, reason: KTVLoadSongFailReason) {
+ override fun onMusicLoadFail(songCode: Long, reason: KTVLoadMusicFailReason) {
Log.d("Music", "onMusicLoadFail, songCode: $songCode, reason: $reason")
}
@@ -402,18 +515,20 @@ class LivingFragment : BaseFragment() {
} else {
// 使用本地音乐文件
val musicConfiguration = KTVLoadMusicConfiguration(
- KeyCenter.songCode.toString(), false, KeyCenter.LeadSingerUid, KTVLoadMusicMode.LOAD_NONE
+ KeyCenter.songCode.toString(), // 需要传入唯一的歌曲id,demo 简化逻辑传了songCode
+ KeyCenter.LeadSingerUid,
+ KTVLoadMusicMode.LOAD_NONE
)
val songPath = requireActivity().filesDir.absolutePath + File.separator
- val songName = "成都"
- ktvApi.loadMusic("$songPath$songName.mp3", musicConfiguration)
+ val songName = "不如跳舞"
+ ktvApi.loadMusic("$songPath$songName.mp4", musicConfiguration)
val fileLrc = File("$songPath$songName.xml")
val lyricsModel = KaraokeView.parseLyricsData(fileLrc)
karaokeView?.lyricsData = lyricsModel
if (KeyCenter.role == KTVSingRole.LeadSinger) {
ktvApi.switchSingerRole(KTVSingRole.LeadSinger, object : ISwitchRoleStateListener {
override fun onSwitchRoleSuccess() {
- ktvApi.startSing("$songPath$songName.mp3", 0)
+ ktvApi.startSing("$songPath$songName.mp4", 0)
}
override fun onSwitchRoleFail(reason: SwitchRoleFailReason) {
diff --git a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/ui/MainFragment.kt b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/ui/MainFragment.kt
index b368f6f..38703a5 100644
--- a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/ui/MainFragment.kt
+++ b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/ui/MainFragment.kt
@@ -8,6 +8,7 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.widget.doAfterTextChanged
import androidx.navigation.fragment.findNavController
import io.agora.ktvapi.KTVSingRole
+import io.agora.ktvdemo.BuildConfig
import io.agora.ktvdemo.rtc.IChannelEventListener
import io.agora.ktvdemo.R
import io.agora.ktvdemo.rtc.RtcEngineController
@@ -40,6 +41,7 @@ class MainFragment : BaseFragment() {
btnLeadSinger.setOnClickListener {
resetRoleView()
KeyCenter.role = KTVSingRole.LeadSinger
+ KeyCenter.localUid = KeyCenter.LeadSingerUid
setRoleView()
}
@@ -54,15 +56,26 @@ class MainFragment : BaseFragment() {
// 选择加载歌曲的类型, MCC 声网歌曲中心或者本地歌曲
groupSongType.setOnCheckedChangeListener { _, checkedId -> KeyCenter.isMcc = checkedId == R.id.rbtMccSong }
+ // 选择体验 KTVApi 的类型, 普通合唱或者大合唱
+ ktvApiType.setOnCheckedChangeListener { _, checkedId -> KeyCenter.isNormalChorus = checkedId == R.id.rbtNormalChorus}
+
// 开始体验按钮
btnStartChorus.setOnClickListener {
+ if (BuildConfig.AGORA_APP_ID.isEmpty()) {
+ toast(getString(R.string.app_appid_check))
+ return@setOnClickListener
+ }
+// if (!KeyCenter.isNormalChorus && BuildConfig.RESTFUL_API_KEY.isEmpty()) {
+// toast(getString(R.string.app_restful_check))
+// return@setOnClickListener
+// }
if (KeyCenter.channelId.isEmpty()){
toast(getString(R.string.app_input_channel_name))
return@setOnClickListener
}
RtcEngineController.eventListener = IChannelEventListener()
- // 这里一共获取了三个 Token
+ // 这里一共获取了四个 Token
// 1、加入主频道使用的 Rtc Token
// 2、如果要使用 MCC 模块获取歌单、下载歌曲,需要 RTM Token 进行鉴权,如果您有自己的歌单就不需要获取该 token
// 3、合唱需要用到的合唱子频道 token,如果您只需要独唱就不需要获取该 token
@@ -76,21 +89,47 @@ class MainFragment : BaseFragment() {
success = { ret ->
val rtcToken = ret[TokenGenerator.AgoraTokenType.rtc] ?: ""
val rtmToken = ret[TokenGenerator.AgoraTokenType.rtm] ?: ""
- TokenGenerator.generateToken("${KeyCenter.channelId}_ex", KeyCenter.localUid.toString(),
- TokenGenerator.TokenGeneratorType.token007, TokenGenerator.AgoraTokenType.rtc,
- success = { chorusToken ->
- RtcEngineController.rtcToken = rtcToken
- RtcEngineController.rtmToken = rtmToken
- RtcEngineController.chorusChannelRtcToken = chorusToken
- findNavController().navigate(R.id.action_mainFragment_to_livingFragment)
- },
- failure = {
- toast("获取 token 异常")
- }
- )
+
+ if (KeyCenter.isNormalChorus) {
+ TokenGenerator.generateToken("${KeyCenter.channelId}_ex", KeyCenter.localUid.toString(),
+ TokenGenerator.TokenGeneratorType.token007, TokenGenerator.AgoraTokenType.rtc,
+ success = { chorusToken ->
+ RtcEngineController.audienceChannelToken = rtcToken
+ RtcEngineController.rtmToken = rtmToken
+ RtcEngineController.chorusChannelRtcToken = chorusToken
+ findNavController().navigate(R.id.action_mainFragment_to_livingFragment)
+ },
+ failure = {
+ toast("获取 token 异常1")
+ }
+ )
+ } else {
+ TokenGenerator.generateToken("${KeyCenter.channelId}_ad", KeyCenter.localUid.toString(),
+ TokenGenerator.TokenGeneratorType.token007, TokenGenerator.AgoraTokenType.rtc,
+ success = { audienceToken ->
+ TokenGenerator.generateToken(KeyCenter.channelId, "2023",
+ TokenGenerator.TokenGeneratorType.token007, TokenGenerator.AgoraTokenType.rtc,
+ success = { musicToken ->
+ RtcEngineController.chorusChannelRtcToken = rtcToken
+ RtcEngineController.rtmToken = rtmToken
+ RtcEngineController.audienceChannelToken = audienceToken
+ RtcEngineController.musicStreamToken = musicToken
+ findNavController().navigate(R.id.action_mainFragment_to_livingFragment)
+ },
+ failure = {
+ toast("获取 token 异常2")
+ }
+ )
+ },
+ failure = {
+ toast("获取 token 异常3")
+ }
+ )
+ }
+
},
failure = {
- toast("获取 token 异常")
+ toast("获取 token 异常4")
}
)
}
diff --git a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/utils/KeyCenter.kt b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/utils/KeyCenter.kt
index 6c95983..25bc09d 100644
--- a/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/utils/KeyCenter.kt
+++ b/KTVAPI/Android/app/src/main/java/io/agora/ktvdemo/utils/KeyCenter.kt
@@ -12,7 +12,7 @@ object KeyCenter {
/*
* 测试歌曲的 songCode
*/
- const val songCode: Long = 6625526607662280
+ const val songCode: Long = 7162848697922600
/*
* 加入的频道名
@@ -29,6 +29,11 @@ object KeyCenter {
*/
var isMcc: Boolean = true
+ /*
+ * 体验 KTVAPI 的类型, true为普通合唱、false为大合唱
+ */
+ var isNormalChorus: Boolean = true
+
/*
* 当前演唱中的身份
*/
diff --git a/KTVAPI/Android/app/src/main/res/layout/fragment_main.xml b/KTVAPI/Android/app/src/main/res/layout/fragment_main.xml
index 5f6e4e3..e548c44 100644
--- a/KTVAPI/Android/app/src/main/res/layout/fragment_main.xml
+++ b/KTVAPI/Android/app/src/main/res/layout/fragment_main.xml
@@ -87,6 +87,31 @@
android:text="@string/app_local_music_tag" />
+
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/ktvApiType" />
\ No newline at end of file
diff --git a/KTVAPI/Android/app/src/main/res/values-zh/strings.xml b/KTVAPI/Android/app/src/main/res/values-zh/strings.xml
index 1ca7588..51ca264 100644
--- a/KTVAPI/Android/app/src/main/res/values-zh/strings.xml
+++ b/KTVAPI/Android/app/src/main/res/values-zh/strings.xml
@@ -19,5 +19,9 @@
请输入 channel name
MCC 声网歌曲中心
Local 本地音乐
+ 独唱、小合唱
+ 大合唱
开始体验
+ 请检查 gradle.properties 文件内 APPID APP 证书是否正确填写
+ 请检查 gradle.properties 文件内 RESTFUL API KEY 是否正确填写
\ No newline at end of file
diff --git a/KTVAPI/Android/app/src/main/res/values/strings.xml b/KTVAPI/Android/app/src/main/res/values/strings.xml
index 21fa8a9..4324cec 100644
--- a/KTVAPI/Android/app/src/main/res/values/strings.xml
+++ b/KTVAPI/Android/app/src/main/res/values/strings.xml
@@ -18,5 +18,9 @@
请输入 channel name
MCC 声网歌曲中心
Local 本地音乐
+ 独唱、小合唱
+ 大合唱
开始体验
+ 请检查 gradle.properties 文件内 APPID APP 证书是否正确填写
+ 请检查 gradle.properties 文件内 RESTFUL API KEY 是否正确填写
\ No newline at end of file
diff --git a/KTVAPI/Android/app/src/main/res/xml/network_security_config.xml b/KTVAPI/Android/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..dca93c0
--- /dev/null
+++ b/KTVAPI/Android/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/KTVAPI/Android/build.gradle b/KTVAPI/Android/build.gradle
index d690d5c..dfa5f96 100644
--- a/KTVAPI/Android/build.gradle
+++ b/KTVAPI/Android/build.gradle
@@ -15,6 +15,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.19"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
diff --git a/KTVAPI/Android/gradle.properties b/KTVAPI/Android/gradle.properties
index 368969b..ddc3612 100644
--- a/KTVAPI/Android/gradle.properties
+++ b/KTVAPI/Android/gradle.properties
@@ -29,4 +29,8 @@ TOOLBOX_SERVER_HOST=https://service.agora.io/toolbox
#----------- Config Agora Keys -----------
# RTM RTC SDK key Config
AGORA_APP_ID=
-AGORA_APP_CERTIFICATE=
\ No newline at end of file
+AGORA_APP_CERTIFICATE=
+
+# Restful Api Config (Giant Chorus Only)
+RESTFUL_API_KEY=
+RESTFUL_API_SECRET=
\ No newline at end of file
diff --git a/KTVAPI/Android/lib_ktvapi/build.gradle b/KTVAPI/Android/lib_ktvapi/build.gradle
index c8c685d..45697fa 100644
--- a/KTVAPI/Android/lib_ktvapi/build.gradle
+++ b/KTVAPI/Android/lib_ktvapi/build.gradle
@@ -2,6 +2,7 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'maven-publish'
+//apply plugin: 'com.google.protobuf'
android {
namespace 'io.agora.ktv'
@@ -38,8 +39,34 @@ android {
checkReleaseBuilds false
abortOnError false
}
+// sourceSets {
+// main {
+// //实际测试指不指定无所谓,不影响 Java 文件生成
+// proto {
+// srcDir 'src/main'
+// }
+// }
+// }
}
+//protobuf {
+// //配置 protoc 编译器
+// protoc {
+// artifact = 'com.google.protobuf:protoc:3.19.2'
+// }
+// //配置生成目录,编译后会在 build 的目录下生成对应的java文件
+// generateProtoTasks {
+// all().each { task ->
+// task.builtins {
+// remove java
+// }
+// task.builtins {
+// java {}
+// }
+// }
+// }
+//}
+
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
@@ -49,7 +76,9 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
- api "io.agora.rtc:agora-special-full:4.1.1.23"
+ api "io.agora.rtc:agora-special-full:4.3.2.4"
+ implementation 'com.google.protobuf:protobuf-java:3.19.3'
+ implementation 'com.google.protobuf:protobuf-java-util:3.19.3'
}
// Because the components are created only during the afterEvaluate phase, you must
diff --git a/KTVAPI/Android/lib_ktvapi/src/main/java/LrcTime.proto b/KTVAPI/Android/lib_ktvapi/src/main/java/LrcTime.proto
new file mode 100644
index 0000000..d468305
--- /dev/null
+++ b/KTVAPI/Android/lib_ktvapi/src/main/java/LrcTime.proto
@@ -0,0 +1,14 @@
+syntax = "proto3";
+
+enum MsgType {
+ UNKNOWN_TYPE = 0;
+ LRC_TIME = 1001;
+}
+
+message LrcTime {
+ MsgType type = 1;
+ bool forward = 2;
+ int64 ts = 3;
+ string songId = 4;
+ int32 uid = 5;
+}
\ No newline at end of file
diff --git a/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/APIReporter.kt b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/APIReporter.kt
new file mode 100644
index 0000000..b0b0560
--- /dev/null
+++ b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/APIReporter.kt
@@ -0,0 +1,141 @@
+package io.agora.ktvapi
+
+import android.util.Log
+import io.agora.rtc2.Constants
+import io.agora.rtc2.RtcEngine
+import org.json.JSONObject
+import java.util.HashMap
+
+enum class APIType(val value: Int) {
+ KTV(1), // K歌
+ CALL(2), // 呼叫连麦
+ BEAUTY(3), // 美颜
+ VIDEO_LOADER(4), // 秒开秒切
+ PK(5), // 团战
+ VIRTUAL_SPACE(6), //
+ SCREEN_SPACE(7), // 屏幕共享
+ AUDIO_SCENARIO(8) // 音频
+}
+
+enum class ApiEventType(val value: Int) {
+ API(0),
+ COST(1),
+ CUSTOM(2)
+}
+
+object ApiEventKey {
+ const val TYPE = "type"
+ const val DESC = "desc"
+ const val API_VALUE = "apiValue"
+ const val TIMESTAMP = "ts"
+ const val EXT = "ext"
+}
+
+object ApiCostEvent {
+ const val CHANNEL_USAGE = "channelUsage" //频道使用耗时
+ const val FIRST_FRAME_ACTUAL = "firstFrameActual" //首帧实际耗时
+ const val FIRST_FRAME_PERCEIVED = "firstFramePerceived" //首帧感官耗时
+}
+
+class APIReporter(
+ private val type: APIType,
+ private val version: String,
+ private val rtcEngine: RtcEngine
+) {
+ private val tag = "APIReporter"
+ private val messageId = "agora:scenarioAPI"
+ private val durationEventStartMap = HashMap()
+ private val category = "${type.value}_Android_$version"
+
+ init {
+ configParameters()
+ }
+
+ // 上报普通场景化API
+ fun reportFuncEvent(name: String, value: Map, ext: Map) {
+ writeLog("reportFuncEvent: $name value: $value ext: $ext", Constants.LOG_LEVEL_INFO)
+ val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.API.value, ApiEventKey.DESC to name)
+ val labelMap = mapOf(ApiEventKey.API_VALUE to value, ApiEventKey.TIMESTAMP to getCurrentTs(), ApiEventKey.EXT to ext)
+ val event = convertToJSONString(eventMap) ?: ""
+ val label = convertToJSONString(labelMap) ?: ""
+ rtcEngine.sendCustomReportMessage(messageId, category, event, label, 0)
+ }
+
+ fun startDurationEvent(name: String) {
+ Log.d(tag, "startDurationEvent: $name")
+ durationEventStartMap[name] = getCurrentTs()
+ }
+
+ fun endDurationEvent(name: String, ext: Map) {
+ Log.d(tag, "endDurationEvent: $name")
+ val beginTs = durationEventStartMap[name] ?: return
+ durationEventStartMap.remove(name)
+ val ts = getCurrentTs()
+ val cost = (ts - beginTs).toInt()
+
+ innerReportCostEvent(ts, name, cost, ext)
+ }
+
+ // 上报耗时打点信息
+ fun reportCostEvent(name: String, cost: Int, ext: Map) {
+ durationEventStartMap.remove(name)
+ innerReportCostEvent(
+ ts = getCurrentTs(),
+ name = name,
+ cost = cost,
+ ext = ext
+ )
+ }
+
+ // 上报自定义信息
+ fun reportCustomEvent(name: String, ext: Map) {
+ Log.d(tag, "reportCustomEvent: $name ext: $ext")
+ val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.CUSTOM.value, ApiEventKey.DESC to name)
+ val labelMap = mapOf(ApiEventKey.TIMESTAMP to getCurrentTs(), ApiEventKey.EXT to ext)
+ val event = convertToJSONString(eventMap) ?: ""
+ val label = convertToJSONString(labelMap) ?: ""
+ rtcEngine.sendCustomReportMessage(messageId, category, event, label, 0)
+ }
+
+ fun writeLog(content: String, level: Int) {
+ Log.d(tag, content)
+ rtcEngine.writeLog(level, content)
+ }
+
+ fun cleanCache() {
+ durationEventStartMap.clear()
+ }
+
+ // ---------------------- private ----------------------
+
+ private fun configParameters() {
+ //rtcEngine.setParameters("{\"rtc.qos_for_test_purpose\": true}") //测试环境使用
+ // 数据上报
+ rtcEngine.setParameters("{\"rtc.direct_send_custom_event\": true}")
+ // 日志写入
+ rtcEngine.setParameters("{\"rtc.log_external_input\": true}")
+ }
+
+ private fun getCurrentTs(): Long {
+ return System.currentTimeMillis()
+ }
+
+ private fun innerReportCostEvent(ts: Long, name: String, cost: Int, ext: Map) {
+ Log.d(tag, "reportCostEvent: $name cost: $cost ms ext: $ext")
+ writeLog("reportCostEvent: $name cost: $cost ms", Constants.LOG_LEVEL_INFO)
+ val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.COST.value, ApiEventKey.DESC to name)
+ val labelMap = mapOf(ApiEventKey.TIMESTAMP to ts, ApiEventKey.EXT to ext)
+ val event = convertToJSONString(eventMap) ?: ""
+ val label = convertToJSONString(labelMap) ?: ""
+ rtcEngine.sendCustomReportMessage(messageId, category, event, label, cost)
+ }
+
+ private fun convertToJSONString(dictionary: Map): String? {
+ return try {
+ JSONObject(dictionary).toString()
+ } catch (e: Exception) {
+ writeLog("[$tag]convert to json fail: $e dictionary: $dictionary", Constants.LOG_LEVEL_WARNING)
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVApi.kt b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVApi.kt
index 9ecff9a..fe22674 100644
--- a/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVApi.kt
+++ b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVApi.kt
@@ -12,12 +12,12 @@ import io.agora.rtc2.RtcEngine
* KTV场景类型
* @param Normal 普通独唱或多人合唱
* @param SingBattle 嗨歌抢唱
+ * @param SingRelay 抢麦接唱
*/
enum class KTVType(val value: Int) {
Normal(0),
SingBattle(1),
- Cantata(2),
- SingRelay(3)
+ SingRelay(2)
}
/**
@@ -33,9 +33,9 @@ enum class KTVMusicType(val value: Int) {
/**
* 在KTVApi中的身份
* @param SoloSinger 独唱者: 当前只有自己在唱歌
- * @param CoSinger 合唱者: 加入合唱需要通过调用switchSingerRole将切换身份成合唱
+ * @param CoSinger 伴唱: 加入合唱需要通过调用switchSingerRole将切换身份成合唱
* @param LeadSinger 主唱: 有合唱者加入后,需要通过调用switchSingerRole切换身份成主唱
- * @param Audience 观众: 默认状态
+ * @param Audience 听众: 默认状态
*/
enum class KTVSingRole(val value: Int) {
SoloSinger(0),
@@ -45,15 +45,16 @@ enum class KTVSingRole(val value: Int) {
}
/**
- * loadSong失败的原因
+ * loadMusic失败的原因
* @param NO_LYRIC_URL 没有歌词,不影响音乐正常播放
* @param MUSIC_PRELOAD_FAIL 音乐加载失败
* @param CANCELED 本次加载已终止
*/
-enum class KTVLoadSongFailReason(val value: Int) {
+enum class KTVLoadMusicFailReason(val value: Int) {
NO_LYRIC_URL(0),
MUSIC_PRELOAD_FAIL(1),
- CANCELED(2)
+ CANCELED(2),
+ GET_SIMPLE_INFO_FAIL(3)
}
/**
@@ -66,6 +67,17 @@ enum class SwitchRoleFailReason(val value: Int) {
NO_PERMISSION(1)
}
+/**
+ * 加入合唱错误原因
+ * @param JOIN_CHANNEL_FAIL 加入合唱子频道失败
+ * @param MUSIC_OPEN_FAIL 歌曲open失败
+ */
+enum class KTVJoinChorusFailReason(val value: Int) {
+ JOIN_CHANNEL_FAIL(0),
+ MUSIC_OPEN_FAIL(1)
+}
+
+
/**
* 加载音乐的模式
* @param LOAD_MUSIC_ONLY 只加载音乐(通常加入合唱前使用此模式)
@@ -92,7 +104,43 @@ enum class MusicLoadStatus(val value: Int) {
}
/**
- * 歌词组件接口,您setLrcView传入的歌词组件需要继承此接口类,并实现以下三个方法
+ * 音乐音轨模式
+ * @param YUAN_CHANG 原唱:主唱开启原唱后,自己听到原唱,听众听到原唱
+ * @param BAN_ZOU 伴奏:主唱开启伴奏后,自己听到伴奏,听众听到伴奏
+ * @param DAO_CHANG 导唱:主唱开启导唱后,自己听到原唱,听众听到伴奏
+ */
+enum class AudioTrackMode(val value: Int) {
+ YUAN_CHANG(0),
+ BAN_ZOU(1),
+ DAO_CHANG(2),
+}
+
+/**
+ * 大合唱中演唱者互相收听对方音频流的选路策略
+ * @param RANDOM 随机选取几条流
+ * @param BY_DELAY 根据延迟选择最低的几条流
+ * @param TOP_N 根据音强选流
+ * @param BY_DELAY_AND_TOP_N 同时开始延迟选路和音强选流
+ */
+enum class GiantChorusRouteSelectionType(val value: Int) {
+ RANDOM(0),
+ BY_DELAY(1),
+ TOP_N(2),
+ BY_DELAY_AND_TOP_N(3)
+}
+
+/**
+ * 大合唱中演唱者互相收听对方音频流的选路配置
+ * @param type 选路策略
+ * @param streamNum 最大选取的流个数(推荐6)
+ */
+data class GiantChorusRouteSelectionConfig constructor(
+ val type: GiantChorusRouteSelectionType,
+ val streamNum: Int
+)
+
+/**
+ * 歌词组件接口,您setLrcView传入的歌词组件需要继承此接口类,并实现以下几个方法
*/
interface ILrcView {
/**
@@ -109,11 +157,14 @@ interface ILrcView {
/**
* ktvApi获取到歌词地址时会主动调用此方法将歌词地址url传给你的歌词组件,您需要在这个回调内完成歌词的下载
+ * @param url 歌词地址
*/
fun onDownloadLrcData(url: String?)
/**
* ktvApi获取到抢唱切片歌曲副歌片段时间时,会调用此方法回调给歌词组件
+ * @param highStartTime 副歌片段起始时间
+ * @param highEndTime 副歌片段终止时间
*/
fun onHighPartTime(highStartTime: Long, highEndTime: Long)
}
@@ -124,24 +175,25 @@ interface ILrcView {
interface IMusicLoadStateListener {
/**
* 音乐加载成功
- * @param songCode 歌曲编码, 和你loadMusic传入的songCode一致
+ * @param songCode 歌曲编码,和loadMusic传入的songCode一致
* @param lyricUrl 歌词地址
*/
fun onMusicLoadSuccess(songCode: Long, lyricUrl: String)
/**
* 音乐加载失败
+ * @param songCode 加载失败的歌曲编码
* @param reason 歌曲加载失败的原因
*/
- fun onMusicLoadFail(songCode: Long, reason: KTVLoadSongFailReason)
+ fun onMusicLoadFail(songCode: Long, reason: KTVLoadMusicFailReason)
/**
* 音乐加载进度
* @param songCode 歌曲编码
* @param percent 歌曲加载进度
* @param status 歌曲加载的状态
- * @param msg
- * @param lyricUrl
+ * @param msg 相关信息
+ * @param lyricUrl 歌词地址
*/
fun onMusicLoadProgress(songCode: Long, percent: Int, status: MusicLoadStatus, msg: String?, lyricUrl: String?)
}
@@ -162,6 +214,11 @@ interface ISwitchRoleStateListener {
fun onSwitchRoleFail(reason: SwitchRoleFailReason)
}
+interface OnJoinChorusStateListener {
+ fun onJoinChorusSuccess()
+ fun onJoinChorusFail(reason: KTVJoinChorusFailReason)
+}
+
/**
* KTVApi事件回调
*/
@@ -169,11 +226,11 @@ abstract class IKTVApiEventHandler {
/**
* 播放器状态变化
* @param state MediaPlayer 播放状态
- * @param error MediaPlayer Error 信息
+ * @param reason MediaPlayer Error 信息
* @param isLocal 本地还是主唱端的 Player 信息
*/
open fun onMusicPlayerStateChanged(
- state: Constants.MediaPlayerState, error: Constants.MediaPlayerError, isLocal: Boolean
+ state: Constants.MediaPlayerState, reason: Constants.MediaPlayerReason, isLocal: Boolean
) {
}
@@ -229,33 +286,97 @@ data class KTVApiConfig constructor(
val maxCacheSize: Int = 10,
val type: KTVType = KTVType.Normal,
val musicType: KTVMusicType = KTVMusicType.SONG_CODE
-)
+) {
+ override fun toString(): String {
+ return "channelName:$channelName, localUid:$localUid, chorusChannelName:$chorusChannelName, type:$type, musicType:$musicType"
+ }
+}
+
+/**
+ * 初始化KTVGiantChorusApi的配置
+ * @param appId 用来初始化 Mcc Engine
+ * @param rtmToken 创建 Mcc Engine 需要
+ * @param engine RTC engine 对象
+ * @param localUid 创建 Mcc engine 和 加入子频道需要用到
+ * @param audienceChannelName 观众频道名 加入听众频道需要用到
+ * @param chorusChannelToken 观众频道token 加入听众频道需要用到
+ * @param chorusChannelName 演唱频道名 加入演唱频道需要用到
+ * @param chorusChannelToken 演唱频道token 加入演唱频道需要用到
+ * @param musicStreamUid 音乐Uid 主唱推入频道
+ * @param musicStreamToken 音乐流token
+ * @param maxCacheSize 最大缓存歌曲数
+ * @param musicType 音乐类型
+ * @param routeSelectionConfig 选路配置
+ */
+data class KTVGiantChorusApiConfig constructor(
+ val appId: String,
+ val rtmToken: String,
+ val engine: RtcEngine,
+ val localUid: Int,
+ val audienceChannelName: String,
+ val audienceChannelToken: String,
+ val chorusChannelName: String,
+ val chorusChannelToken: String,
+ val musicStreamUid: Int,
+ val musicStreamToken: String,
+ val maxCacheSize: Int = 10,
+ val musicType: KTVMusicType = KTVMusicType.SONG_CODE
+) {
+ override fun toString(): String {
+ return "audienceChannelName:$audienceChannelName, localUid:$localUid, chorusChannelName:$chorusChannelName, musicStreamUid:$musicStreamUid, musicType:$musicType"
+ }
+}
/**
* 加载歌曲的配置,不允许在一首歌没有load完成前(成功/失败均算完成)进行下一首歌的加载
- * @param autoPlay 是否自动播放歌曲(通常主唱选择true)默认为false
- * @param mode 歌曲加载的模式, 默认为音乐和歌词均加载
- * @param songCode 歌曲 id
+ * @param songIdentifier 歌曲 id,通常由业务方给每首歌设置一个不同的SongId用于区分
* @param mainSingerUid 主唱的 Uid,如果是伴唱,伴唱需要根据这个信息 mute 主频道主唱的声音
+ * @param mode 歌曲加载的模式,默认为音乐和歌词均加载
+ * @param needPrelude 播放切片歌曲情况下,是否播放
*/
data class KTVLoadMusicConfiguration(
val songIdentifier: String,
- val autoPlay: Boolean = false,
val mainSingerUid: Int,
- val mode: KTVLoadMusicMode = KTVLoadMusicMode.LOAD_MUSIC_AND_LRC
-)
+ val mode: KTVLoadMusicMode = KTVLoadMusicMode.LOAD_MUSIC_AND_LRC,
+ val needPrelude: Boolean = false
+) {
+ override fun toString(): String {
+ return "songIdentifier:$songIdentifier, mainSingerUid:$mainSingerUid, mode:$mode, needPrelude:$needPrelude"
+ }
+}
/**
- * 获取 KTVApi 实例
+ * 创建普通合唱KTVApi实例
*/
-fun createKTVApi(): KTVApi = KTVApiImpl()
+fun createKTVApi(config: KTVApiConfig): KTVApi = KTVApiImpl(config)
+/**
+ * 创建大合唱KTVApi实例
+ */
+fun createKTVGiantChorusApi(config: KTVGiantChorusApiConfig): KTVApi = KTVGiantChorusApiImpl(config)
+
+/**
+ * KTVApi 接口
+ */
interface KTVApi {
- /**
- * 初始化内部变量/缓存数据,并注册相应的监听,必须在其他KTVApi调用前调用initialize初始化KTVApi
- * @param config 初始化KTVApi的配置
- */
- fun initialize(config: KTVApiConfig)
+
+ companion object {
+ // 听到远端的音量
+ var remoteVolume: Int = 30
+ // 本地mpk播放音量
+ var mpkPlayoutVolume: Int = 50
+ // mpk发布音量
+ var mpkPublishVolume: Int = 50
+
+ // 是否使用音频自采集
+ var useCustomAudioSource = false
+ // 调试使用,会输出更多的日志
+ var debugMode = false
+ // 内部测试使用,无需关注
+ var mccDomain = ""
+ // 大合唱的选路策略
+ var routeSelectionConfig = GiantChorusRouteSelectionConfig(GiantChorusRouteSelectionType.BY_DELAY, 6)
+ }
/**
* 更新ktvapi内部使用的streamId,每次加入频道需要更新内部streamId
@@ -279,11 +400,6 @@ interface KTVApi {
*/
fun release()
- /**
- * 开启关闭专业模式
- */
- fun enableProfessionalStreamerMode(enable: Boolean)
-
/**
* 收到 IKTVApiEventHandler.onTokenPrivilegeWillExpire 回调时需要主动调用方法更新Token
* @param rtmToken musicContentCenter模块需要的rtm token
@@ -300,7 +416,7 @@ interface KTVApi {
*/
fun fetchMusicCharts(
onMusicChartResultListener: (
- requestId: String?, // TODO 不需要?
+ requestId: String?,
status: Int, // status=2 时token过期
list: Array?
) -> Unit
@@ -320,7 +436,7 @@ interface KTVApi {
pageSize: Int,
jsonOption: String,
onMusicCollectionResultListener: (
- requestId: String?, // TODO 不需要?
+ requestId: String?,
status: Int, // status=2 时token过期
page: Int,
pageSize: Int,
@@ -341,7 +457,7 @@ interface KTVApi {
page: Int, pageSize: Int,
jsonOption: String,
onMusicCollectionResultListener: (
- requestId: String?, // TODO 不需要?
+ requestId: String?,
status: Int, // status=2 时token过期
page: Int,
pageSize: Int,
@@ -358,10 +474,10 @@ interface KTVApi {
*
* 推荐调用:
* 歌曲开始时:
- * 主唱 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(mode=LOAD_MUSIC_AND_LRC, songCode, mainSingerUid)) switchSingerRole(SoloSinger)
+ * 观众 loadMusic(KTVLoadMusicConfiguration(mode=LOAD_LRC_ONLY, songCode, mainSingerUid))
* 加入合唱时:
- * 准备加入合唱者:loadMusic(KTVLoadMusicConfiguration(autoPlay=false, mode=LOAD_MUSIC_ONLY, songCode, mainSingerUid))
+ * 准备加入合唱者:loadMusic(KTVLoadMusicConfiguration(mode=LOAD_MUSIC_ONLY, songCode, mainSingerUid))
* loadMusic成功后switchSingerRole(CoSinger)
*/
fun loadMusic(
@@ -378,15 +494,15 @@ interface KTVApi {
/**
* 加载歌曲,同时只能为一首歌loadSong,同步调用, 一般使用此loadSong是歌曲已经preload成功(url为本地文件地址)
- * @param config 加载歌曲配置
* @param url 歌曲地址
+ * @param config 加载歌曲配置
*
* 推荐调用:
* 歌曲开始时:
- * 主唱 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(mode=LOAD_MUSIC_AND_LRC, url, mainSingerUid)) switchSingerRole(SoloSinger)
+ * 观众 loadMusic(KTVLoadMusicConfiguration(mode=LOAD_LRC_ONLY, url, mainSingerUid))
* 加入合唱时:
- * 准备加入合唱者:loadMusic(KTVLoadMusicConfiguration(autoPlay=false, mode=LOAD_MUSIC_ONLY, url, mainSingerUid))
+ * 准备加入合唱者:loadMusic(KTVLoadMusicConfiguration(mode=LOAD_MUSIC_ONLY, url, mainSingerUid))
* loadMusic成功后switchSingerRole(CoSinger)
*/
fun loadMusic(
@@ -396,17 +512,17 @@ interface KTVApi {
/**
* 加载歌曲,同时只能为一首歌loadSong,同步调用, 一般使用此loadSong是歌曲已经preload成功(url为本地文件地址)
- * @param config 加载歌曲配置,config.autoPlay = true,默认播放url1
+ * @param config 加载歌曲配置,默认播放url1
* @param url1 歌曲地址1
* @param url2 歌曲地址2
*
*
* 推荐调用:
* 歌曲开始时:
- * 主唱 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(mode=LOAD_MUSIC_AND_LRC, url, mainSingerUid)) switchSingerRole(SoloSinger)
+ * 观众 loadMusic(KTVLoadMusicConfiguration(mode=LOAD_LRC_ONLY, url, mainSingerUid))
* 加入合唱时:
- * 准备加入合唱者:loadMusic(KTVLoadMusicConfiguration(autoPlay=false, mode=LOAD_MUSIC_ONLY, url, mainSingerUid))
+ * 准备加入合唱者:loadMusic(KTVLoadMusicConfiguration(mode=LOAD_MUSIC_ONLY, url, mainSingerUid))
* loadMusic成功后switchSingerRole(CoSinger)
*/
fun load2Music(
@@ -446,9 +562,6 @@ interface KTVApi {
* 播放歌曲
* @param songCode 歌曲唯一编码
* @param startPos 开始播放的位置
- * 对于主唱:
- * 如果loadMusic时你选择了autoPlay = true 则不需要主动调用startSing
- * 如果loadMusic时你选择了autoPlay = false 则需要在loadMusic成功后调用startSing
*/
fun startSing(songCode: Long, startPos: Long)
@@ -456,9 +569,6 @@ interface KTVApi {
* 播放歌曲
* @param url 歌曲地址
* @param startPos 开始播放的位置
- * 对于主唱:
- * 如果loadMusic时你选择了autoPlay = true 则不需要主动调用startSing
- * 如果loadMusic时你选择了autoPlay = false 则需要在loadMusic成功后调用startSing
*/
fun startSing(url: String, startPos: Long)
@@ -504,4 +614,19 @@ interface KTVApi {
* 获取mcc实例
*/
fun getMusicContentCenter() : IAgoraMusicContentCenter
+
+ /**
+ * 切换音轨, 原唱/伴奏/导唱
+ */
+ fun switchAudioTrack(mode: AudioTrackMode)
+
+ /**
+ * 开启关闭专业模式,默认关
+ */
+ fun enableProfessionalStreamerMode(enable: Boolean)
+
+ /**
+ * 开启 Multipathing, 默认开
+ */
+ fun enableMulitpathing(enable: Boolean)
}
\ No newline at end of file
diff --git a/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVApiImpl.kt b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVApiImpl.kt
index 798d9e8..732ce74 100644
--- a/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVApiImpl.kt
+++ b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVApiImpl.kt
@@ -6,48 +6,34 @@ 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.CacheStatistics
+import io.agora.mediaplayer.data.PlayerPlaybackStats
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 io.agora.rtc2.internal.Logging
import org.json.JSONException
import org.json.JSONObject
import java.util.concurrent.*
-/**
- * 加入合唱错误原因
- */
-enum class KTVJoinChorusFailReason(val value: Int) {
- JOIN_CHANNEL_FAIL(0), // 加入channel2失败
- MUSIC_OPEN_FAIL(1) // 歌曲open失败
-}
+class KTVApiImpl(
+ val ktvApiConfig: KTVApiConfig
+) : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver, IRtcEngineEventHandler() {
-interface OnJoinChorusStateListener {
- fun onJoinChorusSuccess()
- fun onJoinChorusFail(reason: KTVJoinChorusFailReason)
-}
-
-class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver,
- IRtcEngineEventHandler() {
- private val tag: String = "KTV_API_LOG"
- var debugMode = false
-
- // 外部可修改
- var useCustomAudioSource:Boolean = false
-
- // 音频最佳实践
- var remoteVolume: Int = 30 // 远端音频
- var mpkPlayoutVolume: Int = 50
- var mpkPublishVolume: Int = 50
+ companion object {
+ private val scheduledThreadPool: ScheduledExecutorService = Executors.newScheduledThreadPool(5)
+ const val tag = "KTV_API_LOG"
+ const val version = "5.0.0"
+ const val lyricSyncVersion = 2
+ }
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
- private lateinit var mRtcEngine: RtcEngineEx
+ private var mRtcEngine: RtcEngineEx = ktvApiConfig.engine as RtcEngineEx
private lateinit var mMusicCenter: IAgoraMusicContentCenter
- private lateinit var mPlayer: IMediaPlayer
+ private var mPlayer: IMediaPlayer
+ private val apiReporter: APIReporter = APIReporter(APIType.KTV, version, mRtcEngine)
- private lateinit var ktvApiConfig: KTVApiConfig
private var innerDataStreamId: Int = 0
private var subChorusConnection: RtcConnection? = null
@@ -60,6 +46,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
private val lyricCallbackMap =
mutableMapOf Unit>() // (requestId, callback)
private val lyricSongCodeMap = mutableMapOf() // (requestId, songCode)
+ private val simpleInfoCallbackMap = mutableMapOf Unit>() // (requestId, callback)
private val loadMusicCallbackMap =
mutableMapOf? = null
+ private val displayLrcTask = object : Runnable {
+ override fun run() {
+ if (!mStopDisplayLrc) {
+ if (singerRole == KTVSingRole.Audience && !recvFromDataStream) return // audioMetaData方案观众return
+ val lastReceivedTime = mLastReceivedPlayPosTime ?: return
+ val curTime = System.currentTimeMillis()
+ val offset = curTime - lastReceivedTime
+ if (offset <= 1000) {
+ val curTs = mReceivedPlayPosition + offset + highStartTime
+ if (singerRole == KTVSingRole.LeadSinger || singerRole == KTVSingRole.SoloSinger) {
+ val lrcTime = LrcTimeOuterClass.LrcTime.newBuilder()
+ .setTypeValue(LrcTimeOuterClass.MsgType.LRC_TIME.number)
+ .setForward(true)
+ .setSongId(songIdentifier)
+ .setTs(curTs)
+ .setUid(ktvApiConfig.localUid)
+ .build()
+
+ mRtcEngine.sendAudioMetadata(lrcTime.toByteArray())
+ }
+ 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
+ }
+ }
+ }
+ }
}
- override fun initialize(
- config: KTVApiConfig
- ) {
- this.mRtcEngine = config.engine as RtcEngineEx
-
- reportCallScenarioApi("initialize", JSONObject().put("config", config))
- this.ktvApiConfig = config
+ // 音高同步
+ private var mStopSyncPitch = true
+ private var mSyncPitchFuture :ScheduledFuture<*>? = null
+ private val mSyncPitchTask = Runnable {
+ if (!mStopSyncPitch) {
+ if (ktvApiConfig.type == KTVType.SingRelay &&
+ (singerRole == KTVSingRole.LeadSinger || singerRole == KTVSingRole.SoloSinger || singerRole == KTVSingRole.CoSinger) &&
+ isOnMicOpen) {
+ sendSyncPitch(pitch)
+ } else if (mediaPlayerState == MediaPlayerState.PLAYER_STATE_PLAYING &&
+ (singerRole == KTVSingRole.LeadSinger || singerRole == KTVSingRole.SoloSinger)) {
+ sendSyncPitch(pitch)
+ }
+ }
+ }
- // ------------------ 初始化内容中心 ------------------
- if (config.musicType == KTVMusicType.SONG_CODE) {
+ init {
+ apiReporter.reportFuncEvent("initialize", mapOf("config" to ktvApiConfig.toString()), mapOf())
+ if (ktvApiConfig.musicType == KTVMusicType.SONG_CODE) {
val contentCenterConfiguration = MusicContentCenterConfiguration()
- contentCenterConfiguration.appId = config.appId
+ contentCenterConfiguration.appId = ktvApiConfig.appId
contentCenterConfiguration.mccUid = ktvApiConfig.localUid.toLong()
- contentCenterConfiguration.token = config.rtmToken
- contentCenterConfiguration.maxCacheSize = config.maxCacheSize
- if (debugMode) {
- contentCenterConfiguration.mccDomain = "api-test.agora.io"
+ contentCenterConfiguration.token = ktvApiConfig.rtmToken
+ contentCenterConfiguration.maxCacheSize = ktvApiConfig.maxCacheSize
+ if (KTVApi.debugMode) {
+ contentCenterConfiguration.mccDomain = KTVApi.mccDomain
}
mMusicCenter = IAgoraMusicContentCenter.create(mRtcEngine)
mMusicCenter.initialize(contentCenterConfiguration)
@@ -152,8 +169,8 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
} else {
mPlayer = mRtcEngine.createMediaPlayer()
}
- mPlayer.adjustPublishSignalVolume(mpkPublishVolume)
- mPlayer.adjustPlayoutVolume(mpkPlayoutVolume)
+ mPlayer.adjustPublishSignalVolume(KTVApi.mpkPublishVolume)
+ mPlayer.adjustPlayoutVolume(KTVApi.mpkPlayoutVolume)
// 注册回调
mRtcEngine.addHandler(this)
@@ -165,13 +182,26 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
startSyncPitch()
isRelease = false
- if (config.type == KTVType.SingRelay) {
- this.remoteVolume = 100
+ if (ktvApiConfig.type == KTVType.SingRelay) {
+ KTVApi.remoteVolume = 100
}
+ mPlayer.setPlayerOption("play_pos_change_callback", 100)
+ }
+
+ // 日志输出
+ private fun ktvApiLog(msg: String) {
+ if (isRelease) return
+ apiReporter.writeLog("[$tag][${ktvApiConfig.type}] $msg", LOG_LEVEL_INFO)
+ }
+
+ // 日志输出
+ private fun ktvApiLogError(msg: String) {
+ if (isRelease) return
+ apiReporter.writeLog("[$tag][${ktvApiConfig.type}] $msg", LOG_LEVEL_ERROR)
}
override fun renewInnerDataStreamId() {
- reportCallScenarioApi("renewInnerDataStreamId", JSONObject())
+ apiReporter.reportFuncEvent("renewInnerDataStreamId", mapOf(), mapOf())
val innerCfg = DataStreamConfig()
innerCfg.syncWithAudio = true
@@ -209,24 +239,44 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
if (ktvApiConfig.type == KTVType.SingRelay) {
mRtcEngine.setParameters("{\"che.audio.aiaec.working_mode\":1}")
}
+
+ // 歌词强同步需要在audio4环境
+ mRtcEngine.setParameters("{\"rtc.use_audio4\": true}")
+
+ // mutipath
+ enableMultipathing = true
+ //mRtcEngine.setParameters("{\"rtc.enableMultipath\": true}")
+ mRtcEngine.setParameters("{\"rtc.enable_tds_request_on_join\": true}")
+ //mRtcEngine.setParameters("{\"rtc.remote_path_scheduling_strategy\": 0}")
+ //mRtcEngine.setParameters("{\"rtc.path_scheduling_strategy\": 0}")
+ }
+
+ private fun resetParameters() {
+ mRtcEngine.setAudioScenario(AUDIO_SCENARIO_GAME_STREAMING)
+ mRtcEngine.setParameters("{\"che.audio.custom_bitrate\": 80000}") // 兼容之前的profile = 3设置
+ mRtcEngine.setParameters("{\"che.audio.max_mixed_participants\": 3}") // 正常3路下行流混流
+ mRtcEngine.setParameters("{\"che.audio.neteq.prebuffer\": false}") // 关闭 接收端快速对齐模式
+ mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp\": false}") // 观众关闭 多端同步
+ mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\": false}") //主播关闭多端同步
}
override fun addEventHandler(ktvApiEventHandler: IKTVApiEventHandler) {
- reportCallScenarioApi("addEventHandler", JSONObject())
+ apiReporter.reportFuncEvent("addEventHandler", mapOf("ktvApiEventHandler" to ktvApiEventHandler), mapOf())
ktvApiEventHandlerList.add(ktvApiEventHandler)
}
override fun removeEventHandler(ktvApiEventHandler: IKTVApiEventHandler) {
- reportCallScenarioApi("removeEventHandler", JSONObject())
+ apiReporter.reportFuncEvent("removeEventHandler", mapOf("ktvApiEventHandler" to ktvApiEventHandler), mapOf())
ktvApiEventHandlerList.remove(ktvApiEventHandler)
}
override fun release() {
- reportCallScenarioApi("release", JSONObject())
+ apiReporter.reportFuncEvent("release", mapOf(), mapOf())
if (isRelease) return
isRelease = true
singerRole = KTVSingRole.Audience
+ resetParameters()
stopSyncPitch()
stopDisplayLrc()
this.mLastReceivedPlayPosTime = null
@@ -237,6 +287,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
loadMusicCallbackMap.clear()
musicChartsCallbackMap.clear()
musicCollectionCallbackMap.clear()
+ simpleInfoCallbackMap.clear()
lrcView = null
mRtcEngine.removeHandler(this)
@@ -257,30 +308,31 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
}
override fun enableProfessionalStreamerMode(enable: Boolean) {
- reportCallScenarioApi("enableProfessionalStreamerMode", JSONObject())
+ apiReporter.reportFuncEvent("enableProfessionalStreamerMode", mapOf("enable" to enable), mapOf())
this.professionalModeOpen = enable
processAudioProfessionalProfile()
}
+ // 专业模式
private fun processAudioProfessionalProfile() {
ktvApiLog("processAudioProfessionalProfile: audioRouting: $audioRouting, professionalModeOpen: $professionalModeOpen, isPublishAudio:$isPublishAudio")
if (!isPublishAudio) return // 必须为麦上者
if (professionalModeOpen) {
// 专业
- if (audioRouting == 0 || audioRouting == 2 || audioRouting == 5 || audioRouting == 6) {
+ if (audioRouting == AUDIO_ROUTE_HEADSET || audioRouting == AUDIO_ROUTE_HEADSETNOMIC || audioRouting == AUDIO_ROUTE_BLUETOOTH_DEVICE_HFP || audioRouting == AUDIO_ROUTE_USBDEVICE || audioRouting == AUDIO_ROUTE_BLUETOOTH_DEVICE_A2DP) {
// 耳机 关闭3A 关闭md
mRtcEngine.setParameters("{\"che.audio.aec.enable\": false}")
mRtcEngine.setParameters("{\"che.audio.agc.enable\": false}")
mRtcEngine.setParameters("{\"che.audio.ans.enable\": false}")
mRtcEngine.setParameters("{\"che.audio.md.enable\": false}")
- mRtcEngine.setAudioProfile(5) // AgoraAudioProfileMusicHighQualityStereo
+ mRtcEngine.setAudioProfile(AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO) // AgoraAudioProfileMusicHighQualityStereo
} else {
// 非耳机 开启3A 关闭md
mRtcEngine.setParameters("{\"che.audio.aec.enable\": true}")
mRtcEngine.setParameters("{\"che.audio.agc.enable\": true}")
mRtcEngine.setParameters("{\"che.audio.ans.enable\": true}")
mRtcEngine.setParameters("{\"che.audio.md.enable\": false}")
- mRtcEngine.setAudioProfile(5) // AgoraAudioProfileMusicHighQualityStereo
+ mRtcEngine.setAudioProfile(AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO) // AgoraAudioProfileMusicHighQualityStereo
}
} else {
// 非专业 开启3A 关闭md
@@ -288,12 +340,44 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
mRtcEngine.setParameters("{\"che.audio.agc.enable\": true}")
mRtcEngine.setParameters("{\"che.audio.ans.enable\": true}")
mRtcEngine.setParameters("{\"che.audio.md.enable\": false}")
- mRtcEngine.setAudioProfile(3) // AgoraAudioProfileMusicStandardStereo
+ mRtcEngine.setAudioProfile(AUDIO_PROFILE_MUSIC_STANDARD_STEREO) // AgoraAudioProfileMusicStandardStereo
+ }
+ }
+
+ override fun enableMulitpathing(enable: Boolean) {
+ apiReporter.reportFuncEvent("enableMulitpathing", mapOf("enable" to enable), mapOf())
+ this.enableMultipathing = enable
+
+ if (singerRole == KTVSingRole.LeadSinger || singerRole == KTVSingRole.CoSinger) {
+ subChorusConnection?.let {
+ mRtcEngine.setParametersEx("{\"rtc.enableMultipath\": $enable, \"rtc.path_scheduling_strategy\": 0, \"rtc.remote_path_scheduling_strategy\": 0}", it)
+ }
+ }
+ }
+
+ override fun switchAudioTrack(mode: AudioTrackMode) {
+ apiReporter.reportFuncEvent("switchAudioTrack", mapOf("mode" to mode), mapOf())
+ when (singerRole) {
+ KTVSingRole.LeadSinger, KTVSingRole.SoloSinger -> {
+ when (mode) {
+ AudioTrackMode.YUAN_CHANG -> mPlayer.selectMultiAudioTrack(0, 0)
+ AudioTrackMode.BAN_ZOU -> mPlayer.selectMultiAudioTrack(1, 1)
+ AudioTrackMode.DAO_CHANG -> mPlayer.selectMultiAudioTrack(0, 1)
+ }
+ }
+ KTVSingRole.CoSinger -> {
+ when (mode) {
+ AudioTrackMode.YUAN_CHANG -> mPlayer.selectAudioTrack(0)
+ AudioTrackMode.BAN_ZOU -> mPlayer.selectAudioTrack(1)
+ AudioTrackMode.DAO_CHANG -> ktvApiLogError("CoSinger can not switch to DAO_CHANG")
+ }
+ }
+ KTVSingRole.Audience -> ktvApiLogError("CoSinger can not switch audio track")
}
}
override fun renewToken(rtmToken: String, chorusChannelRtcToken: String) {
- reportCallScenarioApi("renewToken", JSONObject().put("rtmToken", rtmToken).put("chorusChannelRtcToken", chorusChannelRtcToken))
+ apiReporter.reportFuncEvent("renewToken", mapOf(), mapOf())
// 更新RtmToken
mMusicCenter.renewToken(rtmToken)
// 更新合唱频道RtcToken
@@ -319,16 +403,18 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
newRole: KTVSingRole,
switchRoleStateListener: ISwitchRoleStateListener?
) {
- reportCallScenarioApi("switchSingerRole", JSONObject().put("newRole", newRole))
+ apiReporter.reportFuncEvent("switchSingerRole", mapOf("newRole" to newRole), mapOf())
val oldRole = singerRole
// 调整开关麦状态
- if ((oldRole == KTVSingRole.LeadSinger || oldRole == KTVSingRole.SoloSinger) && (newRole == KTVSingRole.CoSinger || newRole == KTVSingRole.Audience) && !isOnMicOpen) {
- mRtcEngine.muteLocalAudioStream(true)
- mRtcEngine.adjustRecordingSignalVolume(100)
- } else if ((oldRole == KTVSingRole.Audience || oldRole == KTVSingRole.CoSinger) && (newRole == KTVSingRole.LeadSinger || newRole == KTVSingRole.SoloSinger) && !isOnMicOpen) {
- mRtcEngine.adjustRecordingSignalVolume(0)
- mRtcEngine.muteLocalAudioStream(false)
+ if (ktvApiConfig.type != KTVType.SingRelay) {
+ if ((oldRole == KTVSingRole.LeadSinger || oldRole == KTVSingRole.SoloSinger) && (newRole == KTVSingRole.CoSinger || newRole == KTVSingRole.Audience) && !isOnMicOpen) {
+ mRtcEngine.muteLocalAudioStream(true)
+ mRtcEngine.adjustRecordingSignalVolume(100)
+ } else if ((oldRole == KTVSingRole.Audience || oldRole == KTVSingRole.CoSinger) && (newRole == KTVSingRole.LeadSinger || newRole == KTVSingRole.SoloSinger) && !isOnMicOpen) {
+ mRtcEngine.adjustRecordingSignalVolume(0)
+ mRtcEngine.muteLocalAudioStream(false)
+ }
}
if (this.singerRole == KTVSingRole.Audience && newRole == KTVSingRole.SoloSinger) {
@@ -342,16 +428,20 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
becomeSoloSinger()
joinChorus(newRole, ktvApiConfig.chorusChannelToken, object : OnJoinChorusStateListener {
override fun onJoinChorusSuccess() {
- ktvApiLog("onJoinChorusSuccess")
- singerRole = newRole
- ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
- switchRoleStateListener?.onSwitchRoleSuccess()
+ runOnMainThread {
+ ktvApiLog("onJoinChorusSuccess")
+ singerRole = newRole
+ ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
+ switchRoleStateListener?.onSwitchRoleSuccess()
+ }
}
override fun onJoinChorusFail(reason: KTVJoinChorusFailReason) {
- ktvApiLog("onJoinChorusFail reason:$reason")
- leaveChorus(newRole)
- switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.JOIN_CHANNEL_FAIL)
+ runOnMainThread {
+ ktvApiLog("onJoinChorusFail reason:$reason")
+ leaveChorus(newRole)
+ switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.JOIN_CHANNEL_FAIL)
+ }
}
})
} else if (this.singerRole == KTVSingRole.SoloSinger && newRole == KTVSingRole.Audience) {
@@ -366,16 +456,20 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
// 4、Audience -》CoSinger
joinChorus(newRole, ktvApiConfig.chorusChannelToken, object : OnJoinChorusStateListener {
override fun onJoinChorusSuccess() {
- ktvApiLog("onJoinChorusSuccess")
- singerRole = newRole
- switchRoleStateListener?.onSwitchRoleSuccess()
- ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
+ runOnMainThread {
+ ktvApiLog("onJoinChorusSuccess")
+ singerRole = newRole
+ switchRoleStateListener?.onSwitchRoleSuccess()
+ ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
+ }
}
override fun onJoinChorusFail(reason: KTVJoinChorusFailReason) {
- ktvApiLog("onJoinChorusFail reason:$reason")
- leaveChorus(newRole)
- switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.JOIN_CHANNEL_FAIL)
+ runOnMainThread {
+ ktvApiLog("onJoinChorusFail reason:$reason")
+ leaveChorus(newRole)
+ switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.JOIN_CHANNEL_FAIL)
+ }
}
})
@@ -392,16 +486,20 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
joinChorus(newRole, ktvApiConfig.chorusChannelToken, object : OnJoinChorusStateListener {
override fun onJoinChorusSuccess() {
- ktvApiLog("onJoinChorusSuccess")
- singerRole = newRole
- switchRoleStateListener?.onSwitchRoleSuccess()
- ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
+ runOnMainThread {
+ ktvApiLog("onJoinChorusSuccess")
+ singerRole = newRole
+ switchRoleStateListener?.onSwitchRoleSuccess()
+ ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
+ }
}
override fun onJoinChorusFail(reason: KTVJoinChorusFailReason) {
- ktvApiLog("onJoinChorusFail reason:$reason")
- leaveChorus(newRole)
- switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.JOIN_CHANNEL_FAIL)
+ runOnMainThread {
+ ktvApiLog("onJoinChorusFail reason:$reason")
+ leaveChorus(newRole)
+ switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.JOIN_CHANNEL_FAIL)
+ }
}
})
} else if (this.singerRole == KTVSingRole.LeadSinger && newRole == KTVSingRole.SoloSinger) {
@@ -450,7 +548,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
}
override fun fetchMusicCharts(onMusicChartResultListener: (requestId: String?, status: Int, list: Array?) -> Unit) {
- reportCallScenarioApi("fetchMusicCharts", JSONObject())
+ apiReporter.reportFuncEvent("fetchMusicCharts", mapOf(), mapOf())
val requestId = mMusicCenter.musicCharts
musicChartsCallbackMap[requestId] = onMusicChartResultListener
}
@@ -462,7 +560,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
jsonOption: String,
onMusicCollectionResultListener: (requestId: String?, status: Int, page: Int, pageSize: Int, total: Int, list: Array?) -> Unit
) {
- reportCallScenarioApi("searchMusicByMusicChartId", JSONObject())
+ apiReporter.reportFuncEvent("searchMusicByMusicChartId", mapOf("musicChartId" to musicChartId, "page" to page, "pageSize" to pageSize, "jsonOption" to jsonOption), mapOf())
val requestId =
mMusicCenter.getMusicCollectionByMusicChartId(musicChartId, page, pageSize, jsonOption)
musicCollectionCallbackMap[requestId] = onMusicCollectionResultListener
@@ -475,7 +573,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
jsonOption: String,
onMusicCollectionResultListener: (requestId: String?, status: Int, page: Int, pageSize: Int, total: Int, list: Array?) -> Unit
) {
- reportCallScenarioApi("searchMusicByKeyword", JSONObject())
+ apiReporter.reportFuncEvent("searchMusicByKeyword", mapOf(), mapOf())
val requestId = mMusicCenter.searchMusic(keyword, page, pageSize, jsonOption)
musicCollectionCallbackMap[requestId] = onMusicCollectionResultListener
}
@@ -485,15 +583,14 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
config: KTVLoadMusicConfiguration,
musicLoadStateListener: IMusicLoadStateListener
) {
- reportCallScenarioApi("loadMusic", JSONObject().put("songCode", songCode).put("config", config))
+ apiReporter.reportFuncEvent("loadMusic", mapOf("songCode" to songCode, "config" to config), mapOf())
ktvApiLog("loadMusic called: songCode $songCode")
- if (this.ktvApiConfig.type == KTVType.SingBattle) {
- mMusicCenter.getSongSimpleInfo(songCode)
- }
+
// 设置到全局, 连续调用以最新的为准
this.songCode = songCode
this.songIdentifier = config.songIdentifier
this.mainSingerUid = config.mainSingerUid
+ this.needPrelude = config.needPrelude
mLastReceivedPlayPosTime = null
mReceivedPlayPosition = 0
@@ -507,19 +604,31 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
if (this.songCode != song) {
// 当前歌曲已发生变化,以最新load歌曲为准
ktvApiLogError("loadMusic failed: CANCELED")
- musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.CANCELED)
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.CANCELED)
return@loadLyric
}
if (lyricUrl == null) {
// 加载歌词失败
ktvApiLogError("loadMusic failed: NO_LYRIC_URL")
- musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.NO_LYRIC_URL)
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.NO_LYRIC_URL)
} else {
// 加载歌词成功
ktvApiLog("loadMusic success")
lrcView?.onDownloadLrcData(lyricUrl)
- musicLoadStateListener.onMusicLoadSuccess(song, lyricUrl)
+ if (this.ktvApiConfig.type != KTVType.SingBattle) {
+ musicLoadStateListener.onMusicLoadSuccess(song, lyricUrl)
+ } else {
+ getSongSimpleInfo(songCode) { code, success ->
+ if (success) {
+ musicLoadStateListener.onMusicLoadSuccess(song, lyricUrl)
+ } else {
+ musicLoadStateListener.onMusicLoadFail(code,
+ KTVLoadMusicFailReason.GET_SIMPLE_INFO_FAIL
+ )
+ }
+ }
+ }
}
}
return
@@ -532,7 +641,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
if (this.songCode != song) {
// 当前歌曲已发生变化,以最新load歌曲为准
ktvApiLogError("loadMusic failed: CANCELED")
- musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.CANCELED)
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.CANCELED)
return@preLoadMusic
}
if (config.mode == KTVLoadMusicMode.LOAD_MUSIC_AND_LRC) {
@@ -541,59 +650,68 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
if (this.songCode != song) {
// 当前歌曲已发生变化,以最新load歌曲为准
ktvApiLogError("loadMusic failed: CANCELED")
- musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.CANCELED)
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.CANCELED)
return@loadLyric
}
if (lyricUrl == null) {
// 加载歌词失败
ktvApiLogError("loadMusic failed: NO_LYRIC_URL")
- musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.NO_LYRIC_URL)
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.NO_LYRIC_URL)
} else {
// 加载歌词成功
ktvApiLog("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)
+ if (this.ktvApiConfig.type != KTVType.SingBattle) {
+ musicLoadStateListener.onMusicLoadSuccess(song, lyricUrl)
+ } else {
+ getSongSimpleInfo(songCode) { code, success ->
+ if (success) {
+ musicLoadStateListener.onMusicLoadSuccess(song, lyricUrl)
+ } else {
+ musicLoadStateListener.onMusicLoadFail(code,
+ KTVLoadMusicFailReason.GET_SIMPLE_INFO_FAIL
+ )
+ }
+ }
}
- startSing(song, 0)
}
}
} else if (config.mode == KTVLoadMusicMode.LOAD_MUSIC_ONLY) {
// 不需要加载歌词
ktvApiLog("loadMusic success")
- if (config.autoPlay) {
- // 主唱自动播放歌曲
- if (this.singerRole != KTVSingRole.LeadSinger) {
- switchSingerRole(KTVSingRole.SoloSinger, null)
+ musicLoadStateListener.onMusicLoadProgress(song, 100, MusicLoadStatus.COMPLETED, msg, lrcUrl)
+ if (this.ktvApiConfig.type != KTVType.SingBattle) {
+ musicLoadStateListener.onMusicLoadSuccess(song, "")
+ } else {
+ getSongSimpleInfo(songCode) { code, success ->
+ if (success) {
+ musicLoadStateListener.onMusicLoadSuccess(song, "")
+ } else {
+ musicLoadStateListener.onMusicLoadFail(code,
+ KTVLoadMusicFailReason.GET_SIMPLE_INFO_FAIL
+ )
+ }
}
- 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 if (status == 3) {
// 主动停止下载
- musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.CANCELED)
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.CANCELED)
} else {
// 预加载歌曲失败
ktvApiLogError("loadMusic failed: MUSIC_PRELOAD_FAIL")
- musicLoadStateListener.onMusicLoadFail(song, KTVLoadSongFailReason.MUSIC_PRELOAD_FAIL)
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.MUSIC_PRELOAD_FAIL)
}
}
}
override fun removeMusic(songCode: Long) {
- reportCallScenarioApi("removeMusic", JSONObject().put("songCode", songCode))
+ apiReporter.reportFuncEvent("removeMusic", mapOf("songCode" to songCode), mapOf())
val ret = mMusicCenter.removeCache(songCode)
if (ret < 0) {
ktvApiLogError("removeMusic failed, ret: $ret")
@@ -604,38 +722,24 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
url: String,
config: KTVLoadMusicConfiguration
) {
- reportCallScenarioApi("loadMusic", JSONObject().put("url", url).put("config", config))
+ apiReporter.reportFuncEvent("loadMusic", mapOf("url" to url, "config" to config), mapOf())
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)
- }
+ this.needPrelude = config.needPrelude
}
override fun load2Music(url1: String, url2: String, config: KTVLoadMusicConfiguration) {
- reportCallScenarioApi("load2Music", JSONObject().put("url1", url1).put("url2", url2).put("config", config))
+ apiReporter.reportFuncEvent("load2Music", mapOf("url1" to url1, "url2" to url2, "config" to config), mapOf())
this.songIdentifier = config.songIdentifier
this.songUrl = url1
this.songUrl2 = url2
this.mainSingerUid = config.mainSingerUid
-
- if (config.autoPlay) {
- // 主唱自动播放歌曲
- if (this.singerRole != KTVSingRole.LeadSinger) {
- switchSingerRole(KTVSingRole.SoloSinger, null)
- }
- startSing(url1, 0)
- }
+ this.needPrelude = config.needPrelude
}
override fun switchPlaySrc(url: String, syncPts: Boolean) {
- reportCallScenarioApi("switchPlaySrc", JSONObject().put("url", url).put("syncPts", syncPts))
+ apiReporter.reportFuncEvent("switchPlaySrc", mapOf("url" to url, "syncPts" to syncPts), mapOf())
if (this.songUrl != url && this.songUrl2 != url) {
ktvApiLogError("switchPlaySrc failed: canceled")
return
@@ -646,69 +750,96 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
}
override fun startSing(songCode: Long, startPos: Long) {
- reportCallScenarioApi("startSing", JSONObject().put("songCode", songCode).put("startPos", startPos))
+ apiReporter.reportFuncEvent("startSing", mapOf("songCode" to songCode, "startPos" to startPos), mapOf())
ktvApiLog("playSong called: $singerRole")
+ if (singerRole != KTVSingRole.SoloSinger && singerRole != KTVSingRole.LeadSinger) {
+ ktvApiLogError("startSing failed: error singerRole")
+ return
+ }
+
if (this.songCode != songCode) {
ktvApiLogError("startSing failed: canceled")
return
}
- mRtcEngine.adjustPlaybackSignalVolume(remoteVolume)
+ mRtcEngine.adjustPlaybackSignalVolume(KTVApi.remoteVolume)
// 导唱
mPlayer.setPlayerOption("enable_multi_audio_track", 1)
- (mPlayer as IAgoraMusicPlayer).open(songCode, startPos)
+ val ret = (mPlayer as IAgoraMusicPlayer).open(songCode, startPos)
+ if (ret != 0) {
+ ktvApiLogError("mpk open failed: $ret")
+ }
}
override fun startSing(url: String, startPos: Long) {
- reportCallScenarioApi("startSing", JSONObject().put("url", url).put("startPos", startPos))
+ apiReporter.reportFuncEvent("startSing", mapOf("url" to url, "startPos" to startPos), mapOf())
+ if (singerRole != KTVSingRole.SoloSinger && singerRole != KTVSingRole.LeadSinger) {
+ ktvApiLogError("startSing failed: error singerRole")
+ return
+ }
+
if (this.songUrl != url && this.songUrl2 != url) {
ktvApiLogError("startSing failed: canceled")
return
}
- mRtcEngine.adjustPlaybackSignalVolume(remoteVolume)
+ mRtcEngine.adjustPlaybackSignalVolume(KTVApi.remoteVolume)
// 导唱
mPlayer.setPlayerOption("enable_multi_audio_track", 1)
- mPlayer.open(url, startPos)
+ val ret = mPlayer.open(url, startPos)
+ if (ret != 0) {
+ ktvApiLogError("mpk open failed: $ret")
+ }
}
override fun resumeSing() {
- reportCallScenarioApi("resumeSing", JSONObject())
+ apiReporter.reportFuncEvent("resumeSing", mapOf(), mapOf())
mPlayer.resume()
}
override fun pauseSing() {
- reportCallScenarioApi("pauseSing", JSONObject())
+ apiReporter.reportFuncEvent("pauseSing", mapOf(), mapOf())
mPlayer.pause()
}
override fun seekSing(time: Long) {
- reportCallScenarioApi("seekSing", JSONObject().put("time", time))
+ apiReporter.reportFuncEvent("seekSing", mapOf("time" to time), mapOf())
mPlayer.seek(time)
syncPlayProgress(time)
}
override fun setLrcView(view: ILrcView) {
- reportCallScenarioApi("setLrcView", JSONObject())
+ apiReporter.reportFuncEvent("setLrcView", mapOf("view" to view), mapOf())
this.lrcView = view
}
override fun muteMic(mute: Boolean) {
- reportCallScenarioApi("muteMic", JSONObject().put("mute", isOnMicOpen))
+ apiReporter.reportFuncEvent("muteMic", mapOf("mute" to mute), mapOf())
this.isOnMicOpen = !mute
- if (this.singerRole == KTVSingRole.SoloSinger || this.singerRole == KTVSingRole.LeadSinger) {
- mRtcEngine.adjustRecordingSignalVolume(if (isOnMicOpen) 100 else 0)
+ if (ktvApiConfig.type != KTVType.SingRelay) {
+ if (this.singerRole == KTVSingRole.SoloSinger || this.singerRole == KTVSingRole.LeadSinger) {
+ mRtcEngine.adjustRecordingSignalVolume(if (isOnMicOpen) 100 else 0)
+ if (isOnMicOpen) {
+ val channelMediaOption = ChannelMediaOptions()
+ channelMediaOption.publishMicrophoneTrack = true
+ channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER
+ mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+ mRtcEngine.muteLocalAudioStream(!isOnMicOpen)
+ }
+ } else {
+ val channelMediaOption = ChannelMediaOptions()
+ channelMediaOption.publishMicrophoneTrack = isOnMicOpen
+ channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER
+ mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+ mRtcEngine.muteLocalAudioStream(!isOnMicOpen)
+ }
} else {
- val channelMediaOption = ChannelMediaOptions()
- channelMediaOption.publishMicrophoneTrack = isOnMicOpen
- channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER
- mRtcEngine.updateChannelMediaOptions(channelMediaOption)
- mRtcEngine.muteLocalAudioStream(!isOnMicOpen)
+ mRtcEngine.adjustRecordingSignalVolume(if (isOnMicOpen) 100 else 0)
}
}
override fun setAudioPlayoutDelay(audioPlayoutDelay: Int) {
- reportCallScenarioApi("setAudioPlayoutDelay", JSONObject().put("audioPlayoutDelay", audioPlayoutDelay))
+ apiReporter.reportFuncEvent("setAudioPlayoutDelay", mapOf("audioPlayoutDelay" to audioPlayoutDelay), mapOf())
this.audioPlayoutDelay = audioPlayoutDelay
}
@@ -757,10 +888,16 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
// 预加载歌曲成功
if (ktvApiConfig.musicType == KTVMusicType.SONG_CODE) {
mPlayer.setPlayerOption("enable_multi_audio_track", 0)
- (mPlayer as IAgoraMusicPlayer).open(songCode, 0) // TODO open failed
+ val ret = (mPlayer as IAgoraMusicPlayer).open(songCode, 0) // TODO open failed
+ if (ret != 0) {
+ ktvApiLogError("mpk open failed: $ret")
+ }
} else {
mPlayer.setPlayerOption("enable_multi_audio_track", 0)
- mPlayer.open(songUrl, 0) // TODO open failed
+ val ret = mPlayer.open(songUrl, 0) // TODO open failed
+ if (ret != 0) {
+ ktvApiLogError("mpk open failed: $ret")
+ }
}
// 预加载成功后加入第二频道:预加载时间>>joinChannel时间
@@ -840,12 +977,12 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
private fun syncPlayState(
state: MediaPlayerState,
- error: Constants.MediaPlayerError
+ reason: Constants.MediaPlayerReason
) {
val msg: MutableMap = HashMap()
msg["cmd"] = "PlayerState"
msg["state"] = MediaPlayerState.getValue(state)
- msg["error"] = Constants.MediaPlayerError.getValue(error)
+ msg["error"] = Constants.MediaPlayerReason.getValue(reason)
val jsonMsg = JSONObject(msg)
sendStreamMessageWithJsonObject(jsonMsg) {}
}
@@ -859,6 +996,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
}
// 合唱
+ private var handlerEx :IRtcEngineEventHandler? = null
private fun joinChorus2ndChannel(
newRole: KTVSingRole,
token: String,
@@ -898,53 +1036,61 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
token,
rtcConnection,
channelMediaOption,
- object : IRtcEngineEventHandler() {
- override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
- ktvApiLog("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?) {
- ktvApiLog("onLeaveChannel2")
- if (isRelease) return
- super.onLeaveChannel(stats)
- if (newRole == KTVSingRole.LeadSinger) {
- mainSingerHasJoinChannelEx = false
- }
+ null
+ )
+ val handler = object : IRtcEngineEventHandler() {
+ override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
+ if (isRelease) return
+ ktvApiLog("onJoinChannel2Success: channel:$channel, uid:$uid")
+ super.onJoinChannelSuccess(channel, uid, elapsed)
+ if (newRole == KTVSingRole.LeadSinger) {
+ mainSingerHasJoinChannelEx = true
}
+ onJoinChorus2ndChannelCallback(0)
+ mRtcEngine.enableAudioVolumeIndicationEx(50, 10, true, rtcConnection)
+ }
- override fun onError(err: Int) {
- super.onError(err)
- if (isRelease) return
- if (err == ERR_JOIN_CHANNEL_REJECTED) {
- ktvApiLogError("joinChorus2ndChannel failed: ERR_JOIN_CHANNEL_REJECTED")
- onJoinChorus2ndChannelCallback(ERR_JOIN_CHANNEL_REJECTED)
- } else if (err == ERR_LEAVE_CHANNEL_REJECTED) {
- ktvApiLogError("leaveChorus2ndChannel failed: ERR_LEAVE_CHANNEL_REJECTED")
- }
+ override fun onLeaveChannel(stats: RtcStats?) {
+ if (isRelease) return
+ //ktvApiLog("onLeaveChannel2")
+ super.onLeaveChannel(stats)
+ if (newRole == KTVSingRole.LeadSinger) {
+ mainSingerHasJoinChannelEx = false
}
+ }
- override fun onTokenPrivilegeWillExpire(token: String?) {
- super.onTokenPrivilegeWillExpire(token)
- ktvApiEventHandlerList.forEach { it.onTokenPrivilegeWillExpire() }
+ override fun onError(err: Int) {
+ super.onError(err)
+ if (isRelease) return
+ if (err == ERR_JOIN_CHANNEL_REJECTED) {
+ ktvApiLogError("joinChorus2ndChannel failed: ERR_JOIN_CHANNEL_REJECTED")
+ onJoinChorus2ndChannelCallback(ERR_JOIN_CHANNEL_REJECTED)
+ } else if (err == ERR_LEAVE_CHANNEL_REJECTED) {
+ ktvApiLogError("leaveChorus2ndChannel failed: ERR_LEAVE_CHANNEL_REJECTED")
}
+ }
- override fun onAudioVolumeIndication(
- speakers: Array?,
- totalVolume: Int
- ) {
- super.onAudioVolumeIndication(speakers, totalVolume)
- ktvApiEventHandlerList.forEach { it.onChorusChannelAudioVolumeIndication(speakers, totalVolume) }
- }
+ 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) }
+ }
+ }
+ handlerEx = handler
+ mRtcEngine.addHandlerEx(handler, rtcConnection)
+ if (enableMultipathing) {
+ mRtcEngine.setParametersEx(
+ "{\"rtc.path_scheduling_strategy\":0, \"rtc.enableMultipath\": true, \"rtc.remote_path_scheduling_strategy\": 0}",
+ rtcConnection
+ )
+ }
if (ret != 0) {
ktvApiLogError("joinChorus2ndChannel failed: $ret")
}
@@ -956,6 +1102,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
}
private fun leaveChorus2ndChannel(role: KTVSingRole) {
+ mRtcEngine.removeHandlerEx(handlerEx, subChorusConnection)
if (role == KTVSingRole.LeadSinger) {
mRtcEngine.leaveChannelEx(subChorusConnection)
} else if (role == KTVSingRole.CoSinger) {
@@ -974,29 +1121,6 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
}
// ------------------ 歌词播放、同步 ------------------
- // 开始播放歌词
- 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() {
ktvApiLog("startDisplayLrc called")
mStopDisplayLrc = false
@@ -1015,22 +1139,6 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
}
// ------------------ 音高pitch同步 ------------------
-// private var mSyncPitchThread: Thread? = null
- private var mStopSyncPitch = true
-
- private val mSyncPitchTask = Runnable {
- if (!mStopSyncPitch) {
- if (ktvApiConfig.type == KTVType.SingRelay &&
- (singerRole == KTVSingRole.LeadSinger || singerRole == KTVSingRole.SoloSinger || singerRole == KTVSingRole.CoSinger) &&
- isOnMicOpen) {
- sendSyncPitch(pitch)
- } else 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"
@@ -1040,7 +1148,6 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
}
// 开始同步音高
- private var mSyncPitchFuture :ScheduledFuture<*>? = null
private fun startSyncPitch() {
mStopSyncPitch = false
mSyncPitchFuture = scheduledThreadPool.scheduleAtFixedRate(mSyncPitchTask,0,50,TimeUnit.MILLISECONDS)
@@ -1092,6 +1199,16 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
loadMusicCallbackMap[songNo.toString()] = onLoadMusicCallback
}
+ private fun getSongSimpleInfo(songNo: Long, onSongSimpleInfoResult: (songCode: Long, success: Boolean) -> Unit) {
+ ktvApiLog("getSongSimpleInfo: $songNo")
+ val requestId = mMusicCenter.getSongSimpleInfo(songNo)
+ if (requestId == null || requestId.isEmpty()) {
+ onSongSimpleInfoResult.invoke(songNo, false)
+ return
+ }
+ simpleInfoCallbackMap[requestId] = onSongSimpleInfoResult
+ }
+
private fun getNtpTimeInMs(): Long {
val currentNtpTime = mRtcEngine.ntpWallTimeInMs
return if (currentNtpTime != 0L) {
@@ -1114,6 +1231,34 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
override fun onStreamMessage(uid: Int, streamId: Int, data: ByteArray?) {
super.onStreamMessage(uid, streamId, data)
if (uid != mainSingerUid) return
+ dealWithStreamMessage(data)
+ }
+
+ override fun onAudioMetadataReceived(uid: Int, data: ByteArray?) {
+ super.onAudioMetadataReceived(uid, data)
+ val messageData = data ?: return
+ try {
+
+ val lrcTime = LrcTimeOuterClass.LrcTime.parseFrom(messageData)
+ if (lrcTime.type == LrcTimeOuterClass.MsgType.LRC_TIME) { //同步歌词
+ val realPosition = lrcTime.ts
+ val songId = lrcTime.songId
+ val curTs = if (this.songIdentifier == songId) realPosition else 0
+ 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
+ }
+ }
+ } catch (exp: JSONException) {
+ ktvApiLog("onStreamMessage:$exp")
+ }
+ }
+
+ private fun dealWithStreamMessage(data: ByteArray?) {
val jsonMsg: JSONObject
val messageData = data ?: return
try {
@@ -1131,7 +1276,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
// 本地BGM校准逻辑
if (this.mediaPlayerState == MediaPlayerState.PLAYER_STATE_OPEN_COMPLETED) {
// 合唱者开始播放音乐前调小远端人声
- mRtcEngine.adjustPlaybackSignalVolume(remoteVolume)
+ mRtcEngine.adjustPlaybackSignalVolume(KTVApi.remoteVolume)
// 收到leadSinger第一次播放位置消息时开启本地播放(先通过seek校准)
val delta = getNtpTimeInMs() - remoteNtp
val expectPosition = position + delta + audioPlayoutDelay
@@ -1146,9 +1291,11 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
val expectPosition =
localNtpTime - remoteNtp + position + audioPlayoutDelay // 实际主唱的播放时间
val diff = expectPosition - localPosition
- if (debugMode) {
- ktvApiLog("play_status_seek: " + diff + " audioPlayoutDelay:" + audioPlayoutDelay + " localNtpTime: " + localNtpTime + " expectPosition: " + expectPosition +
- " localPosition: " + localPosition + " ntp diff: " + (localNtpTime - remoteNtp))
+ if (KTVApi.debugMode) {
+ ktvApiLog(
+ "play_status_seek: " + diff + " audioPlayoutDelay:" + audioPlayoutDelay + " localNtpTime: " + localNtpTime + " expectPosition: " + expectPosition +
+ " localPosition: " + localPosition + " ntp diff: " + (localNtpTime - remoteNtp)
+ )
}
if ((diff > 50 || diff < -50) && expectPosition < duration) { //设置阈值为50ms,避免频繁seek
ktvApiLog("player seek: $diff")
@@ -1164,21 +1311,30 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
MediaPlayerState.PLAYER_STATE_PAUSED -> {
mPlayer.pause()
}
+
MediaPlayerState.PLAYER_STATE_PLAYING -> {
mPlayer.resume()
}
+
else -> {}
}
}
} else {
// 独唱观众
- if (this.songIdentifier == songId) {
- mLastReceivedPlayPosTime = System.currentTimeMillis()
- mReceivedPlayPosition = realPosition
- ktvApiEventHandlerList.forEach { it.onMusicPlayerPositionChanged(realPosition, 0) }
+ if (jsonMsg.has("ver")) {
+ // 发送端是新发送端, 歌词信息需要从 audioMetadata 里取
+ recvFromDataStream = false
} else {
- mLastReceivedPlayPosTime = null
- mReceivedPlayPosition = 0
+ // 发送端是老发送端, 歌词信息需要从 dataStreamMessage 里取
+ recvFromDataStream = true
+ if (this.songIdentifier == songId) {
+ mLastReceivedPlayPosTime = System.currentTimeMillis()
+ mReceivedPlayPosition = realPosition
+ ktvApiEventHandlerList.forEach { it.onMusicPlayerPositionChanged(realPosition, 0) }
+ } else {
+ mLastReceivedPlayPosTime = null
+ mReceivedPlayPosition = 0
+ }
}
}
} else if (jsonMsg.getString("cmd") == "Seek") {
@@ -1196,19 +1352,23 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
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
- ) }
+ ktvApiEventHandlerList.forEach {
+ it.onMusicPlayerStateChanged(
+ MediaPlayerState.getStateByValue(state),
+ Constants.MediaPlayerReason.getErrorByValue(error),
+ false
+ )
+ }
} else if (jsonMsg.getString("cmd") == "setVoicePitch") {
val pitch = jsonMsg.getDouble("pitch")
if (ktvApiConfig.type == KTVType.SingRelay && !isOnMicOpen && this.singerRole != KTVSingRole.Audience) {
@@ -1224,7 +1384,8 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
mRtcEngine.muteRemoteAudioStream(mainSingerUid, true)
}
}
- } catch (_: JSONException) { }
+ } catch (_: JSONException) {
+ }
}
override fun onAudioVolumeIndication(speakers: Array?, totalVolume: Int) {
@@ -1251,7 +1412,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
// 用于合唱校准
override fun onLocalAudioStats(stats: LocalAudioStats?) {
super.onLocalAudioStats(stats)
- if (useCustomAudioSource) return
+ if (KTVApi.useCustomAudioSource) return
val audioState = stats ?: return
audioPlayoutDelay = audioState.audioPlayoutDelay
}
@@ -1356,24 +1517,40 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
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)
+ val callback = simpleInfoCallbackMap[requestId] ?: return
+ if (errorCode != 0) {
+ ktvApiLogError("onSongSimpleInfoResult failed, requestId: $requestId, songCode: $songCode, errorCode: $errorCode")
+ callback.invoke(songCode, false)
+ return
+ }
+ try {
+ 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")
+ val preludeDuration = highStartTime.getLong("preludeDuration")
+ this.highStartTime = time
+ if (needPrelude) {
+ this.highStartTime -= preludeDuration
+ }
+ lrcView?.onHighPartTime(time, endTime)
+ callback.invoke(songCode, true)
+ } catch (e: JSONException) {
+ ktvApiLogError("onSongSimpleInfoResult: ${e.message}")
+ callback.invoke(songCode, false)
+ }
}
// ------------------------ AgoraRtcMediaPlayerDelegate ------------------------
private var duration: Long = 0
override fun onPlayerStateChanged(
state: MediaPlayerState?,
- error: Constants.MediaPlayerError?
+ reason: Constants.MediaPlayerReason?
) {
val mediaPlayerState = state ?: return
- val mediaPlayerError = error ?: return
+ val mediaPlayerError = reason ?: return
ktvApiLog("onPlayerStateChanged: $state")
this.mediaPlayerState = mediaPlayerState
when (mediaPlayerState) {
@@ -1391,7 +1568,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
}
}
MediaPlayerState.PLAYER_STATE_PLAYING -> {
- mRtcEngine.adjustPlaybackSignalVolume(remoteVolume)
+ mRtcEngine.adjustPlaybackSignalVolume(KTVApi.remoteVolume)
}
MediaPlayerState.PLAYER_STATE_PAUSED -> {
mRtcEngine.adjustPlaybackSignalVolume(100)
@@ -1425,6 +1602,7 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
msg["playerState"] = MediaPlayerState.getValue(this.mediaPlayerState)
msg["pitch"] = pitch
msg["songIdentifier"] = songIdentifier
+ msg["ver"] = lyricSyncVersion
val jsonMsg = JSONObject(msg)
sendStreamMessageWithJsonObject(jsonMsg) {}
}
@@ -1459,5 +1637,9 @@ class KTVApiImpl : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver
override fun onPlayerInfoUpdated(info: PlayerUpdatedInfo?) {}
+ override fun onPlayerCacheStats(stats: CacheStatistics?) {}
+
+ override fun onPlayerPlaybackStats(stats: PlayerPlaybackStats?) {}
+
override fun onAudioVolumeIndication(volume: Int) {}
}
\ No newline at end of file
diff --git a/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVGiantChorusApiImpl.kt b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVGiantChorusApiImpl.kt
new file mode 100644
index 0000000..61bc711
--- /dev/null
+++ b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/KTVGiantChorusApiImpl.kt
@@ -0,0 +1,1569 @@
+package io.agora.ktvapi
+
+import android.os.Handler
+import android.os.Looper
+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.CacheStatistics
+import io.agora.mediaplayer.data.PlayerPlaybackStats
+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.*
+
+class KTVGiantChorusApiImpl(
+ val giantChorusApiConfig: KTVGiantChorusApiConfig
+) : KTVApi, IMusicContentCenterEventHandler, IMediaPlayerObserver, IRtcEngineEventHandler() {
+
+ companion object {
+ private val scheduledThreadPool: ScheduledExecutorService = Executors.newScheduledThreadPool(5)
+ private const val tag = "KTV_API_LOG_GIANT"
+ private const val version = "5.0.0"
+ private const val lyricSyncVersion = 2
+ }
+
+ private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
+ private var mRtcEngine: RtcEngineEx = giantChorusApiConfig.engine as RtcEngineEx
+ private lateinit var mMusicCenter: IAgoraMusicContentCenter
+ private var mPlayer: IMediaPlayer
+ private val apiReporter: APIReporter = APIReporter(APIType.KTV, version, mRtcEngine)
+
+ private var innerDataStreamId: Int = 0
+ private var singChannelRtcConnection: RtcConnection? = null
+ private var subChorusConnection: RtcConnection? = null
+ private var mpkConnection: RtcConnection? = null
+
+ private var mainSingerUid: Int = 0
+ private var songCode: Long = 0
+ private var songUrl: String = ""
+ private var songUrl2: 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 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
+
+ private var professionalModeOpen = false
+ private var audioRouting = 0
+ private var isPublishAudio = false // 通过是否发音频流判断
+
+ // 演唱分数
+ private var singingScore = 0
+
+ // multipath
+ private var enableMultipathing = true
+
+ // 歌词信息是否来源于 dataStream
+ private var recvFromDataStream = false
+
+ // 开始播放歌词
+ private var mStopDisplayLrc = true
+ private var displayLrcFuture: ScheduledFuture<*>? = null
+ private val displayLrcTask = object : Runnable {
+ override fun run() {
+ if (!mStopDisplayLrc){
+ if (singerRole == KTVSingRole.Audience && !recvFromDataStream) return // audioMetaData方案观众return
+ val lastReceivedTime = mLastReceivedPlayPosTime ?: return
+ val curTime = System.currentTimeMillis()
+ val offset = curTime - lastReceivedTime
+ if (offset <= 100) {
+ val curTs = mReceivedPlayPosition + offset
+ if (singerRole == KTVSingRole.LeadSinger || singerRole == KTVSingRole.SoloSinger) {
+ val lrcTime = LrcTimeOuterClass.LrcTime.newBuilder()
+ .setTypeValue(LrcTimeOuterClass.MsgType.LRC_TIME.number)
+ .setForward(true)
+ .setSongId(songIdentifier)
+ .setTs(curTs)
+ .setUid(giantChorusApiConfig.musicStreamUid)
+ .build()
+
+ mRtcEngine.sendAudioMetadataEx(lrcTime.toByteArray(), mpkConnection)
+ }
+ 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 mSyncScoreFuture :ScheduledFuture<*>? = null
+ private var mStopSyncScore = true
+ private val mSyncScoreTask = Runnable {
+ if (!mStopSyncScore) {
+ if (mediaPlayerState == MediaPlayerState.PLAYER_STATE_PLAYING &&
+ (singerRole == KTVSingRole.LeadSinger || singerRole == KTVSingRole.CoSinger)) {
+ sendSyncScore()
+ }
+ }
+ }
+
+ // 云端合流信息
+ private var mSyncCloudConvergenceStatusFuture :ScheduledFuture<*>? = null
+ private var mStopSyncCloudConvergenceStatus = true
+ private val mSyncCloudConvergenceStatusTask = Runnable {
+ if (!mStopSyncCloudConvergenceStatus && singerRole == KTVSingRole.LeadSinger) {
+ sendSyncCloudConvergenceStatus()
+ }
+ }
+
+ init {
+ apiReporter.reportFuncEvent("initialize", mapOf("config" to giantChorusApiConfig), mapOf())
+ this.singChannelRtcConnection = RtcConnection(giantChorusApiConfig.chorusChannelName, giantChorusApiConfig.localUid)
+
+ // ------------------ 初始化内容中心 ------------------
+ if (giantChorusApiConfig.musicType == KTVMusicType.SONG_CODE) {
+ val contentCenterConfiguration = MusicContentCenterConfiguration()
+ contentCenterConfiguration.appId = giantChorusApiConfig.appId
+ contentCenterConfiguration.mccUid = giantChorusApiConfig.localUid.toLong()
+ contentCenterConfiguration.token = giantChorusApiConfig.rtmToken
+ contentCenterConfiguration.maxCacheSize = giantChorusApiConfig.maxCacheSize
+ if (KTVApi.debugMode) {
+ contentCenterConfiguration.mccDomain = KTVApi.mccDomain
+ }
+ mMusicCenter = IAgoraMusicContentCenter.create(mRtcEngine)
+ mMusicCenter.initialize(contentCenterConfiguration)
+ mMusicCenter.registerEventHandler(this)
+
+ // ------------------ 初始化音乐播放器实例 ------------------
+ mPlayer = mMusicCenter.createMusicPlayer()
+ } else {
+ mPlayer = mRtcEngine.createMediaPlayer()
+ }
+ mPlayer.adjustPublishSignalVolume(KTVApi.mpkPublishVolume)
+ mPlayer.adjustPlayoutVolume(KTVApi.mpkPlayoutVolume)
+
+ // 注册回调
+ mPlayer.registerPlayerObserver(this)
+ setKTVParameters()
+ startDisplayLrc()
+ startSyncScore()
+ startSyncCloudConvergenceStatus()
+ isRelease = false
+
+ mPlayer.setPlayerOption("play_pos_change_callback", 100)
+ }
+
+ // 日志输出
+ private fun ktvApiLog(msg: String) {
+ if (isRelease) return
+ apiReporter.writeLog("[${tag}] $msg", LOG_LEVEL_INFO)
+ }
+
+ // 日志输出
+ private fun ktvApiLogError(msg: String) {
+ if (isRelease) return
+ apiReporter.writeLog("[${tag}] $msg", LOG_LEVEL_ERROR)
+ }
+
+ override fun renewInnerDataStreamId() {
+ apiReporter.reportFuncEvent("renewInnerDataStreamId", mapOf(), mapOf())
+
+ val innerCfg = DataStreamConfig()
+ innerCfg.syncWithAudio = true
+ innerCfg.ordered = false
+ this.innerDataStreamId = mRtcEngine.createDataStreamEx(innerCfg, singChannelRtcConnection)
+ }
+
+ private fun setKTVParameters() {
+ mRtcEngine.setParameters("{\"rtc.enable_nasa2\": true}")
+ 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.uplink_apm_async_process\": true}")
+
+ // 标准音质
+ mRtcEngine.setParameters("{\"che.audio.aec.split_srate_for_48k\": 16000}")
+
+ // ENT-901
+ mRtcEngine.setParameters("{\"che.audio.ans.noise_gate\": 20}")
+
+ // Android Only
+ mRtcEngine.setParameters("{\"che.audio.enable_estimated_device_delay\":false}")
+
+ // TopN + SendAudioMetadata
+ mRtcEngine.setParameters("{\"rtc.use_audio4\": true}")
+
+ // mutipath
+ enableMultipathing = false
+ //mRtcEngine.setParameters("{\"rtc.enableMultipath\": true}")
+ mRtcEngine.setParameters("{\"rtc.enable_tds_request_on_join\": true}")
+ //mRtcEngine.setParameters("{\"rtc.remote_path_scheduling_strategy\": 0}")
+ //mRtcEngine.setParameters("{\"rtc.path_scheduling_strategy\": 0}")
+
+ // 数据上报
+ mRtcEngine.setParameters("{\"rtc.direct_send_custom_event\": true}")
+ }
+
+ private fun resetParameters() {
+ mRtcEngine.setAudioScenario(AUDIO_SCENARIO_GAME_STREAMING)
+ mRtcEngine.setParameters("{\"che.audio.custom_bitrate\": 80000}") // 兼容之前的profile = 3设置
+ mRtcEngine.setParameters("{\"che.audio.max_mixed_participants\": 3}") // 正常3路下行流混流
+ mRtcEngine.setParameters("{\"che.audio.neteq.prebuffer\": false}") // 关闭 接收端快速对齐模式
+ mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp\": false}") // 观众关闭 多端同步
+ mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\": false}") //主播关闭多端同步
+ }
+
+ override fun addEventHandler(ktvApiEventHandler: IKTVApiEventHandler) {
+ apiReporter.reportFuncEvent("addEventHandler", mapOf("ktvApiEventHandler" to ktvApiEventHandler), mapOf())
+ ktvApiEventHandlerList.add(ktvApiEventHandler)
+ }
+
+ override fun removeEventHandler(ktvApiEventHandler: IKTVApiEventHandler) {
+ apiReporter.reportFuncEvent("removeEventHandler", mapOf("ktvApiEventHandler" to ktvApiEventHandler), mapOf())
+ ktvApiEventHandlerList.remove(ktvApiEventHandler)
+ }
+
+ override fun release() {
+ apiReporter.reportFuncEvent("release", mapOf(), mapOf())
+ if (isRelease) return
+ isRelease = true
+ singerRole = KTVSingRole.Audience
+
+ resetParameters()
+ stopSyncCloudConvergenceStatus()
+ stopSyncScore()
+ stopDisplayLrc()
+ this.mLastReceivedPlayPosTime = null
+ this.mReceivedPlayPosition = 0
+ this.innerDataStreamId = 0
+ this.singingScore = 0
+
+ lyricCallbackMap.clear()
+ loadMusicCallbackMap.clear()
+ musicChartsCallbackMap.clear()
+ musicCollectionCallbackMap.clear()
+ lrcView = null
+
+ mPlayer.unRegisterPlayerObserver(this)
+
+ if (giantChorusApiConfig.musicType == KTVMusicType.SONG_CODE) {
+ mMusicCenter.unregisterEventHandler()
+ }
+
+ mPlayer.stop()
+ mPlayer.destroy()
+ IAgoraMusicContentCenter.destroy()
+
+ mainSingerHasJoinChannelEx = false
+ professionalModeOpen = false
+ audioRouting = 0
+ isPublishAudio = false
+ }
+
+ override fun enableProfessionalStreamerMode(enable: Boolean) {
+ apiReporter.reportFuncEvent("enableProfessionalStreamerMode", mapOf("enable" to enable), mapOf())
+ this.professionalModeOpen = enable
+ processAudioProfessionalProfile()
+ }
+
+ private fun processAudioProfessionalProfile() {
+ ktvApiLog("processAudioProfessionalProfile: audioRouting: $audioRouting, professionalModeOpen: $professionalModeOpen, isPublishAudio:$isPublishAudio")
+ if (!isPublishAudio) return // 必须为麦上者
+ if (professionalModeOpen) {
+ // 专业
+ if (audioRouting == AUDIO_ROUTE_HEADSET || audioRouting == AUDIO_ROUTE_HEADSETNOMIC || audioRouting == AUDIO_ROUTE_BLUETOOTH_DEVICE_HFP || audioRouting == AUDIO_ROUTE_USBDEVICE || audioRouting == AUDIO_ROUTE_BLUETOOTH_DEVICE_A2DP) {
+ // 耳机 关闭3A 关闭md
+ mRtcEngine.setParameters("{\"che.audio.aec.enable\": false}")
+ mRtcEngine.setParameters("{\"che.audio.agc.enable\": false}")
+ mRtcEngine.setParameters("{\"che.audio.ans.enable\": false}")
+ mRtcEngine.setParameters("{\"che.audio.md.enable\": false}")
+ mRtcEngine.setAudioProfile(AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO) // AgoraAudioProfileMusicHighQualityStereo
+ } else {
+ // 非耳机 开启3A 关闭md
+ mRtcEngine.setParameters("{\"che.audio.aec.enable\": true}")
+ mRtcEngine.setParameters("{\"che.audio.agc.enable\": true}")
+ mRtcEngine.setParameters("{\"che.audio.ans.enable\": true}")
+ mRtcEngine.setParameters("{\"che.audio.md.enable\": false}")
+ mRtcEngine.setAudioProfile(AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO) // AgoraAudioProfileMusicHighQualityStereo
+ }
+ } else {
+ // 非专业 开启3A 关闭md
+ mRtcEngine.setParameters("{\"che.audio.aec.enable\": true}")
+ mRtcEngine.setParameters("{\"che.audio.agc.enable\": true}")
+ mRtcEngine.setParameters("{\"che.audio.ans.enable\": true}")
+ mRtcEngine.setParameters("{\"che.audio.md.enable\": false}")
+ mRtcEngine.setAudioProfile(AUDIO_PROFILE_MUSIC_STANDARD_STEREO) // AgoraAudioProfileMusicStandardStereo
+ }
+ }
+
+ override fun enableMulitpathing(enable: Boolean) {
+ apiReporter.reportFuncEvent("enableMulitpathing", mapOf("enable" to enable), mapOf())
+ this.enableMultipathing = enable
+
+ if (singerRole == KTVSingRole.LeadSinger || singerRole == KTVSingRole.CoSinger) {
+ subChorusConnection?.let {
+ mRtcEngine.setParametersEx("{\"rtc.enableMultipath\": $enable, \"rtc.path_scheduling_strategy\": 0, \"rtc.remote_path_scheduling_strategy\": 0}", it)
+ }
+ }
+ }
+
+ override fun renewToken(rtmToken: String, chorusChannelRtcToken: String) {
+ apiReporter.reportFuncEvent("renewToken", mapOf(), mapOf())
+ // 更新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?
+ ) {
+ apiReporter.reportFuncEvent("switchSingerRole", mapOf("newRole" to newRole), mapOf())
+ ktvApiLog("switchSingerRole oldRole: $singerRole, newRole: $newRole")
+ val oldRole = singerRole
+ if (this.singerRole == KTVSingRole.Audience && newRole == KTVSingRole.LeadSinger) {
+ // 1、Audience -》LeadSinger
+ // 离开观众频道
+ mRtcEngine.leaveChannelEx(RtcConnection(giantChorusApiConfig.audienceChannelName, giantChorusApiConfig.localUid))
+ joinChorus(newRole)
+ singerRole = newRole
+ ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
+ switchRoleStateListener?.onSwitchRoleSuccess()
+ } else if (this.singerRole == KTVSingRole.Audience && newRole == KTVSingRole.CoSinger) {
+ // 2、Audience -》CoSinger
+ // 离开观众频道
+ mRtcEngine.leaveChannelEx(RtcConnection(giantChorusApiConfig.audienceChannelName, giantChorusApiConfig.localUid))
+ joinChorus(newRole)
+ singerRole = newRole
+ switchRoleStateListener?.onSwitchRoleSuccess()
+ ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
+ } else if (this.singerRole == KTVSingRole.CoSinger && newRole == KTVSingRole.Audience) {
+ // 3、CoSinger -》Audience
+ leaveChorus2(singerRole)
+ // 加入观众频道
+ mRtcEngine.joinChannelEx(giantChorusApiConfig.audienceChannelToken, RtcConnection(giantChorusApiConfig.audienceChannelName, giantChorusApiConfig.localUid), ChannelMediaOptions(), object : IRtcEngineEventHandler() {
+ override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
+ super.onJoinChannelSuccess(channel, uid, elapsed)
+ }
+
+ override fun onStreamMessage(uid: Int, streamId: Int, data: ByteArray?) {
+ super.onStreamMessage(uid, streamId, data)
+ dealWithStreamMessage(uid, streamId, data)
+ }
+
+ override fun onAudioMetadataReceived(uid: Int, data: ByteArray?) {
+ super.onAudioMetadataReceived(uid, data)
+ dealWithAudioMetadata(uid, data)
+ }
+ })
+ mRtcEngine.setParametersEx("{\"rtc.use_audio4\": true}", RtcConnection(giantChorusApiConfig.audienceChannelName, giantChorusApiConfig.localUid))
+
+ singerRole = newRole
+ ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
+ switchRoleStateListener?.onSwitchRoleSuccess()
+ } else if (this.singerRole == KTVSingRole.LeadSinger && newRole == KTVSingRole.Audience) {
+ // 4、LeadSinger -》Audience
+ stopSing()
+ leaveChorus2(singerRole)
+
+ // 加入观众频道
+ mRtcEngine.joinChannelEx(giantChorusApiConfig.audienceChannelToken, RtcConnection(giantChorusApiConfig.audienceChannelName, giantChorusApiConfig.localUid), ChannelMediaOptions(), object : IRtcEngineEventHandler() {
+ override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
+ super.onJoinChannelSuccess(channel, uid, elapsed)
+ }
+
+ override fun onStreamMessage(uid: Int, streamId: Int, data: ByteArray?) {
+ super.onStreamMessage(uid, streamId, data)
+ dealWithStreamMessage(uid, streamId, data)
+ }
+
+ override fun onAudioMetadataReceived(uid: Int, data: ByteArray?) {
+ super.onAudioMetadataReceived(uid, data)
+ dealWithAudioMetadata(uid, data)
+ }
+ })
+ mRtcEngine.setParametersEx("{\"rtc.use_audio4\": true}", RtcConnection(giantChorusApiConfig.audienceChannelName, giantChorusApiConfig.localUid))
+
+ singerRole = newRole
+ ktvApiEventHandlerList.forEach { it.onSingerRoleChanged(oldRole, newRole) }
+ switchRoleStateListener?.onSwitchRoleSuccess()
+ } else {
+ switchRoleStateListener?.onSwitchRoleFail(SwitchRoleFailReason.NO_PERMISSION)
+ ktvApiLogError("Error!You can not switch role from $singerRole to $newRole!")
+ }
+ }
+
+ override fun fetchMusicCharts(onMusicChartResultListener: (requestId: String?, status: Int, list: Array?) -> Unit) {
+ apiReporter.reportFuncEvent("fetchMusicCharts", mapOf(), mapOf())
+ 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
+ ) {
+ apiReporter.reportFuncEvent("searchMusicByMusicChartId", mapOf(), mapOf())
+ 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
+ ) {
+ apiReporter.reportFuncEvent("searchMusicByKeyword", mapOf(), mapOf())
+ val requestId = mMusicCenter.searchMusic(keyword, page, pageSize, jsonOption)
+ musicCollectionCallbackMap[requestId] = onMusicCollectionResultListener
+ }
+
+ override fun loadMusic(
+ songCode: Long,
+ config: KTVLoadMusicConfiguration,
+ musicLoadStateListener: IMusicLoadStateListener
+ ) {
+ apiReporter.reportFuncEvent("loadMusic", mapOf("songCode" to songCode, "config" to config), mapOf())
+ ktvApiLog("loadMusic called: songCode $songCode")
+ // 设置到全局, 连续调用以最新的为准
+ 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歌曲为准
+ ktvApiLogError("loadMusic failed: CANCELED")
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.CANCELED)
+ return@loadLyric
+ }
+
+ if (lyricUrl == null) {
+ // 加载歌词失败
+ ktvApiLogError("loadMusic failed: NO_LYRIC_URL")
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.NO_LYRIC_URL)
+ } else {
+ // 加载歌词成功
+ ktvApiLog("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歌曲为准
+ ktvApiLogError("loadMusic failed: CANCELED")
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.CANCELED)
+ return@preLoadMusic
+ }
+ if (config.mode == KTVLoadMusicMode.LOAD_MUSIC_AND_LRC) {
+ // 需要加载歌词
+ loadLyric(song) { _, lyricUrl ->
+ if (this.songCode != song) {
+ // 当前歌曲已发生变化,以最新load歌曲为准
+ ktvApiLogError("loadMusic failed: CANCELED")
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.CANCELED)
+ return@loadLyric
+ }
+
+ if (lyricUrl == null) {
+ // 加载歌词失败
+ ktvApiLogError("loadMusic failed: NO_LYRIC_URL")
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.NO_LYRIC_URL)
+ } else {
+ // 加载歌词成功
+ ktvApiLog("loadMusic success")
+ lrcView?.onDownloadLrcData(lyricUrl)
+ musicLoadStateListener.onMusicLoadProgress(song, 100, MusicLoadStatus.COMPLETED, msg, lrcUrl)
+ musicLoadStateListener.onMusicLoadSuccess(song, lyricUrl)
+ }
+ }
+ } else if (config.mode == KTVLoadMusicMode.LOAD_MUSIC_ONLY) {
+ // 不需要加载歌词
+ ktvApiLog("loadMusic success")
+ 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 {
+ // 预加载歌曲失败
+ ktvApiLogError("loadMusic failed: MUSIC_PRELOAD_FAIL")
+ musicLoadStateListener.onMusicLoadFail(song, KTVLoadMusicFailReason.MUSIC_PRELOAD_FAIL)
+ }
+ }
+ }
+
+ override fun loadMusic(
+ url: String,
+ config: KTVLoadMusicConfiguration
+ ) {
+ apiReporter.reportFuncEvent("loadMusic", mapOf("url" to url, "config" to config), mapOf())
+ ktvApiLog("loadMusic called: songCode $songCode")
+ this.songIdentifier = config.songIdentifier
+ this.songUrl = url
+ this.mainSingerUid = config.mainSingerUid
+ }
+
+ override fun removeMusic(songCode: Long) {
+ apiReporter.reportFuncEvent("removeMusic", mapOf("songCode" to songCode), mapOf())
+ val ret = mMusicCenter.removeCache(songCode)
+ if (ret < 0) {
+ ktvApiLogError("removeMusic failed, ret: $ret")
+ }
+ }
+
+ override fun load2Music(url1: String, url2: String, config: KTVLoadMusicConfiguration) {
+ apiReporter.reportFuncEvent("load2Music", mapOf("url1" to url1, "url2" to url2, "config" to config), mapOf())
+ this.songIdentifier = config.songIdentifier
+ this.songUrl = url1
+ this.songUrl2 = url2
+ this.mainSingerUid = config.mainSingerUid
+ }
+
+ override fun switchPlaySrc(url: String, syncPts: Boolean) {
+ apiReporter.reportFuncEvent("switchPlaySrc", mapOf("url" to url, "syncPts" to syncPts), mapOf())
+ if (this.songUrl != url && this.songUrl2 != url) {
+ ktvApiLogError("switchPlaySrc failed: canceled")
+ return
+ }
+ val curPlayPosition = if (syncPts) mPlayer.playPosition else 0
+ mPlayer.stop()
+ startSing(url, curPlayPosition)
+ }
+
+ override fun startSing(songCode: Long, startPos: Long) {
+ apiReporter.reportFuncEvent("startSing", mapOf("songCode" to songCode, "startPos" to startPos), mapOf())
+ ktvApiLog("playSong called: $singerRole")
+ if (singerRole != KTVSingRole.SoloSinger && singerRole != KTVSingRole.LeadSinger) {
+ ktvApiLogError("startSing failed: error singerRole")
+ return
+ }
+ if (this.songCode != songCode) {
+ ktvApiLogError("startSing failed: canceled")
+ return
+ }
+ mRtcEngine.adjustPlaybackSignalVolume(KTVApi.remoteVolume)
+
+ // 导唱
+ mPlayer.setPlayerOption("enable_multi_audio_track", 1)
+ val ret = (mPlayer as IAgoraMusicPlayer).open(songCode, startPos)
+ if (ret != 0) {
+ ktvApiLogError("mpk open failed: $ret")
+ }
+ }
+
+ override fun startSing(url: String, startPos: Long) {
+ apiReporter.reportFuncEvent("startSing", mapOf("url" to url, "startPos" to startPos), mapOf())
+ ktvApiLog("playSong called: $singerRole")
+ if (singerRole != KTVSingRole.SoloSinger && singerRole != KTVSingRole.LeadSinger) {
+ ktvApiLogError("startSing failed: error singerRole")
+ return
+ }
+ if (this.songUrl != url && this.songUrl2 != url) {
+ ktvApiLogError("startSing failed: canceled")
+ return
+ }
+ mRtcEngine.adjustPlaybackSignalVolume(KTVApi.remoteVolume)
+
+ // 导唱
+ mPlayer.setPlayerOption("enable_multi_audio_track", 1)
+ val ret = mPlayer.open(url, startPos)
+ if (ret != 0) {
+ ktvApiLogError("mpk open failed: $ret")
+ }
+ }
+
+ override fun resumeSing() {
+ apiReporter.reportFuncEvent("resumeSing", mapOf(), mapOf())
+ ktvApiLog("resumePlay called")
+ mPlayer.resume()
+ }
+
+ override fun pauseSing() {
+ apiReporter.reportFuncEvent("pauseSing", mapOf(), mapOf())
+ ktvApiLog("pausePlay called")
+ mPlayer.pause()
+ }
+
+ override fun seekSing(time: Long) {
+ apiReporter.reportFuncEvent("seekSing", mapOf("time" to time), mapOf())
+ ktvApiLog("seek called")
+ mPlayer.seek(time)
+ syncPlayProgress(time)
+ }
+
+ override fun setLrcView(view: ILrcView) {
+ apiReporter.reportFuncEvent("setLrcView", mapOf(), mapOf())
+ ktvApiLog("setLrcView called")
+ this.lrcView = view
+ }
+
+ override fun muteMic(mute: Boolean) {
+ apiReporter.reportFuncEvent("muteMic", mapOf("mute" to mute), mapOf())
+ this.isOnMicOpen = !mute
+ if (singerRole == KTVSingRole.Audience) return
+ val channelMediaOption = ChannelMediaOptions()
+ channelMediaOption.publishMicrophoneTrack = isOnMicOpen
+ channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER
+ mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+ mRtcEngine.muteLocalAudioStreamEx(!isOnMicOpen, singChannelRtcConnection)
+ }
+
+ override fun setAudioPlayoutDelay(audioPlayoutDelay: Int) {
+ apiReporter.reportFuncEvent("setAudioPlayoutDelay", mapOf("audioPlayoutDelay" to audioPlayoutDelay), mapOf())
+ this.audioPlayoutDelay = audioPlayoutDelay
+ }
+
+ fun setSingingScore(score: Int) {
+ this.singingScore = score
+ }
+
+ fun setAudienceStreamMessage(uid: Int, streamId: Int, data: ByteArray?) {
+ dealWithStreamMessage(uid, streamId, data)
+ }
+
+ fun setAudienceAudioMetadataReceived(uid: Int, data: ByteArray?) {
+ dealWithAudioMetadata(uid, data)
+ }
+
+ override fun getMediaPlayer(): IMediaPlayer {
+ return mPlayer
+ }
+
+ override fun getMusicContentCenter(): IAgoraMusicContentCenter {
+ return mMusicCenter
+ }
+
+ override fun switchAudioTrack(mode: AudioTrackMode) {
+ apiReporter.reportFuncEvent("switchAudioTrack", mapOf("mode" to mode), mapOf())
+ when (singerRole) {
+ KTVSingRole.LeadSinger, KTVSingRole.SoloSinger -> {
+ when (mode) {
+ AudioTrackMode.YUAN_CHANG -> mPlayer.selectMultiAudioTrack(0, 0)
+ AudioTrackMode.BAN_ZOU -> mPlayer.selectMultiAudioTrack(1, 1)
+ AudioTrackMode.DAO_CHANG -> mPlayer.selectMultiAudioTrack(0, 1)
+ }
+ }
+ KTVSingRole.CoSinger -> {
+ when (mode) {
+ AudioTrackMode.YUAN_CHANG -> mPlayer.selectAudioTrack(0)
+ AudioTrackMode.BAN_ZOU -> mPlayer.selectAudioTrack(1)
+ AudioTrackMode.DAO_CHANG -> ktvApiLogError("CoSinger can not switch to DAO_CHANG")
+ }
+ }
+ KTVSingRole.Audience -> ktvApiLogError("CoSinger can not switch audio track")
+ }
+ }
+
+ // ------------------ inner KTVApi --------------------
+ private fun stopSing() {
+ ktvApiLog("stopSong called")
+
+ val channelMediaOption = ChannelMediaOptions()
+ channelMediaOption.autoSubscribeAudio = true
+ channelMediaOption.publishMediaPlayerAudioTrack = false
+ mRtcEngine.updateChannelMediaOptionsEx(channelMediaOption, singChannelRtcConnection)
+
+ mPlayer.stop()
+
+ // 更新音频配置
+ mRtcEngine.setAudioScenario(AUDIO_SCENARIO_GAME_STREAMING)
+ 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}")
+ }
+
+ private val subScribeSingerMap = mutableMapOf() //
+ private val singerList = mutableListOf() //
+ private var mainSingerDelay = 0
+ private fun joinChorus(newRole: KTVSingRole) {
+ ktvApiLog("joinChorus: $newRole")
+ val singChannelMediaOptions = ChannelMediaOptions()
+ singChannelMediaOptions.autoSubscribeAudio = true
+ singChannelMediaOptions.publishMicrophoneTrack = true
+ singChannelMediaOptions.clientRoleType = CLIENT_ROLE_BROADCASTER
+ singChannelMediaOptions.isAudioFilterable = newRole != KTVSingRole.LeadSinger // 主唱不参加TopN
+
+ // 加入演唱频道
+ mRtcEngine.joinChannelEx(giantChorusApiConfig.chorusChannelToken, singChannelRtcConnection, singChannelMediaOptions, object :
+ IRtcEngineEventHandler() {
+ override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
+ super.onJoinChannelSuccess(channel, uid, elapsed)
+ ktvApiLog("singChannel onJoinChannelSuccess: $newRole")
+ }
+
+ override fun onStreamMessage(uid: Int, streamId: Int, data: ByteArray?) {
+ super.onStreamMessage(uid, streamId, data)
+ dealWithStreamMessage(uid, streamId, data)
+ }
+
+ override fun onAudioVolumeIndication(speakers: Array?, totalVolume: Int) {
+ val allSpeakers = speakers ?: return
+ // VideoPitch 回调, 用于同步各端音准
+ if (singerRole != KTVSingRole.Audience) {
+ for (info in allSpeakers) {
+ if (info.uid == 0) {
+ pitch =
+ if (mediaPlayerState == MediaPlayerState.PLAYER_STATE_PLAYING && isOnMicOpen) {
+ info.voicePitch
+ } else {
+ 0.0
+ }
+ }
+ }
+ }
+ }
+
+ // 用于合唱校准
+ override fun onLocalAudioStats(stats: LocalAudioStats?) {
+ if (KTVApi.useCustomAudioSource) return
+ val audioState = stats ?: return
+ audioPlayoutDelay = audioState.audioPlayoutDelay
+ }
+
+ // 用于检测耳机状态
+ override fun onAudioRouteChanged(routing: Int) { // 0\2\5 earPhone
+ audioRouting = routing
+ processAudioProfessionalProfile()
+ }
+
+ // 用于检测收发流状态
+ override fun onAudioPublishStateChanged(
+ channel: String?,
+ oldState: Int,
+ newState: Int,
+ elapseSinceLastState: Int
+ ) {
+ ktvApiLog("onAudioPublishStateChanged: oldState: $oldState, newState: $newState")
+ if (newState == 3) {
+ isPublishAudio = true
+ processAudioProfessionalProfile()
+ } else if (newState == 1) {
+ isPublishAudio = false
+ }
+ }
+
+ // 延迟选路策略
+ override fun onUserJoined(uid: Int, elapsed: Int) {
+ super.onUserJoined(uid, elapsed)
+ if (uid != giantChorusApiConfig.musicStreamUid && subScribeSingerMap.size < 8) {
+ mRtcEngine.muteRemoteAudioStreamEx(uid, false, singChannelRtcConnection)
+ if (uid != mainSingerUid) {
+ subScribeSingerMap[uid] = 0
+ }
+ } else if (uid != giantChorusApiConfig.musicStreamUid && subScribeSingerMap.size == 8) {
+ mRtcEngine.muteRemoteAudioStreamEx(uid, true, singChannelRtcConnection)
+ }
+ if (uid != giantChorusApiConfig.musicStreamUid && uid != mainSingerUid) {
+ singerList.add(uid)
+ }
+ }
+
+ override fun onUserOffline(uid: Int, reason: Int) {
+ super.onUserOffline(uid, reason)
+ subScribeSingerMap.remove(uid)
+ singerList.remove(uid)
+ }
+
+ override fun onLeaveChannel(stats: RtcStats?) {
+ super.onLeaveChannel(stats)
+ subScribeSingerMap.clear()
+ singerList.clear()
+ }
+
+ override fun onRemoteAudioStats(stats: RemoteAudioStats?) {
+ super.onRemoteAudioStats(stats)
+ stats ?: return
+ if (KTVApi.routeSelectionConfig.type == GiantChorusRouteSelectionType.RANDOM || KTVApi.routeSelectionConfig.type == GiantChorusRouteSelectionType.TOP_N) return
+ val uid = stats.uid
+ if (uid == mainSingerUid) {
+ mainSingerDelay = stats.e2eDelay
+ }
+// if (uid == mainSingerUid && stats.e2eDelay > 300) {
+// //ToastUtils.showToast("主唱 $mainSingerUid 延迟超过300ms,目前延迟:${stats.ntpE2eDelay}")
+// }
+// if (subScribeSingerMap.any { it.key == uid } && stats.e2eDelay > 300) {
+// //ToastUtils.showToast("当前订阅用户 $uid 延迟超过300ms,目前延迟:${stats.ntpE2eDelay}")
+// }
+ if (uid != mainSingerUid && uid != giantChorusApiConfig.musicStreamUid && subScribeSingerMap.containsKey(uid)) {
+ subScribeSingerMap[uid] = stats.e2eDelay
+ }
+ }
+ })
+
+ mRtcEngine.setParametersEx("{\"che.audio.max_mixed_participants\": 8}", singChannelRtcConnection)
+ mRtcEngine.setParametersEx("{\"rtc.use_audio4\": true}", singChannelRtcConnection)
+
+ // 选路策略处理
+ if (KTVApi.routeSelectionConfig.type == GiantChorusRouteSelectionType.TOP_N || KTVApi.routeSelectionConfig.type == GiantChorusRouteSelectionType.BY_DELAY_AND_TOP_N) {
+ if (newRole == KTVSingRole.LeadSinger) {
+ mRtcEngine.setParametersEx("{\"che.audio.filter_streams\":${KTVApi.routeSelectionConfig.streamNum}}", singChannelRtcConnection)
+ } else {
+ mRtcEngine.setParametersEx("{\"che.audio.filter_streams\":${KTVApi.routeSelectionConfig.streamNum - 1}}", singChannelRtcConnection)
+ }
+ } else {
+ mRtcEngine.setParametersEx("{\"che.audio.filter_streams\": 0}", singChannelRtcConnection)
+ }
+ mRtcEngine.enableAudioVolumeIndicationEx(50, 10, true, singChannelRtcConnection)
+
+ when (newRole) {
+ KTVSingRole.LeadSinger -> {
+ // 更新音频配置
+ mRtcEngine.setAudioScenario(AUDIO_SCENARIO_CHORUS)
+ 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}")
+
+ // mpk流加入频道
+ val options = ChannelMediaOptions()
+ options.autoSubscribeAudio = false
+ options.autoSubscribeVideo = false
+ options.publishMicrophoneTrack = false
+ options.publishMediaPlayerAudioTrack = true
+ options.publishMediaPlayerId = mPlayer.mediaPlayerId
+ options.clientRoleType = CLIENT_ROLE_BROADCASTER
+ // 防止主唱和合唱听见mpk流的声音
+ options.enableAudioRecordingOrPlayout = false
+
+ val rtcConnection = RtcConnection()
+ rtcConnection.channelId = giantChorusApiConfig.chorusChannelName
+ rtcConnection.localUid = giantChorusApiConfig.musicStreamUid
+ mpkConnection = rtcConnection
+
+ mRtcEngine.joinChannelEx(
+ giantChorusApiConfig.musicStreamToken,
+ mpkConnection,
+ options,
+ object : IRtcEngineEventHandler() {
+ override fun onJoinChannelSuccess(channel: String, uid: Int, elapsed: Int) {
+ ktvApiLog("onMPKJoinChannelSuccess, channel: $channel, uid: $uid")
+ }
+
+ override fun onLeaveChannel(stats: RtcStats) {
+ ktvApiLog("onMPKLeaveChannel")
+ }
+ })
+ mRtcEngine.setParametersEx("{\"rtc.use_audio4\": true}", mpkConnection)
+ }
+ KTVSingRole.CoSinger -> {
+ // 防止主唱和合唱听见mpk流的声音
+ mRtcEngine.muteRemoteAudioStreamEx(
+ giantChorusApiConfig.musicStreamUid,
+ true,
+ singChannelRtcConnection
+ )
+
+ // 更新音频配置
+ mRtcEngine.setAudioScenario(AUDIO_SCENARIO_CHORUS)
+ 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}")
+
+ // 预加载歌曲成功
+ // 导唱
+ mPlayer.setPlayerOption("enable_multi_audio_track", 1)
+ if (giantChorusApiConfig.musicType == KTVMusicType.SONG_CODE) {
+ val ret = (mPlayer as IAgoraMusicPlayer).open(songCode, 0) // TODO open failed
+ if (ret != 0) {
+ ktvApiLogError("mpk open failed: $ret")
+ }
+ } else {
+ val ret = mPlayer.open(songUrl, 0) // TODO open failed
+ if (ret != 0) {
+ ktvApiLogError("mpk open failed: $ret")
+ }
+ }
+ }
+ else -> {
+ ktvApiLogError("JoinChorus with Wrong role: $singerRole")
+ }
+ }
+
+ mRtcEngine.muteRemoteAudioStreamEx(giantChorusApiConfig.musicStreamUid, true, singChannelRtcConnection)
+ // 加入演唱频道后,创建data stream
+ renewInnerDataStreamId()
+ }
+
+ private fun leaveChorus2(role: KTVSingRole) {
+ ktvApiLog("leaveChorus: $singerRole")
+ when (role) {
+ KTVSingRole.LeadSinger -> {
+ mRtcEngine.leaveChannelEx(mpkConnection)
+ }
+ KTVSingRole.CoSinger -> {
+ mPlayer.stop()
+
+ // 更新音频配置
+ mRtcEngine.setAudioScenario(AUDIO_SCENARIO_GAME_STREAMING)
+ 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}")
+ }
+ else -> {
+ ktvApiLogError("JoinChorus with wrong role: $singerRole")
+ }
+ }
+ mRtcEngine.leaveChannelEx(singChannelRtcConnection)
+ }
+
+ // ------------------ inner --------------------
+
+ private fun isChorusCoSinger(): Boolean {
+ return singerRole == KTVSingRole.CoSinger
+ }
+
+ private fun sendStreamMessageWithJsonObject(
+ obj: JSONObject,
+ success: (isSendSuccess: Boolean) -> Unit
+ ) {
+ val ret = mRtcEngine.sendStreamMessageEx(innerDataStreamId, obj.toString().toByteArray(), singChannelRtcConnection)
+ if (ret == 0) {
+ success.invoke(true)
+ } else {
+ ktvApiLogError("sendStreamMessageWithJsonObject failed: $ret, innerDataStreamId:$innerDataStreamId")
+ }
+ }
+
+ private fun syncPlayState(
+ state: Constants.MediaPlayerState,
+ error: Constants.MediaPlayerReason
+ ) {
+ val msg: MutableMap = HashMap()
+ msg["cmd"] = "PlayerState"
+ msg["state"] = Constants.MediaPlayerState.getValue(state)
+ msg["error"] = Constants.MediaPlayerReason.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 startDisplayLrc() {
+ ktvApiLog("startDisplayLrc called")
+ mStopDisplayLrc = false
+ displayLrcFuture = scheduledThreadPool.scheduleAtFixedRate(displayLrcTask, 0,20, TimeUnit.MILLISECONDS)
+ }
+
+ // 停止播放歌词
+ private fun stopDisplayLrc() {
+ ktvApiLog("stopDisplayLrc called")
+ mStopDisplayLrc = true
+ displayLrcFuture?.cancel(true)
+ displayLrcFuture = null
+ if (scheduledThreadPool is ScheduledThreadPoolExecutor) {
+ scheduledThreadPool.remove(displayLrcTask)
+ }
+ }
+
+ // ------------------ 评分驱动混音同步 ------------------
+ private fun sendSyncScore() {
+ val jsonObject = JSONObject()
+ jsonObject.put("service", "audio_smart_mixer") // data message的目标消费者(服务)名
+ jsonObject.put("version", "V1") //协议版本号(而非服务版本号)
+ val payloadJson = JSONObject()
+ payloadJson.put("cname", giantChorusApiConfig.chorusChannelName) // 频道名,演唱频道
+ payloadJson.put("uid", giantChorusApiConfig.localUid.toString()) // 自己的uid
+ payloadJson.put("uLv", -1) //user-leve1(用户级别,若无则为 -1,Level 越高,越重要)
+ payloadJson.put("specialLabel", 0) //0: default-mode ,1:这个用户需要被排除出智能混音
+ payloadJson.put("audioRoute", audioRouting) //音频路由:监听 onAudioRouteChanged
+ payloadJson.put("vocalScore", singingScore) //单句打分
+ jsonObject.put("payload", payloadJson)
+ ktvApiLog("sendSyncScore: $jsonObject")
+ sendStreamMessageWithJsonObject(jsonObject) {}
+ }
+
+ // 开始发送分数 3s/次
+ private fun startSyncScore() {
+ mStopSyncScore = false
+ mSyncScoreFuture = scheduledThreadPool.scheduleAtFixedRate(mSyncScoreTask, 0, 3000, TimeUnit.MILLISECONDS)
+ }
+
+ // 停止发送分数
+ private fun stopSyncScore() {
+ mStopSyncScore = true
+ singingScore = 0
+
+ mSyncScoreFuture?.cancel(true)
+ mSyncScoreFuture = null
+ if (scheduledThreadPool is ScheduledThreadPoolExecutor) {
+ scheduledThreadPool.remove(mSyncScoreTask)
+ }
+ }
+
+ // ------------------ 云端合流信息同步 ------------------
+ private fun sendSyncCloudConvergenceStatus() {
+ val jsonObject = JSONObject()
+ jsonObject.put("service", "audio_smart_mixer_status") // data message的目标消费者(服务)名
+ jsonObject.put("version", "V1") //协议版本号(而非服务版本号)
+ val payloadJson = JSONObject()
+ payloadJson.put("Ts", getNtpTimeInMs()) // NTP 时间
+ payloadJson.put("cname", giantChorusApiConfig.chorusChannelName) // 频道名
+ payloadJson.put("status", getCloudConvergenceStatus()) //(-1: unknown,0:非K歌状态,1:K歌播放状态,2:K歌暂停状态)
+ payloadJson.put("bgmUID", mpkConnection?.localUid.toString()) // mpk流的uid
+ payloadJson.put("leadsingerUID", mainSingerUid.toString()) //("-1" = unknown) //主唱Uid
+ jsonObject.put("payload", payloadJson)
+ ktvApiLog("sendSyncCloudConvergenceStatus: $jsonObject")
+ sendStreamMessageWithJsonObject(jsonObject) {}
+ }
+
+ // -1: unknown,0:非K歌状态,1:K歌播放状态,2:K歌暂停状态)
+ private fun getCloudConvergenceStatus(): Int {
+ var status = -1
+ when (this.mediaPlayerState) {
+ MediaPlayerState.PLAYER_STATE_PLAYING -> status = 1
+ MediaPlayerState.PLAYER_STATE_PAUSED -> status = 2
+ else -> {}
+ }
+ return status
+ }
+
+ // 开始发送分数 200ms/次
+ private fun startSyncCloudConvergenceStatus() {
+ mStopSyncCloudConvergenceStatus = false
+ mSyncCloudConvergenceStatusFuture = scheduledThreadPool.scheduleAtFixedRate(mSyncCloudConvergenceStatusTask, 0, 200,TimeUnit.MILLISECONDS)
+ }
+
+ // 停止发送分数
+ private fun stopSyncCloudConvergenceStatus() {
+ mStopSyncCloudConvergenceStatus = true
+
+ mSyncCloudConvergenceStatusFuture?.cancel(true)
+ mSyncCloudConvergenceStatusFuture = null
+ if (scheduledThreadPool is ScheduledThreadPoolExecutor) {
+ scheduledThreadPool.remove(mSyncCloudConvergenceStatusTask)
+ }
+ }
+
+ // ------------------ 延迟选路 ------------------
+ private var mStopProcessDelay = true
+
+ private val mProcessDelayTask = Runnable {
+ if (!mStopProcessDelay && singerRole != KTVSingRole.Audience) {
+ val n = if (singerRole == KTVSingRole.LeadSinger) KTVApi.routeSelectionConfig.streamNum else KTVApi.routeSelectionConfig.streamNum -1
+ val sortedEntries = subScribeSingerMap.entries.sortedBy { it.value }
+ val other = sortedEntries.drop(3)
+ val drop = mutableListOf()
+ if (n > 3) {
+ other.drop(n - 3).forEach { (uid, _) ->
+ drop.add(uid)
+ mRtcEngine.muteRemoteAudioStreamEx(uid, true, singChannelRtcConnection)
+ subScribeSingerMap.remove(uid)
+ }
+ }
+ ktvApiLog("选路重新订阅, drop:$drop")
+
+ val filteredList = singerList.filter { !subScribeSingerMap.containsKey(it) }
+ val filteredList2 = filteredList.filter { !drop.contains(it) }
+ val shuffledList = filteredList2.shuffled()
+ if (subScribeSingerMap.size < 8) {
+ val randomSingers = shuffledList.take(8 - subScribeSingerMap.size)
+ ktvApiLog("选路重新订阅, newSingers:$randomSingers")
+ for (singer in randomSingers) {
+ subScribeSingerMap[singer] = 0
+ mRtcEngine.muteRemoteAudioStreamEx(singer, false, singChannelRtcConnection)
+ }
+ }
+ ktvApiLog("选路重新订阅, newSubScribeSingerMap:$subScribeSingerMap")
+ }
+ }
+
+ private val mProcessSubscribeTask = Runnable {
+ if (!mStopProcessDelay && singerRole != KTVSingRole.Audience) {
+ val n = if (singerRole == KTVSingRole.LeadSinger) KTVApi.routeSelectionConfig.streamNum else KTVApi.routeSelectionConfig.streamNum -1
+ val sortedEntries = subScribeSingerMap.entries.sortedBy { it.value }
+ val mustToHave = sortedEntries.take(3)
+ mustToHave.forEach { (uid, _) ->
+ mRtcEngine.adjustUserPlaybackSignalVolumeEx(uid, 100, singChannelRtcConnection)
+ }
+ val other = sortedEntries.drop(3)
+ if (n > 3) {
+ other.take(n - 3).forEach { (uid, delay) ->
+ if (delay > 300) {
+ mRtcEngine.adjustUserPlaybackSignalVolumeEx(uid, 0, singChannelRtcConnection)
+ } else {
+ mRtcEngine.adjustUserPlaybackSignalVolumeEx(uid, 100, singChannelRtcConnection)
+ }
+ }
+ other.drop(n - 3).forEach { (uid, _) ->
+ mRtcEngine.adjustUserPlaybackSignalVolumeEx(uid, 0, singChannelRtcConnection)
+ }
+ }
+
+ ktvApiLog("选路排序+调整播放音量, mustToHave:$mustToHave, other:$other")
+ }
+ }
+
+ private var mProcessDelayFuture :ScheduledFuture<*>? = null
+ private var mProcessSubscribeFuture :ScheduledFuture<*>? = null
+ private fun startProcessDelay() {
+ if (KTVApi.routeSelectionConfig.type == GiantChorusRouteSelectionType.TOP_N || KTVApi.routeSelectionConfig.type == GiantChorusRouteSelectionType.RANDOM) return
+ mStopProcessDelay = false
+ mProcessDelayFuture = scheduledThreadPool.scheduleAtFixedRate(mProcessDelayTask, 10000, 20000, TimeUnit.MILLISECONDS)
+ mProcessSubscribeFuture = scheduledThreadPool.scheduleAtFixedRate(mProcessSubscribeTask,15000,20000, TimeUnit.MILLISECONDS)
+ }
+
+ private fun stopProcessDelay() {
+ mStopProcessDelay = true
+
+ mProcessDelayFuture?.cancel(true)
+ mProcessSubscribeFuture?.cancel(true)
+ mProcessDelayFuture = null
+ if (scheduledThreadPool is ScheduledThreadPoolExecutor) {
+ scheduledThreadPool.remove(mProcessDelayTask)
+ scheduledThreadPool.remove(mProcessSubscribeTask)
+ }
+ }
+
+ private fun loadLyric(songNo: Long, onLoadLyricCallback: (songNo: Long, lyricUrl: String?) -> Unit) {
+ ktvApiLog("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) {
+ ktvApiLog("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) {
+ ktvApiLogError("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 {
+ ktvApiLogError("getNtpTimeInMs DeviceDelay is zero!!!")
+ System.currentTimeMillis()
+ }
+ }
+
+ private fun runOnMainThread(r: Runnable) {
+ if (Thread.currentThread() == mainHandler.looper.thread) {
+ r.run()
+ } else {
+ mainHandler.post(r)
+ }
+ }
+
+ // ------------------------ AgoraRtcEvent ------------------------
+ private fun dealWithStreamMessage(uid: Int, streamId: Int, data: ByteArray?) {
+ val jsonMsg: JSONObject
+ val messageData = data ?: return
+ try {
+ val strMsg = String(messageData)
+ jsonMsg = JSONObject(strMsg)
+ if (!jsonMsg.has("cmd")) return
+ 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(KTVApi.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
+ if (KTVApi.debugMode) {
+ ktvApiLog("play_status_seek: " + diff + " audioPlayoutDelay:" + audioPlayoutDelay + " 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 (jsonMsg.has("ver")) {
+ recvFromDataStream = false
+ } else {
+ recvFromDataStream = true
+ 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")
+ ktvApiLog("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.MediaPlayerReason.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) {
+ ktvApiLogError("onStreamMessage:$exp")
+ }
+ }
+
+ private fun dealWithAudioMetadata(uid: Int, data: ByteArray?) {
+ val messageData = data ?: return
+ val lrcTime = LrcTimeOuterClass.LrcTime.parseFrom(messageData)
+ if (lrcTime.type == LrcTimeOuterClass.MsgType.LRC_TIME) { //同步歌词
+ val realPosition = lrcTime.ts
+ val songId = lrcTime.songId
+ val curTs = if (this.songIdentifier == songId) realPosition else 0
+ 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
+ }
+ }
+ }
+
+ // ------------------------ 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
+ ) {
+ ktvApiLog("onMusicCollectionResult, requestId: $requestId, list: $list, errorCode: $errorCode")
+ 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)
+ }
+
+
+ override fun onSongSimpleInfoResult(
+ requestId: String?,
+ songCode: Long,
+ simpleInfo: String,
+ errorCode: Int
+ ) {}
+
+ // ------------------------ AgoraRtcMediaPlayerDelegate ------------------------
+ private var duration: Long = 0
+ override fun onPlayerStateChanged(
+ state: Constants.MediaPlayerState?,
+ reason: Constants.MediaPlayerReason?
+ ) {
+ val mediaPlayerState = state ?: return
+ val mediaPlayerError = reason ?: return
+ ktvApiLog("onPlayerStateChanged called, state: $mediaPlayerState, error: $mediaPlayerError")
+ this.mediaPlayerState = mediaPlayerState
+ when (mediaPlayerState) {
+ MediaPlayerState.PLAYER_STATE_OPEN_COMPLETED -> {
+ duration = mPlayer.duration
+ this.localPlayerPosition = 0
+ // 伴奏
+ mPlayer.selectMultiAudioTrack(1, 1)
+ if (this.singerRole == KTVSingRole.SoloSinger ||
+ this.singerRole == KTVSingRole.LeadSinger
+ ) {
+ mPlayer.play()
+ }
+ startProcessDelay()
+ }
+ MediaPlayerState.PLAYER_STATE_PLAYING -> {
+ mRtcEngine.adjustPlaybackSignalVolume(KTVApi.remoteVolume)
+ }
+ MediaPlayerState.PLAYER_STATE_PAUSED -> {
+ mRtcEngine.adjustPlaybackSignalVolume(100)
+ }
+ MediaPlayerState.PLAYER_STATE_STOPPED -> {
+ mRtcEngine.adjustPlaybackSignalVolume(100)
+ duration = 0
+ stopProcessDelay()
+ }
+ 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
+ msg["forward"] = true
+ msg["ver"] = lyricSyncVersion
+ 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 onPlayerCacheStats(stats: CacheStatistics?) {}
+
+ override fun onPlayerPlaybackStats(stats: PlayerPlaybackStats?) {}
+
+ override fun onAudioVolumeIndication(volume: Int) {}
+}
\ No newline at end of file
diff --git a/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/LrcTimeOuterClass.java b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/LrcTimeOuterClass.java
new file mode 100644
index 0000000..9e69709
--- /dev/null
+++ b/KTVAPI/Android/lib_ktvapi/src/main/java/io/agora/ktvapi/LrcTimeOuterClass.java
@@ -0,0 +1,1042 @@
+package io.agora.ktvapi;// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: LrcTime.proto
+
+public final class LrcTimeOuterClass {
+ private LrcTimeOuterClass() {}
+ public static void registerAllExtensions(
+ com.google.protobuf.ExtensionRegistryLite registry) {
+ }
+
+ public static void registerAllExtensions(
+ com.google.protobuf.ExtensionRegistry registry) {
+ registerAllExtensions(
+ (com.google.protobuf.ExtensionRegistryLite) registry);
+ }
+ /**
+ * Protobuf enum {@code MsgType}
+ */
+ public enum MsgType
+ implements com.google.protobuf.ProtocolMessageEnum {
+ /**
+ * UNKNOWN_TYPE = 0;
+ */
+ UNKNOWN_TYPE(0),
+ /**
+ * LRC_TIME = 1001;
+ */
+ LRC_TIME(1001),
+ UNRECOGNIZED(-1),
+ ;
+
+ /**
+ * UNKNOWN_TYPE = 0;
+ */
+ public static final int UNKNOWN_TYPE_VALUE = 0;
+ /**
+ * LRC_TIME = 1001;
+ */
+ public static final int LRC_TIME_VALUE = 1001;
+
+
+ public final int getNumber() {
+ if (this == UNRECOGNIZED) {
+ throw new IllegalArgumentException(
+ "Can't get the number of an unknown enum value.");
+ }
+ return value;
+ }
+
+ /**
+ * @param value The numeric wire value of the corresponding enum entry.
+ * @return The enum associated with the given numeric wire value.
+ * @deprecated Use {@link #forNumber(int)} instead.
+ */
+ @Deprecated
+ public static MsgType valueOf(int value) {
+ return forNumber(value);
+ }
+
+ /**
+ * @param value The numeric wire value of the corresponding enum entry.
+ * @return The enum associated with the given numeric wire value.
+ */
+ public static MsgType forNumber(int value) {
+ switch (value) {
+ case 0: return UNKNOWN_TYPE;
+ case 1001: return LRC_TIME;
+ default: return null;
+ }
+ }
+
+ public static com.google.protobuf.Internal.EnumLiteMap
+ internalGetValueMap() {
+ return internalValueMap;
+ }
+ private static final com.google.protobuf.Internal.EnumLiteMap<
+ MsgType> internalValueMap =
+ new com.google.protobuf.Internal.EnumLiteMap() {
+ public MsgType findValueByNumber(int number) {
+ return MsgType.forNumber(number);
+ }
+ };
+
+ public final com.google.protobuf.Descriptors.EnumValueDescriptor
+ getValueDescriptor() {
+ if (this == UNRECOGNIZED) {
+ throw new IllegalStateException(
+ "Can't get the descriptor of an unrecognized enum value.");
+ }
+ return getDescriptor().getValues().get(ordinal());
+ }
+ public final com.google.protobuf.Descriptors.EnumDescriptor
+ getDescriptorForType() {
+ return getDescriptor();
+ }
+ public static final com.google.protobuf.Descriptors.EnumDescriptor
+ getDescriptor() {
+ return LrcTimeOuterClass.getDescriptor().getEnumTypes().get(0);
+ }
+
+ private static final MsgType[] VALUES = values();
+
+ public static MsgType valueOf(
+ com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
+ if (desc.getType() != getDescriptor()) {
+ throw new IllegalArgumentException(
+ "EnumValueDescriptor is not for this type.");
+ }
+ if (desc.getIndex() == -1) {
+ return UNRECOGNIZED;
+ }
+ return VALUES[desc.getIndex()];
+ }
+
+ private final int value;
+
+ private MsgType(int value) {
+ this.value = value;
+ }
+
+ // @@protoc_insertion_point(enum_scope:MsgType)
+ }
+
+ public interface LrcTimeOrBuilder extends
+ // @@protoc_insertion_point(interface_extends:LrcTime)
+ com.google.protobuf.MessageOrBuilder {
+
+ /**
+ * .MsgType type = 1;
+ * @return The enum numeric value on the wire for type.
+ */
+ int getTypeValue();
+ /**
+ * .MsgType type = 1;
+ * @return The type.
+ */
+ MsgType getType();
+
+ /**
+ * bool forward = 2;
+ * @return The forward.
+ */
+ boolean getForward();
+
+ /**
+ * int64 ts = 3;
+ * @return The ts.
+ */
+ long getTs();
+
+ /**
+ * string songId = 4;
+ * @return The songId.
+ */
+ String getSongId();
+ /**
+ * string songId = 4;
+ * @return The bytes for songId.
+ */
+ com.google.protobuf.ByteString
+ getSongIdBytes();
+
+ /**
+ * int32 uid = 5;
+ * @return The uid.
+ */
+ int getUid();
+ }
+ /**
+ * Protobuf type {@code LrcTime}
+ */
+ public static final class LrcTime extends
+ com.google.protobuf.GeneratedMessageV3 implements
+ // @@protoc_insertion_point(message_implements:LrcTime)
+ LrcTimeOrBuilder {
+ private static final long serialVersionUID = 0L;
+ // Use LrcTime.newBuilder() to construct.
+ private LrcTime(com.google.protobuf.GeneratedMessageV3.Builder> builder) {
+ super(builder);
+ }
+ private LrcTime() {
+ type_ = 0;
+ songId_ = "";
+ }
+
+ @Override
+ @SuppressWarnings({"unused"})
+ protected Object newInstance(
+ UnusedPrivateParameter unused) {
+ return new LrcTime();
+ }
+
+ @Override
+ public final com.google.protobuf.UnknownFieldSet
+ getUnknownFields() {
+ return this.unknownFields;
+ }
+ private LrcTime(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ this();
+ if (extensionRegistry == null) {
+ throw new NullPointerException();
+ }
+ com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+ com.google.protobuf.UnknownFieldSet.newBuilder();
+ try {
+ boolean done = false;
+ while (!done) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ done = true;
+ break;
+ case 8: {
+ int rawValue = input.readEnum();
+
+ type_ = rawValue;
+ break;
+ }
+ case 16: {
+
+ forward_ = input.readBool();
+ break;
+ }
+ case 24: {
+
+ ts_ = input.readInt64();
+ break;
+ }
+ case 34: {
+ String s = input.readStringRequireUtf8();
+
+ songId_ = s;
+ break;
+ }
+ case 40: {
+
+ uid_ = input.readInt32();
+ break;
+ }
+ default: {
+ if (!parseUnknownField(
+ input, unknownFields, extensionRegistry, tag)) {
+ done = true;
+ }
+ break;
+ }
+ }
+ }
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ throw e.setUnfinishedMessage(this);
+ } catch (java.io.IOException e) {
+ throw new com.google.protobuf.InvalidProtocolBufferException(
+ e).setUnfinishedMessage(this);
+ } finally {
+ this.unknownFields = unknownFields.build();
+ makeExtensionsImmutable();
+ }
+ }
+ public static final com.google.protobuf.Descriptors.Descriptor
+ getDescriptor() {
+ return LrcTimeOuterClass.internal_static_LrcTime_descriptor;
+ }
+
+ @Override
+ protected FieldAccessorTable
+ internalGetFieldAccessorTable() {
+ return LrcTimeOuterClass.internal_static_LrcTime_fieldAccessorTable
+ .ensureFieldAccessorsInitialized(
+ LrcTime.class, Builder.class);
+ }
+
+ public static final int TYPE_FIELD_NUMBER = 1;
+ private int type_;
+ /**
+ * .MsgType type = 1;
+ * @return The enum numeric value on the wire for type.
+ */
+ @Override public int getTypeValue() {
+ return type_;
+ }
+ /**
+ * .MsgType type = 1;
+ * @return The type.
+ */
+ @Override public MsgType getType() {
+ @SuppressWarnings("deprecation")
+ MsgType result = MsgType.valueOf(type_);
+ return result == null ? MsgType.UNRECOGNIZED : result;
+ }
+
+ public static final int FORWARD_FIELD_NUMBER = 2;
+ private boolean forward_;
+ /**
+ * bool forward = 2;
+ * @return The forward.
+ */
+ @Override
+ public boolean getForward() {
+ return forward_;
+ }
+
+ public static final int TS_FIELD_NUMBER = 3;
+ private long ts_;
+ /**
+ * int64 ts = 3;
+ * @return The ts.
+ */
+ @Override
+ public long getTs() {
+ return ts_;
+ }
+
+ public static final int SONGID_FIELD_NUMBER = 4;
+ private volatile Object songId_;
+ /**
+ * string songId = 4;
+ * @return The songId.
+ */
+ @Override
+ public String getSongId() {
+ Object ref = songId_;
+ if (ref instanceof String) {
+ return (String) ref;
+ } else {
+ com.google.protobuf.ByteString bs =
+ (com.google.protobuf.ByteString) ref;
+ String s = bs.toStringUtf8();
+ songId_ = s;
+ return s;
+ }
+ }
+ /**
+ * string songId = 4;
+ * @return The bytes for songId.
+ */
+ @Override
+ public com.google.protobuf.ByteString
+ getSongIdBytes() {
+ Object ref = songId_;
+ if (ref instanceof String) {
+ com.google.protobuf.ByteString b =
+ com.google.protobuf.ByteString.copyFromUtf8(
+ (String) ref);
+ songId_ = b;
+ return b;
+ } else {
+ return (com.google.protobuf.ByteString) ref;
+ }
+ }
+
+ public static final int UID_FIELD_NUMBER = 5;
+ private int uid_;
+ /**
+ * int32 uid = 5;
+ * @return The uid.
+ */
+ @Override
+ public int getUid() {
+ return uid_;
+ }
+
+ private byte memoizedIsInitialized = -1;
+ @Override
+ public final boolean isInitialized() {
+ byte isInitialized = memoizedIsInitialized;
+ if (isInitialized == 1) return true;
+ if (isInitialized == 0) return false;
+
+ memoizedIsInitialized = 1;
+ return true;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.CodedOutputStream output)
+ throws java.io.IOException {
+ if (type_ != MsgType.UNKNOWN_TYPE.getNumber()) {
+ output.writeEnum(1, type_);
+ }
+ if (forward_ != false) {
+ output.writeBool(2, forward_);
+ }
+ if (ts_ != 0L) {
+ output.writeInt64(3, ts_);
+ }
+ if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(songId_)) {
+ com.google.protobuf.GeneratedMessageV3.writeString(output, 4, songId_);
+ }
+ if (uid_ != 0) {
+ output.writeInt32(5, uid_);
+ }
+ unknownFields.writeTo(output);
+ }
+
+ @Override
+ public int getSerializedSize() {
+ int size = memoizedSize;
+ if (size != -1) return size;
+
+ size = 0;
+ if (type_ != MsgType.UNKNOWN_TYPE.getNumber()) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeEnumSize(1, type_);
+ }
+ if (forward_ != false) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeBoolSize(2, forward_);
+ }
+ if (ts_ != 0L) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeInt64Size(3, ts_);
+ }
+ if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(songId_)) {
+ size += com.google.protobuf.GeneratedMessageV3.computeStringSize(4, songId_);
+ }
+ if (uid_ != 0) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeInt32Size(5, uid_);
+ }
+ size += unknownFields.getSerializedSize();
+ memoizedSize = size;
+ return size;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof LrcTime)) {
+ return super.equals(obj);
+ }
+ LrcTime other = (LrcTime) obj;
+
+ if (type_ != other.type_) return false;
+ if (getForward()
+ != other.getForward()) return false;
+ if (getTs()
+ != other.getTs()) return false;
+ if (!getSongId()
+ .equals(other.getSongId())) return false;
+ if (getUid()
+ != other.getUid()) return false;
+ if (!unknownFields.equals(other.unknownFields)) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ if (memoizedHashCode != 0) {
+ return memoizedHashCode;
+ }
+ int hash = 41;
+ hash = (19 * hash) + getDescriptor().hashCode();
+ hash = (37 * hash) + TYPE_FIELD_NUMBER;
+ hash = (53 * hash) + type_;
+ hash = (37 * hash) + FORWARD_FIELD_NUMBER;
+ hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean(
+ getForward());
+ hash = (37 * hash) + TS_FIELD_NUMBER;
+ hash = (53 * hash) + com.google.protobuf.Internal.hashLong(
+ getTs());
+ hash = (37 * hash) + SONGID_FIELD_NUMBER;
+ hash = (53 * hash) + getSongId().hashCode();
+ hash = (37 * hash) + UID_FIELD_NUMBER;
+ hash = (53 * hash) + getUid();
+ hash = (29 * hash) + unknownFields.hashCode();
+ memoizedHashCode = hash;
+ return hash;
+ }
+
+ public static LrcTime parseFrom(
+ java.nio.ByteBuffer data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data);
+ }
+ public static LrcTime parseFrom(
+ java.nio.ByteBuffer data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data, extensionRegistry);
+ }
+ public static LrcTime parseFrom(
+ com.google.protobuf.ByteString data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data);
+ }
+ public static LrcTime parseFrom(
+ com.google.protobuf.ByteString data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data, extensionRegistry);
+ }
+ public static LrcTime parseFrom(byte[] data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data);
+ }
+ public static LrcTime parseFrom(
+ byte[] data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data, extensionRegistry);
+ }
+ public static LrcTime parseFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3
+ .parseWithIOException(PARSER, input);
+ }
+ public static LrcTime parseFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3
+ .parseWithIOException(PARSER, input, extensionRegistry);
+ }
+ public static LrcTime parseDelimitedFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3
+ .parseDelimitedWithIOException(PARSER, input);
+ }
+ public static LrcTime parseDelimitedFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3
+ .parseDelimitedWithIOException(PARSER, input, extensionRegistry);
+ }
+ public static LrcTime parseFrom(
+ com.google.protobuf.CodedInputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3
+ .parseWithIOException(PARSER, input);
+ }
+ public static LrcTime parseFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3
+ .parseWithIOException(PARSER, input, extensionRegistry);
+ }
+
+ @Override
+ public Builder newBuilderForType() { return newBuilder(); }
+ public static Builder newBuilder() {
+ return DEFAULT_INSTANCE.toBuilder();
+ }
+ public static Builder newBuilder(LrcTime prototype) {
+ return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+ }
+ @Override
+ public Builder toBuilder() {
+ return this == DEFAULT_INSTANCE
+ ? new Builder() : new Builder().mergeFrom(this);
+ }
+
+ @Override
+ protected Builder newBuilderForType(
+ BuilderParent parent) {
+ Builder builder = new Builder(parent);
+ return builder;
+ }
+ /**
+ * Protobuf type {@code LrcTime}
+ */
+ public static final class Builder extends
+ com.google.protobuf.GeneratedMessageV3.Builder implements
+ // @@protoc_insertion_point(builder_implements:LrcTime)
+ LrcTimeOrBuilder {
+ public static final com.google.protobuf.Descriptors.Descriptor
+ getDescriptor() {
+ return LrcTimeOuterClass.internal_static_LrcTime_descriptor;
+ }
+
+ @Override
+ protected FieldAccessorTable
+ internalGetFieldAccessorTable() {
+ return LrcTimeOuterClass.internal_static_LrcTime_fieldAccessorTable
+ .ensureFieldAccessorsInitialized(
+ LrcTime.class, Builder.class);
+ }
+
+ // Construct using LrcTimeOuterClass.LrcTime.newBuilder()
+ private Builder() {
+ maybeForceBuilderInitialization();
+ }
+
+ private Builder(
+ BuilderParent parent) {
+ super(parent);
+ maybeForceBuilderInitialization();
+ }
+ private void maybeForceBuilderInitialization() {
+ if (com.google.protobuf.GeneratedMessageV3
+ .alwaysUseFieldBuilders) {
+ }
+ }
+ @Override
+ public Builder clear() {
+ super.clear();
+ type_ = 0;
+
+ forward_ = false;
+
+ ts_ = 0L;
+
+ songId_ = "";
+
+ uid_ = 0;
+
+ return this;
+ }
+
+ @Override
+ public com.google.protobuf.Descriptors.Descriptor
+ getDescriptorForType() {
+ return LrcTimeOuterClass.internal_static_LrcTime_descriptor;
+ }
+
+ @Override
+ public LrcTime getDefaultInstanceForType() {
+ return LrcTime.getDefaultInstance();
+ }
+
+ @Override
+ public LrcTime build() {
+ LrcTime result = buildPartial();
+ if (!result.isInitialized()) {
+ throw newUninitializedMessageException(result);
+ }
+ return result;
+ }
+
+ @Override
+ public LrcTime buildPartial() {
+ LrcTime result = new LrcTime(this);
+ result.type_ = type_;
+ result.forward_ = forward_;
+ result.ts_ = ts_;
+ result.songId_ = songId_;
+ result.uid_ = uid_;
+ onBuilt();
+ return result;
+ }
+
+ @Override
+ public Builder clone() {
+ return super.clone();
+ }
+ @Override
+ public Builder setField(
+ com.google.protobuf.Descriptors.FieldDescriptor field,
+ Object value) {
+ return super.setField(field, value);
+ }
+ @Override
+ public Builder clearField(
+ com.google.protobuf.Descriptors.FieldDescriptor field) {
+ return super.clearField(field);
+ }
+ @Override
+ public Builder clearOneof(
+ com.google.protobuf.Descriptors.OneofDescriptor oneof) {
+ return super.clearOneof(oneof);
+ }
+ @Override
+ public Builder setRepeatedField(
+ com.google.protobuf.Descriptors.FieldDescriptor field,
+ int index, Object value) {
+ return super.setRepeatedField(field, index, value);
+ }
+ @Override
+ public Builder addRepeatedField(
+ com.google.protobuf.Descriptors.FieldDescriptor field,
+ Object value) {
+ return super.addRepeatedField(field, value);
+ }
+ @Override
+ public Builder mergeFrom(com.google.protobuf.Message other) {
+ if (other instanceof LrcTime) {
+ return mergeFrom((LrcTime)other);
+ } else {
+ super.mergeFrom(other);
+ return this;
+ }
+ }
+
+ public Builder mergeFrom(LrcTime other) {
+ if (other == LrcTime.getDefaultInstance()) return this;
+ if (other.type_ != 0) {
+ setTypeValue(other.getTypeValue());
+ }
+ if (other.getForward() != false) {
+ setForward(other.getForward());
+ }
+ if (other.getTs() != 0L) {
+ setTs(other.getTs());
+ }
+ if (!other.getSongId().isEmpty()) {
+ songId_ = other.songId_;
+ onChanged();
+ }
+ if (other.getUid() != 0) {
+ setUid(other.getUid());
+ }
+ this.mergeUnknownFields(other.unknownFields);
+ onChanged();
+ return this;
+ }
+
+ @Override
+ public final boolean isInitialized() {
+ return true;
+ }
+
+ @Override
+ public Builder mergeFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ LrcTime parsedMessage = null;
+ try {
+ parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ parsedMessage = (LrcTime) e.getUnfinishedMessage();
+ throw e.unwrapIOException();
+ } finally {
+ if (parsedMessage != null) {
+ mergeFrom(parsedMessage);
+ }
+ }
+ return this;
+ }
+
+ private int type_ = 0;
+ /**
+ * .MsgType type = 1;
+ * @return The enum numeric value on the wire for type.
+ */
+ @Override public int getTypeValue() {
+ return type_;
+ }
+ /**
+ * .MsgType type = 1;
+ * @param value The enum numeric value on the wire for type to set.
+ * @return This builder for chaining.
+ */
+ public Builder setTypeValue(int value) {
+
+ type_ = value;
+ onChanged();
+ return this;
+ }
+ /**
+ * .MsgType type = 1;
+ * @return The type.
+ */
+ @Override
+ public MsgType getType() {
+ @SuppressWarnings("deprecation")
+ MsgType result = MsgType.valueOf(type_);
+ return result == null ? MsgType.UNRECOGNIZED : result;
+ }
+ /**
+ * .MsgType type = 1;
+ * @param value The type to set.
+ * @return This builder for chaining.
+ */
+ public Builder setType(MsgType value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ type_ = value.getNumber();
+ onChanged();
+ return this;
+ }
+ /**
+ * .MsgType type = 1;
+ * @return This builder for chaining.
+ */
+ public Builder clearType() {
+
+ type_ = 0;
+ onChanged();
+ return this;
+ }
+
+ private boolean forward_ ;
+ /**
+ * bool forward = 2;
+ * @return The forward.
+ */
+ @Override
+ public boolean getForward() {
+ return forward_;
+ }
+ /**
+ * bool forward = 2;
+ * @param value The forward to set.
+ * @return This builder for chaining.
+ */
+ public Builder setForward(boolean value) {
+
+ forward_ = value;
+ onChanged();
+ return this;
+ }
+ /**
+ * bool forward = 2;
+ * @return This builder for chaining.
+ */
+ public Builder clearForward() {
+
+ forward_ = false;
+ onChanged();
+ return this;
+ }
+
+ private long ts_ ;
+ /**
+ * int64 ts = 3;
+ * @return The ts.
+ */
+ @Override
+ public long getTs() {
+ return ts_;
+ }
+ /**
+ * int64 ts = 3;
+ * @param value The ts to set.
+ * @return This builder for chaining.
+ */
+ public Builder setTs(long value) {
+
+ ts_ = value;
+ onChanged();
+ return this;
+ }
+ /**
+ * int64 ts = 3;
+ * @return This builder for chaining.
+ */
+ public Builder clearTs() {
+
+ ts_ = 0L;
+ onChanged();
+ return this;
+ }
+
+ private Object songId_ = "";
+ /**
+ * string songId = 4;
+ * @return The songId.
+ */
+ public String getSongId() {
+ Object ref = songId_;
+ if (!(ref instanceof String)) {
+ com.google.protobuf.ByteString bs =
+ (com.google.protobuf.ByteString) ref;
+ String s = bs.toStringUtf8();
+ songId_ = s;
+ return s;
+ } else {
+ return (String) ref;
+ }
+ }
+ /**
+ * string songId = 4;
+ * @return The bytes for songId.
+ */
+ public com.google.protobuf.ByteString
+ getSongIdBytes() {
+ Object ref = songId_;
+ if (ref instanceof String) {
+ com.google.protobuf.ByteString b =
+ com.google.protobuf.ByteString.copyFromUtf8(
+ (String) ref);
+ songId_ = b;
+ return b;
+ } else {
+ return (com.google.protobuf.ByteString) ref;
+ }
+ }
+ /**
+ * string songId = 4;
+ * @param value The songId to set.
+ * @return This builder for chaining.
+ */
+ public Builder setSongId(
+ String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ songId_ = value;
+ onChanged();
+ return this;
+ }
+ /**
+ * string songId = 4;
+ * @return This builder for chaining.
+ */
+ public Builder clearSongId() {
+
+ songId_ = getDefaultInstance().getSongId();
+ onChanged();
+ return this;
+ }
+ /**
+ * string songId = 4;
+ * @param value The bytes for songId to set.
+ * @return This builder for chaining.
+ */
+ public Builder setSongIdBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+
+ songId_ = value;
+ onChanged();
+ return this;
+ }
+
+ private int uid_ ;
+ /**
+ * int32 uid = 5;
+ * @return The uid.
+ */
+ @Override
+ public int getUid() {
+ return uid_;
+ }
+ /**
+ * int32 uid = 5;
+ * @param value The uid to set.
+ * @return This builder for chaining.
+ */
+ public Builder setUid(int value) {
+
+ uid_ = value;
+ onChanged();
+ return this;
+ }
+ /**
+ * int32 uid = 5;
+ * @return This builder for chaining.
+ */
+ public Builder clearUid() {
+
+ uid_ = 0;
+ onChanged();
+ return this;
+ }
+ @Override
+ public final Builder setUnknownFields(
+ final com.google.protobuf.UnknownFieldSet unknownFields) {
+ return super.setUnknownFields(unknownFields);
+ }
+
+ @Override
+ public final Builder mergeUnknownFields(
+ final com.google.protobuf.UnknownFieldSet unknownFields) {
+ return super.mergeUnknownFields(unknownFields);
+ }
+
+
+ // @@protoc_insertion_point(builder_scope:LrcTime)
+ }
+
+ // @@protoc_insertion_point(class_scope:LrcTime)
+ private static final LrcTime DEFAULT_INSTANCE;
+ static {
+ DEFAULT_INSTANCE = new LrcTime();
+ }
+
+ public static LrcTime getDefaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ private static final com.google.protobuf.Parser
+ PARSER = new com.google.protobuf.AbstractParser() {
+ @Override
+ public LrcTime parsePartialFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return new LrcTime(input, extensionRegistry);
+ }
+ };
+
+ public static com.google.protobuf.Parser parser() {
+ return PARSER;
+ }
+
+ @Override
+ public com.google.protobuf.Parser getParserForType() {
+ return PARSER;
+ }
+
+ @Override
+ public LrcTime getDefaultInstanceForType() {
+ return DEFAULT_INSTANCE;
+ }
+
+ }
+
+ private static final com.google.protobuf.Descriptors.Descriptor
+ internal_static_LrcTime_descriptor;
+ private static final
+ com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ internal_static_LrcTime_fieldAccessorTable;
+
+ public static com.google.protobuf.Descriptors.FileDescriptor
+ getDescriptor() {
+ return descriptor;
+ }
+ private static com.google.protobuf.Descriptors.FileDescriptor
+ descriptor;
+ static {
+ String[] descriptorData = {
+ "\n\rLrcTime.proto\"[\n\007LrcTime\022\026\n\004type\030\001 \001(\016" +
+ "2\010.MsgType\022\017\n\007forward\030\002 \001(\010\022\n\n\002ts\030\003 \001(\003\022" +
+ "\016\n\006songId\030\004 \001(\t\022\013\n\003uid\030\005 \001(\005**\n\007MsgType\022" +
+ "\020\n\014UNKNOWN_TYPE\020\000\022\r\n\010LRC_TIME\020\351\007b\006proto3"
+ };
+ descriptor = com.google.protobuf.Descriptors.FileDescriptor
+ .internalBuildGeneratedFileFrom(descriptorData,
+ new com.google.protobuf.Descriptors.FileDescriptor[] {
+ });
+ internal_static_LrcTime_descriptor =
+ getDescriptor().getMessageTypes().get(0);
+ internal_static_LrcTime_fieldAccessorTable = new
+ com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
+ internal_static_LrcTime_descriptor,
+ new String[] { "Type", "Forward", "Ts", "SongId", "Uid", });
+ }
+
+ // @@protoc_insertion_point(outer_class_scope)
+}
diff --git a/KTVAPI/iOS/Classes/APIReporter.swift b/KTVAPI/iOS/Classes/APIReporter.swift
new file mode 100644
index 0000000..936e59b
--- /dev/null
+++ b/KTVAPI/iOS/Classes/APIReporter.swift
@@ -0,0 +1,159 @@
+//
+// APIReporter.swift
+// CallAPI
+//
+// Created by wushengtao on 2024/4/8.
+//
+
+import AgoraRtcKit
+
+
+/// 场景化类型
+public enum APIType: Int {
+ case ktv = 1 //K歌
+ case call = 2 //呼叫
+ case beauty = 3 //美颜
+ case videoLoader = 4 //秒开/秒切
+ case pk = 5 //团战
+ case vitualSpace = 6 //
+ case screenSpace = 7 //屏幕共享
+ case audioScenario = 8 //音频scenario
+}
+
+enum APIEventType: Int {
+ case api = 0 //api事件
+ case cost //耗时事件
+ case custom //自定义事件
+}
+
+struct ApiEventKey {
+ static let type = "type"
+ static let desc = "desc"
+ static let apiValue = "apiValue"
+ static let ts = "ts"
+ static let ext = "ext"
+}
+
+struct APICostEvent {
+ static let channelUsage = "channelUsage" //频道使用耗时
+ static let firstFrameActual = "firstFrameActual" //首帧实际耗时
+ static let firstFramePerceived = "firstFramePerceived" //首帧感官耗时
+}
+
+let formatter = DateFormatter()
+func debugApiPrint(_ message: String) {
+#if DEBUG
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
+ let timeString = formatter.string(from: Date())
+ print("\(timeString) \(message)")
+#endif
+}
+
+@objcMembers
+public class APIReporter: NSObject {
+ private var engine: AgoraRtcEngineKit
+ private let messsageId: String = "agora:scenarioAPI"
+ private var category: String
+ private var durationEventStartMap: [String: Int64] = [:]
+
+ //MARK: public
+ public init(type: APIType, version: String, engine: AgoraRtcEngineKit) {
+ self.category = "\(type.rawValue)_iOS_\(version)"
+ self.engine = engine
+ super.init()
+
+ configParameters()
+ }
+
+ public func reportFuncEvent(name: String, value: [String: Any], ext: [String: Any]) {
+ let content = "[APIReporter]reportFuncEvent: \(name) value: \(value) ext: \(ext)"
+ debugApiPrint(content)
+ let eventMap: [String: Any] = [ApiEventKey.type: APIEventType.api.rawValue, ApiEventKey.desc: name]
+ let labelMap: [String: Any] = [ApiEventKey.apiValue: value, ApiEventKey.ts: getCurrentTs(), ApiEventKey.ext: ext]
+ let event = convertToJSONString(eventMap) ?? ""
+ let label = convertToJSONString(labelMap) ?? ""
+ engine.sendCustomReportMessage(messsageId,
+ category: category,
+ event: event,
+ label: label,
+ value: 0)
+ }
+
+ public func startDurationEvent(name: String) {
+ durationEventStartMap[name] = getCurrentTs()
+ }
+
+ public func endDurationEvent(name: String, ext: [String: Any]) {
+ guard let beginTs = durationEventStartMap[name] else {return}
+ durationEventStartMap.removeValue(forKey: name)
+ let ts = getCurrentTs()
+ let cost = Int(ts - beginTs)
+
+ reportCostEvent(ts: ts, name: name, cost: cost, ext: ext)
+ }
+
+ public func reportCostEvent(name: String, cost: Int, ext: [String: Any]) {
+ durationEventStartMap.removeValue(forKey: name)
+ reportCostEvent(ts: getCurrentTs(), name: name, cost: cost, ext: ext)
+ }
+
+ public func reportCustomEvent(name: String, ext: [String: Any]) {
+ let content = "[APIReporter]reportCustomEvent: \(name) ext: \(ext)"
+ debugApiPrint(content)
+ let eventMap: [String: Any] = [ApiEventKey.type: APIEventType.custom.rawValue, ApiEventKey.desc: name]
+ let labelMap: [String: Any] = [ApiEventKey.ts: getCurrentTs(), ApiEventKey.ext: ext]
+ let event = convertToJSONString(eventMap) ?? ""
+ let label = convertToJSONString(labelMap) ?? ""
+ engine.sendCustomReportMessage(messsageId,
+ category: category,
+ event: event,
+ label: label,
+ value: 0)
+ }
+
+ public func writeLog(content: String, level: AgoraLogLevel) {
+ engine.writeLog(level, content: content)
+ }
+
+ public func cleanCache() {
+ durationEventStartMap.removeAll()
+ }
+
+ //MARK: private
+ private func reportCostEvent(ts: Int64, name: String, cost: Int, ext: [String: Any]) {
+ let content = "[APIReporter]reportCostEvent: \(name) cost: \(cost) ms ext: \(ext)"
+ debugApiPrint(content)
+ writeLog(content: content, level: .info)
+ let eventMap: [String: Any] = [ApiEventKey.type: APIEventType.cost.rawValue, ApiEventKey.desc: name]
+ let labelMap: [String: Any] = [ApiEventKey.ts: ts, ApiEventKey.ext: ext]
+ let event = convertToJSONString(eventMap) ?? ""
+ let label = convertToJSONString(labelMap) ?? ""
+ engine.sendCustomReportMessage(messsageId,
+ category: category,
+ event: event,
+ label: label,
+ value: cost)
+ }
+
+ private func configParameters() {
+// engine.setParameters("{\"rtc.qos_for_test_purpose\": true}")
+ engine.setParameters("{\"rtc.direct_send_custom_event\": true}")
+ engine.setParameters("{\"rtc.log_external_input\": true}")
+ }
+
+ private func getCurrentTs() -> Int64 {
+ return Int64(round(Date().timeIntervalSince1970 * 1000.0))
+ }
+
+ private func convertToJSONString(_ dictionary: [String: Any]) -> String? {
+ do {
+ let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: [])
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
+ return jsonString
+ }
+ } catch {
+ writeLog(content: "[APIReporter]convert to json fail: \(error) dictionary: \(dictionary)", level: .warn)
+ }
+ return nil
+ }
+}
diff --git a/KTVAPI/iOS/Classes/KTVApi.swift b/KTVAPI/iOS/Classes/KTVApi.swift
index d1fca98..16395b8 100644
--- a/KTVAPI/iOS/Classes/KTVApi.swift
+++ b/KTVAPI/iOS/Classes/KTVApi.swift
@@ -65,16 +65,13 @@ import AgoraRtcKit
/// 加入合唱失败原因
@objc public enum KTVJoinChorusFailReason: Int {
- case musicPreloadFail //歌曲预加载失败
case musicOpenFail //歌曲打开失败
case joinChannelFail //加入ex频道失败
- case musicPreloadFailAndJoinChannelFail
}
@objc public enum KTVType: Int {
case normal
case singbattle
- case cantata
case singRelay
}
@@ -88,7 +85,7 @@ import AgoraRtcKit
/// - status: <#status description#>
/// - msg: <#msg description#>
/// - lyricUrl: <#lyricUrl description#>
- func onMusicLoadProgress(songCode: Int, percent: Int, status: AgoraMusicContentCenterPreloadStatus, msg: String?, lyricUrl: String?)
+ func onMusicLoadProgress(songCode: Int, percent: Int, state: AgoraMusicContentCenterPreloadState, msg: String?, lyricUrl: String?)
/// 歌曲加载成功
/// - Parameters:
@@ -105,17 +102,6 @@ import AgoraRtcKit
func onMusicLoadFail(songCode: Int, reason: KTVLoadSongFailReason)
}
-
-//public protocol KTVJoinChorusStateListener: NSObjectProtocol {
-//
-// /// 加入合唱成功
-// func onJoinChorusSuccess()
-//
-// /// 加入合唱失败
-// /// - Parameter reason: 失败原因
-// func onJoinChorusFail(reason: KTVJoinChorusFailReason)
-//}
-
@objc public protocol KTVLrcViewDelegate: NSObjectProtocol {
func onUpdatePitch(pitch: Float)
func onUpdateProgress(progress: Int)
@@ -131,7 +117,7 @@ import AgoraRtcKit
/// - error: <#error description#>
/// - isLocal: <#isLocal description#>
func onMusicPlayerStateChanged(state: AgoraMediaPlayerState,
- error: AgoraMediaPlayerError,
+ reason: AgoraMediaPlayerReason,
isLocal: Bool)
@@ -160,6 +146,73 @@ import AgoraRtcKit
func onMusicPlayerProgressChanged(with progress: Int)
}
+// 大合唱中演唱者互相收听对方音频流的选路策略
+enum GiantChorusRouteSelectionType: Int {
+ case random = 0 // 随机选取几条流
+ case byDelay = 1 // 根据延迟选择最低的几条流
+ case topN = 2 // 根据音强选流
+ case byDelayAndTopN = 3 // 同时开始延迟选路和音强选流
+}
+
+// 大合唱中演唱者互相收听对方音频流的选路配置
+@objc public class GiantChorusRouteSelectionConfig: NSObject {
+ let type: GiantChorusRouteSelectionType // 选路策略
+ let streamNum: Int // 最大选取的流个数(推荐6)
+
+ init(type: GiantChorusRouteSelectionType, streamNum: Int) {
+ self.type = type
+ self.streamNum = streamNum
+ }
+}
+
+@objc open class GiantChorusConfiguration: NSObject {
+ var appId: String
+ var rtmToken: String
+ weak var engine: AgoraRtcEngineKit?
+ var channelName: String
+ var localUid: Int = 0
+ var chorusChannelName: String
+ var chorusChannelToken: String
+ var maxCacheSize: Int = 10
+ var musicType: loadMusicType = .mcc
+ var audienceChannelToken: String = ""
+ var musicStreamUid: Int = 0
+ var musicChannelToken: String = ""
+ var routeSelectionConfig: GiantChorusRouteSelectionConfig = GiantChorusRouteSelectionConfig(type: .byDelay, streamNum: 6)
+ var mccDomain: String?
+ @objc public
+ init(appId: String,
+ rtmToken: String,
+ engine: AgoraRtcEngineKit,
+ localUid: Int,
+ audienceChannelName: String,
+ audienceChannelToken: String,
+ chorusChannelName: String,
+ chorusChannelToken: String,
+ musicStreamUid: Int,
+ musicChannelToken: String,
+ maxCacheSize: Int,
+ musicType: loadMusicType,
+ routeSelectionConfig: GiantChorusRouteSelectionConfig,
+ mccDomain: String?
+ ) {
+ self.appId = appId
+ self.rtmToken = rtmToken
+ self.engine = engine
+ self.channelName = audienceChannelName
+ self.localUid = localUid
+ self.chorusChannelName = chorusChannelName
+ self.chorusChannelToken = chorusChannelToken
+ self.maxCacheSize = maxCacheSize
+ self.musicType = musicType
+ self.audienceChannelToken = audienceChannelToken
+ self.musicStreamUid = musicStreamUid
+ self.musicChannelToken = musicChannelToken
+ self.routeSelectionConfig = routeSelectionConfig
+ self.mccDomain = mccDomain
+ }
+}
+
@objc open class KTVApiConfig: NSObject{
var appId: String
var rtmToken: String
@@ -171,7 +224,7 @@ import AgoraRtcKit
var type: KTVType = .normal
var maxCacheSize: Int = 10
var musicType: loadMusicType = .mcc
- var isDebugMode: Bool = false
+ var mccDomain: String?
@objc public
init(appId: String,
rtmToken: String,
@@ -181,9 +234,9 @@ import AgoraRtcKit
chorusChannelName: String,
chorusChannelToken: String,
type: KTVType,
- maxCacheSize: Int,
musicType: loadMusicType,
- isDebugMode: Bool
+ maxCacheSize: Int,
+ mccDomain: String?
) {
self.appId = appId
self.rtmToken = rtmToken
@@ -195,49 +248,49 @@ import AgoraRtcKit
self.type = type
self.maxCacheSize = maxCacheSize
self.musicType = musicType
- self.isDebugMode = isDebugMode
+ self.mccDomain = mccDomain
}
+
+
}
/// 歌曲加载配置信息
@objcMembers open class KTVSongConfiguration: NSObject {
public var songIdentifier: String = ""
- public var autoPlay: Bool = false //是否加载完成自动播放
public var mainSingerUid: Int = 0 //主唱uid
public var mode: KTVLoadMusicMode = .loadMusicAndLrc
-
- func printObjectContent() -> String {
- var content = ""
-
- let mirror = Mirror(reflecting: self)
- for child in mirror.children {
- if let propertyName = child.label {
- if let propertyValue = child.value as? CustomStringConvertible {
- content += "\(propertyName): \(propertyValue)\n"
- } else {
- content += "\(propertyName): \(child.value)\n"
- }
- }
- }
-
- return content
- }
+ public var songCutter: Bool = false
+// func printObjectContent() -> String {
+// var content = ""
+//
+// let mirror = Mirror(reflecting: self)
+// for child in mirror.children {
+// if let propertyName = child.label {
+// if let propertyValue = child.value as? CustomStringConvertible {
+// content += "\(propertyName): \(propertyValue)\n"
+// } else {
+// content += "\(propertyName): \(child.value)\n"
+// }
+// }
+// }
+//
+// return content
+// }
}
public typealias LyricCallback = ((String?) -> Void)
-public typealias LoadMusicCallback = ((AgoraMusicContentCenterPreloadStatus, NSInteger) -> Void)
+public typealias LoadMusicCallback = ((AgoraMusicContentCenterPreloadState, NSInteger) -> Void)
public typealias ISwitchRoleStateListener = (KTVSwitchRoleState, KTVSwitchRoleFailReason) -> Void
-public typealias MusicChartCallBacks = (String, AgoraMusicContentCenterStatusCode, [AgoraMusicChartInfo]?) -> Void
-public typealias MusicResultCallBacks = (String, AgoraMusicContentCenterStatusCode, AgoraMusicCollection) -> Void
+public typealias MusicChartCallBacks = (String, AgoraMusicContentCenterStateReason, [AgoraMusicChartInfo]?) -> Void
+public typealias MusicResultCallBacks = (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void
public typealias JoinExChannelCallBack = ((Bool, KTVJoinChorusFailReason?)-> Void)
@objc public protocol KTVApiDelegate: NSObjectProtocol {
- /// 初始化
- /// - Parameter config: <#config description#>
- init(config: KTVApiConfig)
+ @objc optional func createKtvApi(config: KTVApiConfig) //小合唱必选
+ @objc optional func createKTVGiantChorusApi(config: GiantChorusConfiguration) //大合唱必选
/// 订阅KTVApi事件
/// - Parameter ktvApiEventHandler: <#ktvApiEventHandler description#>
@@ -410,4 +463,6 @@ public typealias JoinExChannelCallBack = ((Bool, KTVJoinChorusFailReason?)-> Voi
*/
func removeMusic(songCode: Int)
+
+ @objc func didAudioMetadataReceived( uid: UInt, metadata: Data)
}
diff --git a/KTVAPI/iOS/Classes/KTVApiImpl.swift b/KTVAPI/iOS/Classes/KTVApiImpl.swift
index f76082f..f302e55 100644
--- a/KTVAPI/iOS/Classes/KTVApiImpl.swift
+++ b/KTVAPI/iOS/Classes/KTVApiImpl.swift
@@ -7,25 +7,21 @@
import Foundation
import AgoraRtcKit
-
+import SwiftProtobuf
/// 加载歌曲状态
-@objc public enum KTVLoadSongState: Int {
+@objc fileprivate enum KTVLoadSongState: Int {
case idle = -1 //空闲
case ok = 0 //成功
case failed //失败
case inProgress //加载中
}
-enum KTVSongMode: Int {
+fileprivate enum KTVSongMode: Int {
case songCode
case songUrl
}
-private func agoraPrint(_ message: String) {
- print(message)
-}
-
-@objc class KTVApiImpl: NSObject{
+@objc class KTVApiImpl: NSObject, KTVApiDelegate{
private var apiConfig: KTVApiConfig?
@@ -55,6 +51,7 @@ private func agoraPrint(_ message: String) {
private var startHighTime: Int = 0
private var isRelease: Bool = false
private var songUrl2: String = ""
+ private var enableMultipathing = true
private var playerState: AgoraMediaPlayerState = .idle {
didSet {
agoraPrint("playerState did changed: \(oldValue.rawValue)->\(playerState.rawValue)")
@@ -83,6 +80,11 @@ private func agoraPrint(_ message: String) {
private var songUrl: String = ""
private var songCode: Int = 0
private var songIdentifier: String = ""
+
+ private let tag = "KTV_API_LOG"
+ private let messageId = "agora:scenarioAPI"
+ private let version = "5.0.0"
+ private let lyricSyncVersion = 2
private var singerRole: KTVSingRole = .audience {
didSet {
@@ -93,24 +95,29 @@ private func agoraPrint(_ message: String) {
private var timer: Timer?
private var isPause: Bool = false
-
+ private var recvFromDataStream = false
public var remoteVolume: Int = 30
private var joinChorusNewRole: KTVSingRole = .audience
private var oldPitch: Double = 0
private var isWearingHeadPhones: Bool = false
private var enableProfessional: Bool = false
private var isPublishAudio: Bool = false
+ private var preludeDuration: Int64 = 0
private lazy var apiDelegateHandler = KTVApiRTCDelegateHandler(with: self)
+
+ private var totalSize: Int = 0
+
+ private var apiRepoter: APIReporter?
+
deinit {
mcc?.register(nil)
agoraPrint("deinit KTVApiImpl")
}
-
- @objc required init(config: KTVApiConfig) {
- super.init()
- agoraPrint("init KTVApiImpl")
+
+ @objc func createKtvApi(config: KTVApiConfig) {
self.apiConfig = config
+ apiRepoter = APIReporter(type: .ktv, version: version, engine: apiConfig?.engine ?? AgoraRtcEngineKit())
setParams()
if config.musicType == .mcc {
@@ -121,11 +128,14 @@ private func agoraPrint(_ message: String) {
contentCenterConfiguration.token = config.rtmToken
contentCenterConfiguration.rtcEngine = config.engine
contentCenterConfiguration.maxCacheSize = UInt(config.maxCacheSize)
- if config.isDebugMode {
- //如果这一块报错为contentCenterConfiguration没有mccDomain这个属性 说明该版本不支持这个 可以注释掉这行代码。完全不影响
- contentCenterConfiguration.mccDomain = "api-test.agora.io"
+ if let domain = config.mccDomain {
+ contentCenterConfiguration.mccDomain = domain
}
mcc = AgoraMusicContentCenter.sharedContentCenter(config: contentCenterConfiguration)
+ if mcc == nil {
+ agoraPrint("mcc create fail")
+// assert(mcc != nil, "mcc == nil")
+ }
mcc?.register(self)
// ------------------ 初始化音乐播放器实例 ------------------
mediaPlayer = mcc?.createMusicPlayer(delegate: self)
@@ -137,8 +147,11 @@ private func agoraPrint(_ message: String) {
mediaPlayer?.adjustPlayoutVolume(50)
mediaPlayer?.adjustPublishSignalVolume(50)
}
+
apiConfig?.engine?.addDelegate(apiDelegateHandler)
+ mediaPlayer?.setPlayerOption("play_pos_change_callback", value: 100)
initTimer()
+ agoraPrint("init KTVApiImpl")
}
private func setParams() {
@@ -153,13 +166,24 @@ private func agoraPrint(_ message: String) {
engine.setParameters("{\"che.audio.neteq.prebuffer_max_delay\": 600}")
engine.setParameters("{\"che.audio.max_mixed_participants\": 8}")
engine.setParameters("{\"che.audio.custom_bitrate\": 48000}")
- engine.setParameters("{\"che.audio.direct.uplink_process\": false}")
engine.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
engine.setParameters("{\"che.audio.neteq.targetlevel_offset\": 20}")
engine.setParameters("{\"che.audio.ans.noise_gate\": 20}")
+ engine.setParameters("{\"rtc.use_audio4\": true}")
if apiConfig?.type == .singRelay {
engine.setParameters("{\"che.audio.aiaec.working_mode\": 1}")
}
+
+ //4.3.0 add
+ enableMultipathing = true
+// engine.setParameters("{\"rtc.enable_tds_request_on_join\": true}")
+// engine.setParameters("{\"rtc.remote_path_scheduling_strategy\": 0}")
+ engine.setParameters("{\"rtc.path_scheduling_strategy\": 0}")
+ // engine.setParameters("{\"rtc.enableMultipath\": true}")
+ engine.setParameters("{\"rtc.log_external_input\":true}")
+ // 数据上报
+ engine.setParameters("{\"rtc.direct_send_custom_event\": true}")
+ // engine.setParameters("{\"rtc.qos_for_test_purpose\": true}")
}
func renewInnerDataStreamId() {
@@ -167,19 +191,58 @@ private func agoraPrint(_ message: String) {
dataStreamConfig.ordered = false
dataStreamConfig.syncWithAudio = true
self.apiConfig?.engine?.createDataStream(&dataStreamId, config: dataStreamConfig)
- sendCustomMessage(with: "renewInnerDataStreamId", label: "")
+ sendCustomMessage(with: "renewInnerDataStreamId", dict: [:])
+ agoraPrint("renewInnerDataStreamId")
}
}
//MARK: KTVApiDelegate
-extension KTVApiImpl: KTVApiDelegate {
+extension KTVApiImpl {
+
+ func objectContent(of object: Any) -> [String: Any] {
+ var content = [String: Any]()
+
+ let mirror = Mirror(reflecting: object)
+ for child in mirror.children {
+ if let propertyName = child.label {
+ if let convertibleValue = convertToJSONSerializable(child.value) {
+ content[propertyName] = convertibleValue
+ }
+ }
+ }
+
+ return content
+ }
+
+ func convertToJSONSerializable(_ value: Any) -> Any? {
+ switch value {
+ case let value as String:
+ return value
+ case let value as Int:
+ return value
+ case let value as Double:
+ return value
+ case let value as Bool:
+ return value
+ case let value as Int?:
+ return value
+ case let value as Double?:
+ return value
+ case let value as Bool?:
+ return value
+ case let value as String?:
+ return value
+ default:
+ return nil
+ }
+ }
func getMusicContentCenter() -> AgoraMusicContentCenter? {
return mcc
}
func setLrcView(view: KTVLrcViewDelegate) {
- sendCustomMessage(with: "renewInnerDataStreamId", label: "view:\(view.description)")
+ sendCustomMessage(with: "setLrcView", dict: [:])
lrcControl = view
}
@@ -192,15 +255,14 @@ extension KTVApiImpl: KTVApiDelegate {
self.songUrl = url1
self.songUrl2 = url2
- if config.autoPlay {
- // 主唱自动播放歌曲
- if self.singerRole != .leadSinger {
- switchSingerRole(newRole: .soloSinger) { state, failRes in
-
- }
- }
- startSing(url: url1, startPos: 0)
- }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// switchSingerRole(newRole: .soloSinger) { state, failRes in
+// }
+// }
+// startSing(url: url1, startPos: 0)
+ // }
}
//主要针对本地歌曲播放的主唱伴奏切换的 MCC直接忽视这个方法
@@ -218,8 +280,8 @@ extension KTVApiImpl: KTVApiDelegate {
}
func loadMusic(songCode: Int, config: KTVSongConfiguration, onMusicLoadStateListener: IMusicLoadStateListener) {
- sendCustomMessage(with: "loadMusic", label: "config:\(config.printObjectContent())")
- agoraPrint("loadMusic songCode:\(songCode) ")
+ sendCustomMessage(with: "loadMusic", dict: objectContent(of: config))
+ agoraPrint("loadMusic songCode:\(songCode) mode:\(config.mode.rawValue)")
self.songMode = .songCode
self.songCode = songCode
self.songIdentifier = config.songIdentifier
@@ -227,28 +289,27 @@ extension KTVApiImpl: KTVApiDelegate {
}
func loadMusic(config: KTVSongConfiguration, url: String) {
- sendCustomMessage(with: "loadMusic", label: "config:\(config.printObjectContent()), url:\(url)")
+ sendCustomMessage(with: "loadMusicWithUrl:\(url)", dict: objectContent(of: config))
self.songMode = .songUrl
self.songUrl = url
self.songIdentifier = config.songIdentifier
- if config.autoPlay {
- // 主唱自动播放歌曲
- if singerRole != .leadSinger {
- switchSingerRole(newRole: .soloSinger) { _, _ in
-
- }
- }
- startSing(url: url, startPos: 0)
- }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if singerRole != .leadSinger {
+// switchSingerRole(newRole: .soloSinger) { _, _ in
+// }
+// }
+// startSing(url: url, startPos: 0)
+// }
}
func getMusicPlayer() -> AgoraRtcMediaPlayerProtocol? {
- sendCustomMessage(with: "getMusicPlayer", label: "")
+ sendCustomMessage(with: "getMusicPlayer", dict: [:])
return mediaPlayer
}
func addEventHandler(ktvApiEventHandler: KTVApiEventHandlerDelegate) {
- sendCustomMessage(with: "addEventHandler", label: "")
+ sendCustomMessage(with: "addEventHandler", dict: [:])
if eventHandlers.contains(ktvApiEventHandler) {
return
}
@@ -256,12 +317,12 @@ extension KTVApiImpl: KTVApiDelegate {
}
func removeEventHandler(ktvApiEventHandler: KTVApiEventHandlerDelegate) {
- sendCustomMessage(with: "removeEventHandler", label: "")
+ sendCustomMessage(with: "removeEventHandler", dict: [:])
eventHandlers.remove(ktvApiEventHandler)
}
func cleanCache() {
- sendCustomMessage(with: "cleanCache", label: "")
+ sendCustomMessage(with: "cleanCache", dict: [:])
isRelease = true
freeTimer()
agoraPrint("cleanCache")
@@ -282,7 +343,12 @@ extension KTVApiImpl: KTVApiDelegate {
}
func renewToken(rtmToken: String, chorusChannelRtcToken: String) {
- sendCustomMessage(with: "renewToken", label: "rtmToken:\(rtmToken), chorusChannelRtcToken:\(chorusChannelRtcToken)")
+
+ let dict: [String: Any] = [
+ "rtmToken":rtmToken,
+ "chorusChannelRtcToken":chorusChannelRtcToken
+ ]
+ sendCustomMessage(with: "renewToken", dict: dict)
// 更新RtmToken
mcc?.renewToken(rtmToken)
// 更新合唱频道RtcToken
@@ -294,7 +360,7 @@ extension KTVApiImpl: KTVApiDelegate {
}
func fetchMusicCharts(completion: @escaping MusicChartCallBacks) {
- sendCustomMessage(with: "fetchMusicCharts", label: "")
+ sendCustomMessage(with: "fetchMusicCharts", dict: [:])
agoraPrint("fetchMusicCharts")
let requestId = mcc!.getMusicCharts()
musicChartDict[requestId] = completion
@@ -304,9 +370,15 @@ extension KTVApiImpl: KTVApiDelegate {
page: Int,
pageSize: Int,
jsonOption: String,
- completion:@escaping (String, AgoraMusicContentCenterStatusCode, AgoraMusicCollection) -> Void) {
+ completion:@escaping (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void) {
agoraPrint("searchMusic with musicChartId: \(musicChartId)")
- sendCustomMessage(with: "searchMusic", label: "musicChartId:\(musicChartId), page:\(page), pageSize:\(pageSize), jsonOption:\(jsonOption)")
+ let dict: [String: Any] = [
+ "musicChartId":musicChartId,
+ "page": page,
+ "pageSize": pageSize,
+ "jsonOption": jsonOption
+ ]
+ sendCustomMessage(with: "searchMusic", dict: dict)
let requestId = mcc!.getMusicCollection(musicChartId: musicChartId, page: page, pageSize: pageSize, jsonOption: jsonOption)
musicSearchDict[requestId] = completion
}
@@ -315,25 +387,38 @@ extension KTVApiImpl: KTVApiDelegate {
page: Int,
pageSize: Int,
jsonOption: String,
- completion: @escaping (String, AgoraMusicContentCenterStatusCode, AgoraMusicCollection) -> Void) {
+ completion: @escaping (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void) {
agoraPrint("searchMusic with keyword: \(keyword)")
- sendCustomMessage(with: "searchMusic", label: "keyword:\(keyword), page:\(page), pageSize:\(pageSize), jsonOption:\(jsonOption)")
+ let dict: [String: Any] = [
+ "keyword": keyword,
+ "page": page,
+ "pageSize": pageSize,
+ "jsonOption": jsonOption
+ ]
+ sendCustomMessage(with: "searchMusic", dict: dict)
let requestId = mcc!.searchMusic(keyWord: keyword, page: page, pageSize: pageSize, jsonOption: jsonOption)
musicSearchDict[requestId] = completion
}
func switchSingerRole(newRole: KTVSingRole, onSwitchRoleState: @escaping (KTVSwitchRoleState, KTVSwitchRoleFailReason) -> Void) {
let oldRole = singerRole
- sendCustomMessage(with: "switchSingerRole", label: "oldRole:\(oldRole.rawValue), newRole: \(newRole.rawValue)")
+
+ let dict: [String: Any] = [
+ "oldRole": oldRole.rawValue,
+ "newRole": newRole.rawValue
+ ]
+ sendCustomMessage(with: "switchSingerRole", dict: dict)
agoraPrint("switchSingerRole oldRole:\(oldRole.rawValue), newRole: \(newRole.rawValue)")
- if ((oldRole == .leadSinger || oldRole == .soloSinger) && (newRole == .coSinger || newRole == .audience) && isNowMicMuted) {
- apiConfig?.engine?.muteLocalAudioStream(true)
- apiConfig?.engine?.adjustRecordingSignalVolume(100)
- } else if ((oldRole == .audience || oldRole == .coSinger) && (newRole == .leadSinger || newRole == .soloSinger) && isNowMicMuted) {
- apiConfig?.engine?.adjustRecordingSignalVolume(0)
- apiConfig?.engine?.muteLocalAudioStream(false)
- }
+// if (apiConfig?.type != .singRelay) {
+// if ((oldRole == .leadSinger || oldRole == .soloSinger) && (newRole == .coSinger || newRole == .audience) && isNowMicMuted) {
+// apiConfig?.engine?.muteLocalAudioStream(true)
+// apiConfig?.engine?.adjustRecordingSignalVolume(100)
+// } else if ((oldRole == .audience || oldRole == .coSinger) && (newRole == .leadSinger || newRole == .soloSinger) && isNowMicMuted) {
+// apiConfig?.engine?.adjustRecordingSignalVolume(0)
+// apiConfig?.engine?.muteLocalAudioStream(false)
+// }
+// }
self.switchSingerRole(oldRole: oldRole, newRole: newRole, token: apiConfig?.chorusChannelToken ?? "", stateCallBack: onSwitchRoleState)
}
@@ -342,7 +427,7 @@ extension KTVApiImpl: KTVApiDelegate {
* 恢复播放
*/
@objc public func resumeSing() {
- sendCustomMessage(with: "resumeSing", label: "")
+ sendCustomMessage(with: "resumeSing", dict: [:])
agoraPrint("resumeSing")
if mediaPlayer?.getPlayerState() == .paused {
mediaPlayer?.resume()
@@ -356,7 +441,7 @@ extension KTVApiImpl: KTVApiDelegate {
* 暂停播放
*/
@objc public func pauseSing() {
- sendCustomMessage(with: "pauseSing", label: "")
+ sendCustomMessage(with: "pauseSing", dict: [:])
agoraPrint("pauseSing")
mediaPlayer?.pause()
}
@@ -365,7 +450,7 @@ extension KTVApiImpl: KTVApiDelegate {
* 调整进度
*/
@objc public func seekSing(time: NSInteger) {
- sendCustomMessage(with: "seekSing", label: "")
+ sendCustomMessage(with: "seekSing", dict: ["time":time])
agoraPrint("seekSing")
mediaPlayer?.seek(toPosition: time)
}
@@ -381,25 +466,60 @@ extension KTVApiImpl: KTVApiDelegate {
* 设置当前mic开关状态
*/
@objc public func muteMic(muteStatus: Bool) {
- sendCustomMessage(with: "setMicStatus", label: "\(muteStatus)")
+ sendCustomMessage(with: "setMicStatus", dict: ["muteStatus":muteStatus])
self.isNowMicMuted = muteStatus
- if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
- apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
+ if (apiConfig?.type != .singRelay) {
+ if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
+ apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
+ } else {
+// let channelMediaOptions = AgoraRtcChannelMediaOptions()
+// channelMediaOptions.publishMicrophoneTrack = !muteStatus
+// channelMediaOptions.clientRoleType = .broadcaster
+// apiConfig?.engine?.updateChannel(with: channelMediaOptions)
+// apiConfig?.engine?.muteLocalAudioStream(muteStatus)
+
+ apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
+ }
} else {
- apiConfig?.engine?.muteLocalAudioStream(muteStatus)
+ apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
}
}
@objc public func removeMusic(songCode: Int) {
- sendCustomMessage(with: "removeMusic", label: "songCode:\(songCode)")
+ sendCustomMessage(with: "removeMusic", dict: ["songCode": songCode])
let ret: Int = mcc?.removeCache(songCode: songCode) ?? 0
if ret < 0 {
agoraPrint("removeMusic failed: ret:\(ret)")
}
}
+
+ @objc public func enableMutipath(enable: Bool) {
+ sendCustomMessage(with: "enableMutipath", dict: ["enable":enable])
+ agoraPrint("enableMutipath:\(enable)")
+ enableMultipathing = enable
+ if singerRole == .coSinger || singerRole == .leadSinger {
+ if let subChorusConnection = subChorusConnection {
+ apiConfig?.engine?.setParametersEx("{\"rtc.enableMultipath\": \(enable), \"rtc.path_scheduling_strategy\": 0, \"rtc.remote_path_scheduling_strategy\": 0}", connection: subChorusConnection)
+ }
+ }
+ }
+ private func agoraPrint(_ message: String) {
+ #if DEBUG
+ print("[KTVAPI]\(message)")
+ #endif
+ apiRepoter?.writeLog(content: "[KTVAPI]\(message)", level: .info)
+ }
+
+ private func agoraPrintError(_ message: String) {
+ #if DEBUG
+ print("[KTVAPI][Error]\(message)")
+ #endif
+ apiRepoter?.writeLog(content: "[KTVAPI][Error]\(message)", level: .error)
+ }
}
+
// 主要是角色切换,加入合唱,加入多频道,退出合唱,退出多频道
extension KTVApiImpl {
private func switchSingerRole(oldRole: KTVSingRole, newRole: KTVSingRole, token: String, stateCallBack:@escaping ISwitchRoleStateListener) {
@@ -606,10 +726,10 @@ extension KTVApiImpl {
let rtcConnection = AgoraRtcConnection()
rtcConnection.channelId = apiConfig?.chorusChannelName ?? ""
rtcConnection.localUid = UInt(apiConfig?.localUid ?? 0)
- subChorusConnection = rtcConnection
+ subChorusConnection = rtcConnection
joinChorusNewRole = role
- let ret = apiConfig?.engine?.joinChannelEx(byToken: token, connection: rtcConnection, delegate: self, mediaOptions: mediaOption, joinSuccess: nil)
+ let ret = apiConfig?.engine?.joinChannelEx(byToken: token, connection: rtcConnection, delegate: self, mediaOptions: mediaOption, joinSuccess: nil)
agoraPrint("joinChannelEx ret: \(ret ?? -999)")
if newRole == .coSinger {
let uid = UInt(songConfig?.mainSingerUid ?? 0)
@@ -617,6 +737,10 @@ extension KTVApiImpl {
apiConfig?.engine?.muteRemoteAudioStream(uid, mute: true)
agoraPrint("muteRemoteAudioStream: \(uid), ret: \(ret ?? -1)")
}
+ if enableMultipathing {
+ apiConfig?.engine?.setParametersEx("{\"rtc.path_scheduling_strategy\":0, \"rtc.enableMultipath\": true, \"rtc.remote_path_scheduling_strategy\": 0}", connection: rtcConnection)
+ }
+ apiConfig?.engine?.setParameters("{\"rtc.use_audio4\": true}")
}
private func leaveChorus2ndChannel(_ role: KTVSingRole) {
@@ -666,7 +790,6 @@ extension KTVApiImpl {
}
private func _loadMusic(config: KTVSongConfiguration, mode: KTVLoadMusicMode, onMusicLoadStateListener: IMusicLoadStateListener){
-
songConfig = config
lastReceivedPosition = 0
localPosition = 0
@@ -676,6 +799,7 @@ extension KTVApiImpl {
}
if (config.mode == .loadNone) {
+ agoraPrint("load music none")
return
}
@@ -696,23 +820,23 @@ extension KTVApiImpl {
onMusicLoadStateListener.onMusicLoadFail(songCode: self.songCode, reason: .noLyricUrl)
}
- if (config.autoPlay) {
- // 主唱自动播放歌曲
- if self.singerRole != .leadSinger {
- self.switchSingerRole(newRole: .soloSinger) { _, _ in
-
- }
- }
- self.startSing(songCode: self.songCode, startPos: 0)
- }
+// if (config.autoPlay) {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
}
} else {
loadMusicListeners.setObject(onMusicLoadStateListener, forKey: "\(self.songCode)" as NSString)
- onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, status: .preloading, msg: "", lyricUrl: "")
+ // onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, status: .preloading, msg: "", lyricUrl: "")
// TODO: 只有未缓存时才显示进度条
if mcc?.isPreloaded(songCode: songCode) != 0 {
- onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, status: .preloading, msg: "", lyricUrl: "")
+ onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, state: .preloading, msg: "", lyricUrl: "")
}
+
preloadMusic(with: songCode) { [weak self] status, songCode in
guard let self = self else { return }
if self.songCode != songCode {
@@ -723,7 +847,6 @@ extension KTVApiImpl {
if mode == .loadMusicAndLrc {
// 需要加载歌词
self.loadLyric(with: songCode) { url in
- agoraPrint("loadMusicAndLrc: songCode:\(songCode) status:\(status.rawValue) ulr:\(String(describing: url))")
if self.songCode != songCode {
onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .cancled)
return
@@ -732,35 +855,35 @@ extension KTVApiImpl {
self.lyricUrlMap[String(songCode)] = urlPath
self.setLyric(with: urlPath) { lyricUrl in
onMusicLoadStateListener.onMusicLoadSuccess(songCode: songCode, lyricUrl: urlPath)
+ self.agoraPrint("loadMusicAndLrc: songCode:\(songCode) status:\(status.rawValue) ulr:\(String(describing: url))")
}
} else {
onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .noLyricUrl)
+ self.agoraPrint("loadMusicAndLrc: songCode:\(songCode) status:\(status.rawValue) ulr:\(String(describing: url))")
}
- if config.autoPlay {
- // 主唱自动播放歌曲
- if self.singerRole != .leadSinger {
- self.switchSingerRole(newRole: .soloSinger) { _, _ in
-
- }
- }
- self.startSing(songCode: self.songCode, startPos: 0)
- }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
}
} else if mode == .loadMusicOnly {
agoraPrint("loadMusicOnly: songCode:\(songCode) load success")
- if config.autoPlay {
- // 主唱自动播放歌曲
- if self.singerRole != .leadSinger {
- self.switchSingerRole(newRole: .soloSinger) { _, _ in
-
- }
- }
- self.startSing(songCode: self.songCode, startPos: 0)
- }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
onMusicLoadStateListener.onMusicLoadSuccess(songCode: songCode, lyricUrl: "")
}
} else {
- agoraPrint("load music failed songCode:\(songCode)")
+ agoraPrintError("load music failed songCode:\(songCode)")
onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .musicPreloadFail)
}
}
@@ -769,18 +892,28 @@ extension KTVApiImpl {
private func loadLyric(with songCode: NSInteger, callBack:@escaping LyricCallback) {
agoraPrint("loadLyric songCode: \(songCode)")
- let requestId: String = self.mcc?.getLyric(songCode: songCode, lyricType: 0) ?? ""
+ guard let mcc = self.mcc else {
+ agoraPrint("loadLyric songCode: \(songCode) fail")
+ callBack(nil)
+ return
+ }
+ let requestId: String = mcc.getLyric(songCode: songCode, lyricType: 0)
self.lyricCallbacks.updateValue(callBack, forKey: requestId)
}
private func preloadMusic(with songCode: Int, callback: @escaping LoadMusicCallback) {
agoraPrint("preloadMusic songCode: \(songCode)")
- if self.mcc?.isPreloaded(songCode: songCode) == 0 {
+ guard let mcc = self.mcc else {
+ agoraPrint("preloadMusic songCode: \(songCode) fail")
+ callback(.error, songCode)
+ return
+ }
+ if mcc.isPreloaded(songCode: songCode) == 0 {
musicCallbacks.removeValue(forKey: String(songCode))
callback(.OK, songCode)
return
}
- let err = self.mcc?.preload(songCode: songCode, jsonOption: nil)
+ let err = mcc.preload(songCode: songCode, jsonOption: nil)
if err != 0 {
musicCallbacks.removeValue(forKey: String(songCode))
callback(.error, songCode)
@@ -796,11 +929,15 @@ extension KTVApiImpl {
}
func startSing(songCode: Int, startPos: Int) {
- sendCustomMessage(with: "startSing", label: "songCode:\(songCode), startPos: \(startPos)")
+ let dict: [String: Any] = [
+ "songCode": songCode,
+ "startPos": startPos
+ ]
+ sendCustomMessage(with: "startSing", dict: dict)
let role = singerRole
agoraPrint("startSing role: \(role.rawValue)")
if self.songCode != songCode {
- agoraPrint("startSing failed: canceled")
+ agoraPrintError("startSing failed: canceled")
return
}
@@ -809,20 +946,24 @@ extension KTVApiImpl {
}
apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
let ret = (mediaPlayer as? AgoraMusicPlayerProtocol)?.openMedia(songCode: songCode, startPos: startPos)
- agoraPrint("startSing->openMedia(\(songCode) fail: \(ret ?? -1)")
+ agoraPrintError("startSing->openMedia(\(songCode) fail: \(ret ?? -1)")
}
func startSing(url: String, startPos: Int) {
- sendCustomMessage(with: "startSing", label: "url:\(url), startPos: \(startPos)")
+ let dict: [String: Any] = [
+ "url": url,
+ "startPos": startPos
+ ]
+ sendCustomMessage(with: "startSing", dict: dict)
let role = singerRole
agoraPrint("startSing role: \(role.rawValue)")
if self.songUrl != songUrl {
- agoraPrint("startSing failed: canceled")
+ agoraPrintError("startSing failed: canceled")
return
}
apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
let ret = mediaPlayer?.open(url, startPos: 0)
- agoraPrint("startSing->openMedia(\(url) fail: \(ret ?? -1)")
+ agoraPrintError("startSing->openMedia(\(url) fail: \(ret ?? -1)")
}
/**
@@ -830,7 +971,7 @@ extension KTVApiImpl {
*/
@objc public func stopSing() {
agoraPrint("stopSing")
- sendCustomMessage(with: "stopSing", label: "")
+ sendCustomMessage(with: "stopSing", dict: [:])
let mediaOption = AgoraRtcChannelMediaOptions()
mediaOption.publishMediaPlayerAudioTrack = false
apiConfig?.engine?.updateChannel(with: mediaOption)
@@ -850,6 +991,7 @@ extension KTVApiImpl {
@objc func enableProfessionalStreamerMode(_ enable: Bool) {
if self.isPublishAudio == false {return}
+ agoraPrint("enableProfessionalStreamerMode enable:\(enable)")
self.enableProfessional = enable
//专业非专业还需要根据是否佩戴耳机来判断是否开启3A
apiConfig?.engine?.setAudioProfile(enable ? .musicHighQualityStereo : .musicStandardStereo)
@@ -868,6 +1010,7 @@ extension KTVApiImpl {
}
}
+
}
// rtc的子频道代理回调
@@ -888,9 +1031,8 @@ extension KTVApiImpl: AgoraRtcEngineDelegate {
}
public func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
- agoraPrint("didOccurError: \(errorCode.rawValue)")
+ agoraPrintError("didOccurError: \(errorCode.rawValue)")
if errorCode != .joinChannelRejected {return}
- agoraPrint("join ex channel failed")
engine.setAudioScenario(.gameStreaming)
if joinChorusNewRole == .leadSinger {
mainSingerHasJoinChannelEx = false
@@ -928,27 +1070,17 @@ extension KTVApiImpl {
let ntpTime = dict["ntp"] as? Int,
let songId = dict["songIdentifier"] as? String
else { return }
- agoraPrint("realTime:\(realPosition) position:\(position) lastNtpTime:\(lastNtpTime) ntpTime:\(ntpTime) ntpGap:\(ntpTime - self.lastNtpTime) ")
- //如果接收到的歌曲和自己本地的歌曲不一致就不更新进度
-// guard songCode == self.songCode else {
-// agoraPrint("local songCode[\(songCode)] is not equal to recv songCode[\(self.songCode)] role: \(singerRole.rawValue)")
-// return
-// }
self.lastNtpTime = ntpTime
self.remotePlayerDuration = TimeInterval(duration)
let state = AgoraMediaPlayerState(rawValue: mainSingerState) ?? .stopped
-// self.lastMainSingerUpdateTime = Date().milListamp
-// self.remotePlayerPosition = TimeInterval(realPosition)
if self.playerState != state {
agoraPrint("[setLrcTime] recv state: \(self.playerState.rawValue)->\(state.rawValue) role: \(singerRole.rawValue) role: \(singerRole.rawValue)")
if state == .playing, singerRole == .coSinger, playerState == .openCompleted {
//如果是伴唱等待主唱开始播放,seek 到指定位置开始播放保证歌词显示位置准确
self.localPlayerPosition = self.lastMainSingerUpdateTime - Double(position)
- print("localPlayerPosition:playerKit:handleSetLrcTimeCommand \(localPlayerPosition)")
- agoraPrint("seek toPosition: \(position)")
mediaPlayer?.seek(toPosition: Int(position))
}
@@ -960,28 +1092,25 @@ extension KTVApiImpl {
self.remotePlayerPosition = TimeInterval(realPosition)
handleCoSingerRole(dict: dict)
} else if role == .audience {
- if self.songIdentifier == songId {
- self.lastMainSingerUpdateTime = Date().milListamp
- self.remotePlayerPosition = TimeInterval(realPosition)
+ if dict.keys.contains("ver") {
+ recvFromDataStream = false
} else {
- self.lastMainSingerUpdateTime = 0
- self.remotePlayerPosition = 0
+ recvFromDataStream = true
+ if self.songIdentifier == songId {
+ self.lastMainSingerUpdateTime = Date().milListamp
+ self.remotePlayerPosition = TimeInterval(realPosition)
+ } else {
+ self.lastMainSingerUpdateTime = 0
+ self.remotePlayerPosition = 0
+ }
+ handleAudienceRole(dict: dict)
}
- handleAudienceRole(dict: dict)
}
}
private func handlePlayerStateCommand(dict: [String: Any], role: KTVSingRole) {
let mainSingerState: Int = dict["state"] as? Int ?? 0
let state = AgoraMediaPlayerState(rawValue: mainSingerState) ?? .idle
-//
-// if state == .playing, singerRole == .coSinger, playerState == .openCompleted {
-// //如果是伴唱等待主唱开始播放,seek 到指定位置开始播放保证歌词显示位置准确
-// self.localPlayerPosition = getPlayerCurrentTime()
-// print("localPlayerPosition:playerKit:handlePlayerStateCommand \(localPlayerPosition)")
-// agoraPrint("seek toPosition: \(self.localPlayerPosition)")
-// mediaPlayer?.seek(toPosition: Int(self.localPlayerPosition))
-// }
agoraPrint("recv state with MainSinger: \(state.rawValue)")
syncPlayStateFromRemote(state: state, needDisplay: true)
@@ -1010,9 +1139,9 @@ extension KTVApiImpl {
let threshold = expectPosition - Int(localPosition)
let ntpTime = dict["ntp"] as? Int ?? 0
let time = dict["time"] as? Int64 ?? 0
- agoraPrint("checkNtp, diff:\(threshold), localNtp:\(getNtpTimeInMs()), localPosition:\(localPosition), audioPlayoutDelay:\(audioPlayoutDelay), remoteDiff:\(String(describing: ntpTime - Int(time)))")
+ // agoraPrint("checkNtp, diff:\(threshold), localNtp:\(getNtpTimeInMs()), localPosition:\(localPosition), audioPlayoutDelay:\(audioPlayoutDelay), remoteDiff:\(String(describing: ntpTime - Int(time)))")
if abs(threshold) > 50 {
- print("expectPosition:\(expectPosition)")
+ agoraPrint("expectPosition:\(expectPosition)")
mediaPlayer?.seek(toPosition: expectPosition)
}
}
@@ -1026,7 +1155,7 @@ extension KTVApiImpl {
let mainSingerUid = dict["uid"] as? Int ?? 0
songConfig?.mainSingerUid = mainSingerUid
let ret = apiConfig?.engine?.muteRemoteAudioStream(UInt(mainSingerUid), mute: true)
- print("ret:\(ret)")
+ agoraPrint("handleCosingerToLeadSinger:ret:\(String(describing: ret))")
}
}
}
@@ -1073,7 +1202,31 @@ extension KTVApiImpl {
if self.singerRole != .audience {
current = Date().milListamp - self.lastReceivedPosition + Double(self.localPosition)
}
- self.setProgress(with: Int(current) + Int(self.startHighTime))
+
+ if self.singerRole == .audience && !recvFromDataStream {
+
+ } else {
+ var curTime:Int64 = Int64(current) + Int64(self.startHighTime)
+ if songConfig?.songCutter == true {
+ curTime = curTime - preludeDuration > 0 ? curTime - preludeDuration : curTime
+ }
+ if self.singerRole != .audience {
+ current = Date().milListamp - self.lastReceivedPosition + Double(self.localPosition)
+
+ if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
+ var time: LrcTime = LrcTime()
+ time.forward = true
+ time.ts = curTime
+ time.songID = songIdentifier
+ time.type = .lrcTime
+ //大合唱的uid是musicuid
+ time.uid = Int32(apiConfig?.localUid ?? 0)
+ sendMetaMsg(with: time)
+ }
+ }
+ self.setProgress(with: Int(curTime))
+ }
+
self.oldPitch = self.pitch
})
}
@@ -1163,13 +1316,13 @@ extension KTVApiImpl {
resumeSing()
} else if (state == .playBackAllLoopsCompleted && needDisplay == true) {
getEventHander { delegate in
- delegate.onMusicPlayerStateChanged(state: state, error: .none, isLocal: true)
+ delegate.onMusicPlayerStateChanged(state: state, reason: .none, isLocal: true)
}
}
} else {
self.playerState = state
getEventHander { delegate in
- delegate.onMusicPlayerStateChanged(state: self.playerState, error: .none, isLocal: false)
+ delegate.onMusicPlayerStateChanged(state: self.playerState, reason: .none, isLocal: false)
}
}
}
@@ -1208,22 +1361,25 @@ extension KTVApiImpl {
return localNtpTime
}
- private func syncPlayState(state: AgoraMediaPlayerState, error: AgoraMediaPlayerError) {
- let dict: [String: Any] = ["cmd": "PlayerState", "userId": apiConfig?.localUid as Any, "state": state.rawValue, "error": "\(error.rawValue)"]
+ private func syncPlayState(state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
+ let dict: [String: Any] = ["cmd": "PlayerState", "userId": apiConfig?.localUid as Any, "state": state.rawValue, "error": "\(reason.rawValue)"]
sendStreamMessageWithDict(dict, success: nil)
}
- private func sendCustomMessage(with event: String, label: String) {
- apiConfig?.engine?.sendCustomReportMessage("scenarioAPI", category: "1_ios_4.0.0", event: event, label: label, value: 0)
+ private func sendCustomMessage(with event: String, dict: [String: Any]) {
+ apiRepoter?.reportFuncEvent(name: event, value: dict, ext: [:])
}
private func sendStreamMessageWithDict(_ dict: [String: Any], success: ((_ success: Bool) -> Void)?) {
let messageData = compactDictionaryToData(dict as [String: Any])
+ let sizeInBits = (messageData ?? Data()).count * 8
+ totalSize += sizeInBits
let code = apiConfig?.engine?.sendStreamMessage(dataStreamId, data: messageData ?? Data())
if code == 0 && success != nil { success!(true) }
if code != 0 {
agoraPrint("sendStreamMessage fail: \(String(describing: code))")
}
+// print("totalSize:\(totalSize)")
}
private func syncPlayState(_ state: AgoraMediaPlayerState) {
@@ -1235,6 +1391,14 @@ extension KTVApiImpl {
lrcControl?.onUpdatePitch(pitch: Float(self.pitch))
lrcControl?.onUpdateProgress(progress: pos > 200 ? pos - 200 : pos)
}
+
+ private func sendMetaMsg(with time: LrcTime) {
+ let data: Data? = try? time.serializedData()
+ let code = apiConfig?.engine?.sendAudioMetadata(data ?? Data())
+ if code != 0 {
+ agoraPrintError("sendStreamMessage fail: \(String(describing: code))")
+ }
+ }
}
//主要是MPK的回调
@@ -1253,11 +1417,10 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
"realTime":position_ms,
"ntp": timestamp_ms,
"playerState": self.playerState.rawValue,
- "songIdentifier": songIdentifier
- // "songCode": self.songCode
+ "songIdentifier": songIdentifier,
+ "ver":2,
]
- agoraPrint("position_ms:\(position_ms), ntp:\(getNtpTimeInMs()), delta:\(self.getNtpTimeInMs() - position_ms), autoPlayoutDelay:\(self.audioPlayoutDelay)")
- print("autoPlayoutDelay:\(self.audioPlayoutDelay)")
+ // agoraPrint("position_ms:\(position_ms), ntp:\(getNtpTimeInMs()), delta:\(self.getNtpTimeInMs() - position_ms), autoPlayoutDelay:\(self.audioPlayoutDelay)")
sendStreamMessageWithDict(dict) { _ in
@@ -1275,12 +1438,12 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
}
- func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol, didChangedTo state: AgoraMediaPlayerState, error: AgoraMediaPlayerError) {
+ func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol, didChangedTo state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
agoraPrint("agoraRtcMediaPlayer didChangedToState: \(state.rawValue) \(self.songCode)")
if isRelease {return}
if state == .openCompleted {
self.localPlayerPosition = Date().milListamp
- print("localPlayerPosition:playerKit:openCompleted \(localPlayerPosition)")
+ agoraPrint("localPlayerPosition:playerKit:openCompleted \(localPlayerPosition)")
self.playerDuration = TimeInterval(mediaPlayer?.getDuration() ?? 0)
if isMainSinger() { //主唱播放,通过同步消息“setLrcTime”通知伴唱play
playerKit.play()
@@ -1298,11 +1461,11 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
} else if state == .playing {
apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
self.localPlayerPosition = Date().milListamp - Double(mediaPlayer?.getPosition() ?? 0)
- print("localPlayerPosition:playerKit:playing \(localPlayerPosition)")
+ agoraPrint("localPlayerPosition:playerKit:playing \(localPlayerPosition)")
}
if isMainSinger() {
- syncPlayState(state: state, error: error)
+ syncPlayState(state: state, reason: reason)
}
self.playerState = state
agoraPrint("recv state with player callback : \(state.rawValue)")
@@ -1310,10 +1473,10 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
return
}
getEventHander { delegate in
- delegate.onMusicPlayerStateChanged(state: state, error: .none, isLocal: true)
+ delegate.onMusicPlayerStateChanged(state: state, reason: .none, isLocal: true)
}
}
-
+
private func isMainSinger() -> Bool {
return singerRole == .soloSinger || singerRole == .leadSinger
}
@@ -1322,7 +1485,7 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
//主要是MCC的回调
extension KTVApiImpl: AgoraMusicContentCenterEventDelegate {
- func onSongSimpleInfoResult(_ requestId: String, songCode: Int, simpleInfo: String?, errorCode: AgoraMusicContentCenterStatusCode) {
+ func onSongSimpleInfoResult(_ requestId: String, songCode: Int, simpleInfo: String?, reason: AgoraMusicContentCenterStateReason) {
if let jsonData = simpleInfo?.data(using: .utf8) {
do {
let jsonMsg = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any]
@@ -1330,74 +1493,80 @@ extension KTVApiImpl: AgoraMusicContentCenterEventDelegate {
let highPart = format["highPart"] as! [[String: Any]]
let highStartTime = highPart[0]["highStartTime"] as! Int
let highEndTime = highPart[0]["highEndTime"] as! Int
+ if highPart[0].keys.contains("preludeDuration") {
+ self.preludeDuration = highPart[0]["preludeDuration"] as! Int64
+ }
let time = highStartTime
startHighTime = time
self.lrcControl?.onHighPartTime(highStartTime: highStartTime, highEndTime: highEndTime)
} catch {
- print("Error while parsing JSON: \(error.localizedDescription)")
+ agoraPrintError("Error while parsing JSON: \(error.localizedDescription)")
}
}
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
}
-
- func onMusicChartsResult(_ requestId: String, result: [AgoraMusicChartInfo], errorCode: AgoraMusicContentCenterStatusCode) {
+
+ func onMusicChartsResult(_ requestId: String, result: [AgoraMusicChartInfo], reason: AgoraMusicContentCenterStateReason) {
guard let callback = musicChartDict[requestId] else {return}
- callback(requestId, errorCode, result)
+ callback(requestId, reason, result)
musicChartDict.removeValue(forKey: requestId)
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
}
- func onMusicCollectionResult(_ requestId: String, result: AgoraMusicCollection, errorCode: AgoraMusicContentCenterStatusCode) {
+ func onMusicCollectionResult(_ requestId: String, result: AgoraMusicCollection, reason: AgoraMusicContentCenterStateReason) {
guard let callback = musicSearchDict[requestId] else {return}
- callback(requestId, errorCode, result)
+ callback(requestId, reason, result)
musicSearchDict.removeValue(forKey: requestId)
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
}
- func onLyricResult(_ requestId: String, songCode: Int, lyricUrl: String?, errorCode: AgoraMusicContentCenterStatusCode) {
+ func onLyricResult(_ requestId: String, songCode: Int, lyricUrl: String?, reason: AgoraMusicContentCenterStateReason) {
+ agoraPrint("onLyricResult requestId: \(requestId) songCode: \(songCode) lyricUrl: \(lyricUrl ?? "") reason: \(reason.rawValue)")
guard let lrcUrl = lyricUrl else {return}
let callback = self.lyricCallbacks[requestId]
guard let lyricCallback = callback else { return }
self.lyricCallbacks.removeValue(forKey: requestId)
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
if lrcUrl.isEmpty {
lyricCallback(nil)
+ agoraPrintError("onLyricResult: lrcUrl.isEmpty")
return
}
lyricCallback(lrcUrl)
+ agoraPrint("onLyricResult: lrcUrl is \(lrcUrl)")
}
- func onPreLoadEvent(_ requestId: String, songCode: Int, percent: Int, lyricUrl: String?, status: AgoraMusicContentCenterPreloadStatus, errorCode: AgoraMusicContentCenterStatusCode) {
+ func onPreLoadEvent(_ requestId: String, songCode: Int, percent: Int, lyricUrl: String?, state: AgoraMusicContentCenterPreloadState, reason: AgoraMusicContentCenterStateReason) {
if let listener = self.loadMusicListeners.object(forKey: "\(songCode)" as NSString) as? IMusicLoadStateListener {
- listener.onMusicLoadProgress(songCode: songCode, percent: percent, status: status, msg: String(errorCode.rawValue), lyricUrl: lyricUrl)
+ listener.onMusicLoadProgress(songCode: songCode, percent: percent, state: state, msg: String(reason.rawValue), lyricUrl: lyricUrl)
}
- if (status == .preloading) { return }
- agoraPrint("songCode:\(songCode), status:\(status.rawValue), code:\(errorCode.rawValue)")
+ if (state == .preloading) { return }
+ agoraPrint("songCode:\(songCode), status:\(state.rawValue), code:\(reason.rawValue)")
let SongCode = "\(songCode)"
guard let block = self.musicCallbacks[SongCode] else { return }
self.musicCallbacks.removeValue(forKey: SongCode)
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
- block(status, songCode)
+ block(state, songCode)
}
}
@@ -1419,7 +1588,7 @@ extension Date {
extension KTVApiImpl: KTVApiRTCDelegate {
func didJoinChannel(channel: String, withUid uid: UInt, elapsed: Int) {
- print("ktvapi加入主频道成功")
+ agoraPrint("ktvapi加入主频道成功")
}
func didJoinedOfUid(uid: UInt, elapsed: Int) {
@@ -1454,11 +1623,12 @@ extension KTVApiImpl: KTVApiRTCDelegate {
func didAudioPublishStateChange(channelId: String, oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int32) {
self.isPublishAudio = newState == .published
enableProfessionalStreamerMode(self.enableProfessional)
- print("PublishStateChange:\(newState)")
+ agoraPrint("PublishStateChange:\(newState)")
}
func receiveStreamMessageFromUid(uid: UInt, streamId: Int, data: Data) {
let role = singerRole
+ if isRelease {return}
guard let dict = dataToDictionary(data: data), let cmd = dict["cmd"] as? String else { return }
switch cmd {
@@ -1481,8 +1651,8 @@ extension KTVApiImpl: KTVApiRTCDelegate {
}
func didRTCAudioRouteChanged(routing: AgoraAudioOutputRouting) {
- print("Route changed:\(routing)")
- let headPhones: [AgoraAudioOutputRouting] = [.headset, .headsetBluetooth, .headsetNoMic]
+ agoraPrint("Route changed:\(routing)")
+ let headPhones: [AgoraAudioOutputRouting] = [.headset, .bluetoothDeviceHfp, .bluetoothDeviceA2dp, .headsetNoMic]
let wearHeadPhone: Bool = headPhones.contains(routing)
if wearHeadPhone == self.isWearingHeadPhones {
return
@@ -1490,7 +1660,17 @@ extension KTVApiImpl: KTVApiRTCDelegate {
self.isWearingHeadPhones = wearHeadPhone
enableProfessionalStreamerMode(self.enableProfessional)
}
+
+ func audioMetadataReceived(uid: UInt, metadata: Data) {
+ guard let time: LrcTime = try? LrcTime(serializedData: metadata) else {return}
+ if time.type == .lrcTime && self.singerRole == .audience {
+ self.setProgress(with: Int(time.ts))
+ }
+ }
+ @objc func didAudioMetadataReceived( uid: UInt, metadata: Data) {
+
+ }
}
/*----这一块的代码主要是用来处理主频道的RTC代理事件,外部不再需要手动转代理,😁---*/
@@ -1502,6 +1682,7 @@ protocol KTVApiRTCDelegate: NSObjectProtocol {
func didAudioPublishStateChange(channelId: String, oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int32)
func receiveStreamMessageFromUid(uid: UInt, streamId: Int, data: Data)
func localAudioStats(stats: AgoraRtcLocalAudioStats)
+ func audioMetadataReceived( uid: UInt, metadata: Data)
}
class KTVApiRTCDelegateHandler: NSObject, AgoraRtcEngineDelegate {
@@ -1538,5 +1719,15 @@ class KTVApiRTCDelegateHandler: NSObject, AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) {
delegate.localAudioStats(stats: stats)
}
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, audioMetadataReceived uid: UInt, metadata: Data) {
+ delegate.audioMetadataReceived(uid: uid, metadata: metadata)
+ }
}
+
+extension KTVApiImpl {
+ @objc public func isSongLoading(songCode: String) -> Bool {
+ return musicCallbacks[songCode] == nil ? false : true
+ }
+}
diff --git a/KTVAPI/iOS/Classes/KTVGiantChorusApiImpl.swift b/KTVAPI/iOS/Classes/KTVGiantChorusApiImpl.swift
new file mode 100644
index 0000000..cdc5458
--- /dev/null
+++ b/KTVAPI/iOS/Classes/KTVGiantChorusApiImpl.swift
@@ -0,0 +1,2059 @@
+//
+// KTVApiImpl.swift
+// AgoraEntScenarios
+//
+// Created by wushengtao on 2023/3/14.
+//
+
+import Foundation
+import AgoraRtcKit
+
+/// 加载歌曲状态
+@objc fileprivate enum KTVLoadSongState: Int {
+ case idle = -1 //空闲
+ case ok = 0 //成功
+ case failed //失败
+ case inProgress //加载中
+}
+
+fileprivate enum KTVSongMode: Int {
+ case songCode
+ case songUrl
+}
+
+@objc class KTVGiantChorusApiImpl: NSObject, KTVApiDelegate{
+
+ private var apiConfig: GiantChorusConfiguration?
+
+ private var songConfig: KTVSongConfiguration?
+ private var subChorusConnection: AgoraRtcConnection?
+ private var singChannelConnection: AgoraRtcConnection?
+ private var mpkConnection: AgoraRtcConnection?
+
+ private var eventHandlers: NSHashTable = NSHashTable.weakObjects()
+ private var loadMusicListeners: NSMapTable = NSMapTable(keyOptions: .copyIn, valueOptions: .weakMemory)
+
+ // private var musicPlayer: AgoraRtcMediaPlayerProtocol? //mcc
+ private var mediaPlayer: AgoraRtcMediaPlayerProtocol? //local
+ private var mcc: AgoraMusicContentCenter?
+
+ private var loadSongMap = Dictionary()
+ private var lyricUrlMap = Dictionary()
+ private var loadDict = Dictionary()
+ private var lyricCallbacks = Dictionary()
+ private var musicCallbacks = Dictionary()
+
+ private var hasSendPreludeEndPosition: Bool = false
+ private var hasSendEndPosition: Bool = false
+
+ //multipath
+ private var enableMultipathing: Bool = true
+
+ private var audioPlayoutDelay: NSInteger = 0
+ private var isNowMicMuted: Bool = false
+ private var loadSongState: KTVLoadSongState = .idle
+ private var lastNtpTime: Int = 0
+ private var startHighTime: Int = 0
+ private var isRelease: Bool = false
+ private var songUrl2: String = ""
+ private var playerState: AgoraMediaPlayerState = .idle {
+ didSet {
+ agoraPrint("playerState did changed: \(oldValue.rawValue)->\(playerState.rawValue)")
+ updateRemotePlayBackVolumeIfNeed()
+ updateTimer(with: playerState)
+ }
+ }
+ private var pitch: Double = 0
+ private var localPlayerPosition: TimeInterval = 0
+ private var remotePlayerPosition: TimeInterval = 0
+ private var remotePlayerDuration: TimeInterval = 0
+ private var localPlayerSystemTime: TimeInterval = 0
+ private var lastMainSingerUpdateTime: TimeInterval = 0
+ private var playerDuration: TimeInterval = 0
+ // private lazy var apiDelegateHandler = KTVApiRTCDelegateHandler(with: self)
+
+ private var musicChartDict: [String: MusicChartCallBacks] = [:]
+ private var musicSearchDict: Dictionary = Dictionary()
+ private var onJoinExChannelCallBack : JoinExChannelCallBack?
+ private var mainSingerHasJoinChannelEx: Bool = false
+ private var dataStreamId: Int = 0
+ private var lastReceivedPosition: TimeInterval = 0
+ private var localPosition: Int = 0
+
+ private var songMode: KTVSongMode = .songCode
+ private var useCustomAudioSource:Bool = false
+ private var songUrl: String = ""
+ private var songCode: Int = 0
+ private var songIdentifier: String = ""
+
+ private var singerRole: KTVSingRole = .audience {
+ didSet {
+ agoraPrint("singerRole changed: \(oldValue.rawValue)->\(singerRole.rawValue)")
+ }
+ }
+ private var lrcControl: KTVLrcViewDelegate?
+
+ private var timer: Timer?
+ private var isPause: Bool = false
+
+ private var singingScore: Int = 0
+
+ public var remoteVolume: Int = 30
+ private var joinChorusNewRole: KTVSingRole = .audience
+ private var oldPitch: Double = 0
+ private var isWearingHeadPhones: Bool = false
+ private var enableProfessional: Bool = false
+ private var isPublishAudio: Bool = false
+ private var audioRouting: Int = -1
+ private var recvFromDataStream = false
+ //大合唱独有
+ private var mStopSyncPitch = true
+ private var mSyncPitchTimer: DispatchSourceTimer?
+ private var mStopSyncScore = true
+ private var mSyncScoreTimer: DispatchSourceTimer?
+ private var mStopSyncCloudConvergenceStatus = true
+ private var mSyncCloudConvergenceStatusTimer: DispatchSourceTimer?
+ private var mStopProcessDelay = true
+ private var processDelayFuture: DispatchSourceTimer?
+ private var processSubscribeFuture: DispatchSourceTimer?
+ private var subScribeSingerMap = [Int: Int]() //
+ private var singerList = [Int]() //
+ private var mainSingerDelay = 0
+
+ private let tag = "KTV_API_LOG"
+ private let messageId = "agora:scenarioAPI"
+ private let version = "5.0.0"
+ private let lyricSyncVersion = 2
+
+ private var apiRepoter: APIReporter?
+
+ deinit {
+ mcc?.register(nil)
+ agoraPrint("deinit KTVApiImpl")
+ }
+
+ func createKTVGiantChorusApi(config: GiantChorusConfiguration) {
+ self.apiConfig = config
+ agoraPrint("createKTVGiantChorusApi")
+ self.singChannelConnection = AgoraRtcConnection(channelId: config.chorusChannelName, localUid: config.localUid)
+
+ setParams()
+
+ if config.musicType == .mcc {
+ // ------------------ 初始化内容中心 ------------------
+ let contentCenterConfiguration = AgoraMusicContentCenterConfig()
+ contentCenterConfiguration.appId = config.appId
+ contentCenterConfiguration.mccUid = config.localUid
+ contentCenterConfiguration.token = config.rtmToken
+ contentCenterConfiguration.rtcEngine = config.engine
+ contentCenterConfiguration.maxCacheSize = UInt(config.maxCacheSize)
+ if let domain = config.mccDomain {
+ contentCenterConfiguration.mccDomain = domain
+ }
+ mcc = AgoraMusicContentCenter.sharedContentCenter(config: contentCenterConfiguration)
+ mcc?.register(self)
+ // ------------------ 初始化音乐播放器实例 ------------------
+ mediaPlayer = mcc?.createMusicPlayer(delegate: self)
+ mediaPlayer?.adjustPlayoutVolume(50)
+ mediaPlayer?.adjustPublishSignalVolume(50)
+ } else {
+ mediaPlayer = apiConfig?.engine?.createMediaPlayer(with: self)
+ // 音量最佳实践调整
+ mediaPlayer?.adjustPlayoutVolume(50)
+ mediaPlayer?.adjustPublishSignalVolume(50)
+ }
+
+ apiRepoter = APIReporter(type: .ktv, version: version, engine: apiConfig?.engine ?? AgoraRtcEngineKit())
+
+ initTimer()
+ mediaPlayer?.setPlayerOption("play_pos_change_callback", value: 100)
+ apiConfig?.engine?.setDelegateEx(self, connection: mpkConnection ?? AgoraRtcConnection())
+ startSyncPitch()
+ startSyncScore()
+ startSyncCloudConvergenceStatus()
+ }
+
+ private func setParams() {
+ guard let engine = self.apiConfig?.engine else {return}
+ engine.setParameters("{\"rtc.enable_nasa2\": true}")
+ engine.setParameters("{\"rtc.ntp_delay_drop_threshold\": 1000}")
+ engine.setParameters("{\"rtc.video.enable_sync_render_ntp\": true}")
+ engine.setParameters("{\"rtc.net.maxS2LDelay\": 800}")
+ engine.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\": true}")
+ engine.setParameters("{\"rtc.net.maxS2LDelayBroadcast\": 400}")
+ engine.setParameters("{\"che.audio.neteq.prebuffer\": true}")
+ engine.setParameters("{\"che.audio.neteq.prebuffer_max_delay\": 600}")
+ engine.setParameters("{\"che.audio.max_mixed_participants\": 8}")
+ engine.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ engine.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
+ engine.setParameters("{\"che.audio.neteq.targetlevel_offset\": 20}")
+ engine.setParameters("{\"che.audio.uplink_apm_async_process\": true}")
+ // 标准音质
+ engine.setParameters("{\"che.audio.aec.split_srate_for_48k\": 16000}")
+ engine.setParameters("{\"che.audio.ans.noise_gate\": 20}")//
+ engine.setParameters("{\"rtc.use_audio4\": true}")
+
+ //4.3.0 add
+ // mutipath
+ enableMultipathing = true
+ engine.setParameters("{\"rtc.enable_tds_request_on_join\": true}")
+ engine.setParameters("{\"rtc.remote_path_scheduling_strategy\": 0}")
+ engine.setParameters("{\"rtc.path_scheduling_strategy\": 0}")
+ engine.setParameters("{\"rtc.enableMultipath\": true}")
+
+ // 数据上报
+ engine.setParameters("{\"rtc.direct_send_custom_event\": true}")
+ // engine.setParameters("{\"rtc.qos_for_test_purpose\": true}")
+ }
+
+ func renewInnerDataStreamId() {
+ let dataStreamConfig = AgoraDataStreamConfig()
+ dataStreamConfig.ordered = false
+ dataStreamConfig.syncWithAudio = true
+ self.apiConfig?.engine?.createDataStreamEx(&dataStreamId, config: dataStreamConfig, connection: singChannelConnection ?? AgoraRtcConnection())
+
+ sendCustomMessage(with: "renewInnerDataStreamId", dict: [:])
+ agoraPrint("renewInnerDataStreamId")
+ }
+}
+
+//MARK: KTVApiDelegate
+extension KTVGiantChorusApiImpl {
+
+ func objectContent(of object: Any) -> [String: Any] {
+ var content = [String: Any]()
+
+ let mirror = Mirror(reflecting: object)
+ for child in mirror.children {
+ if let propertyName = child.label {
+ if let convertibleValue = convertToJSONSerializable(child.value) {
+ content[propertyName] = convertibleValue
+ }
+ }
+ }
+
+ return content
+ }
+
+ func convertToJSONSerializable(_ value: Any) -> Any? {
+ switch value {
+ case let value as String:
+ return value
+ case let value as Int:
+ return value
+ case let value as Double:
+ return value
+ case let value as Bool:
+ return value
+ case let value as Int?:
+ return value
+ case let value as Double?:
+ return value
+ case let value as Bool?:
+ return value
+ case let value as String?:
+ return value
+ default:
+ return nil
+ }
+ }
+
+ func getMusicContentCenter() -> AgoraMusicContentCenter? {
+ return mcc
+ }
+
+ func setLrcView(view: KTVLrcViewDelegate) {
+ sendCustomMessage(with: "renewInnerDataStreamId", dict: [:])
+ lrcControl = view
+ }
+
+ //主要针对本地歌曲播放的主唱伴奏切换的 loadmusic MCC直接忽视这个方法
+ func load2Music(url1: String, url2: String, config: KTVSongConfiguration) {
+ agoraPrint("load2Music called: songUrl url1:(url1),url2:(url2)")
+ self.songMode = .songUrl
+ self.songConfig = config
+ self.songIdentifier = config.songIdentifier
+ self.songUrl = url1
+ self.songUrl2 = url2
+
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// switchSingerRole(newRole: .soloSinger) { state, failRes in
+//
+// }
+// }
+// startSing(url: url1, startPos: 0)
+// }
+ }
+
+ //主要针对本地歌曲播放的主唱伴奏切换的 MCC直接忽视这个方法
+ func switchPlaySrc(url: String, syncPts: Bool) {
+ agoraPrint("switchPlaySrc called: \(url)")
+
+ if self.songUrl != url && self.songUrl2 != url {
+ print("switchPlaySrc failed: canceled")
+ return
+ }
+
+ let curPlayPosition: Int = syncPts ? mediaPlayer?.getPosition() ?? 0 : 0
+ mediaPlayer?.stop()
+ startSing(url: url, startPos: curPlayPosition)
+ }
+
+ func loadMusic(songCode: Int, config: KTVSongConfiguration, onMusicLoadStateListener: IMusicLoadStateListener) {
+ sendCustomMessage(with: "loadMusicWithSongCode:\(songCode)", dict: objectContent(of: config))
+ agoraPrint("loadMusic songCode:\(songCode) ")
+ self.songMode = .songCode
+ self.songCode = songCode
+ self.songIdentifier = config.songIdentifier
+ _loadMusic(config: config, mode: config.mode, onMusicLoadStateListener: onMusicLoadStateListener)
+ }
+
+ func loadMusic(config: KTVSongConfiguration, url: String) {
+ sendCustomMessage(with: "loadMusicWithUrl:\(url)", dict: objectContent(of: config))
+ agoraPrint("loadMusic url:\(url)")
+ self.songMode = .songUrl
+ self.songUrl = url
+ self.songIdentifier = config.songIdentifier
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if singerRole != .leadSinger {
+// switchSingerRole(newRole: .soloSinger) { _, _ in
+//
+// }
+// }
+// startSing(url: url, startPos: 0)
+// }
+ }
+
+ func getMusicPlayer() -> AgoraRtcMediaPlayerProtocol? {
+ return mediaPlayer
+ }
+
+ func addEventHandler(ktvApiEventHandler: KTVApiEventHandlerDelegate) {
+ sendCustomMessage(with: "addEventHandler", dict: [:])
+ agoraPrint("addEventHandler")
+ if eventHandlers.contains(ktvApiEventHandler) {
+ return
+ }
+ eventHandlers.add(ktvApiEventHandler)
+ }
+
+ func removeEventHandler(ktvApiEventHandler: KTVApiEventHandlerDelegate) {
+ sendCustomMessage(with: "removeEventHandler", dict: [:])
+ agoraPrint("removeEventHandler")
+ eventHandlers.remove(ktvApiEventHandler)
+ }
+
+ func cleanCache() {
+ sendCustomMessage(with: "cleanCache", dict: [:])
+ isRelease = true
+ mediaPlayer?.stop()
+ freeTimer()
+ agoraPrint("cleanCache")
+ singerRole = .audience
+
+ stopSyncCloudConvergenceStatus()
+ stopSyncScore()
+ singingScore = 0
+ lrcControl = nil
+ lyricCallbacks.removeAll()
+ musicCallbacks.removeAll()
+ onJoinExChannelCallBack = nil
+ loadMusicListeners.removeAllObjects()
+ apiConfig?.engine?.destroyMediaPlayer(mediaPlayer)
+ mediaPlayer = nil
+ if apiConfig?.musicType == .mcc {
+ mcc?.register(nil)
+ mcc = nil
+ }
+ apiConfig = nil
+ AgoraMusicContentCenter.destroy()
+ self.eventHandlers.removeAllObjects()
+ }
+
+ @objc public func enableMutipath(enable: Bool) {
+ sendCustomMessage(with: "enableMutipath", dict: ["enable":enable])
+ agoraPrint("enableMutipath:\(enable)")
+ enableMultipathing = enable
+ if singerRole == .coSinger || singerRole == .leadSinger {
+ if let subChorusConnection = subChorusConnection {
+ apiConfig?.engine?.setParametersEx("{\"rtc.enableMultipath\": \(enable), \"rtc.path_scheduling_strategy\": 0, \"rtc.remote_path_scheduling_strategy\": 0}", connection: subChorusConnection)
+ }
+ }
+ }
+
+ func renewToken(rtmToken: String, chorusChannelRtcToken: String) {
+ let dict: [String: Any] = [
+ "rtmToken":rtmToken,
+ "chorusChannelRtcToken":chorusChannelRtcToken
+ ]
+ sendCustomMessage(with: "renewToken", dict: dict)
+ agoraPrint("renewToken rtmToken:\(rtmToken) chorusChannelRtcToken:\(chorusChannelRtcToken)")
+ // 更新RtmToken
+ mcc?.renewToken(rtmToken)
+ // 更新合唱频道RtcToken
+ if let subChorusConnection = subChorusConnection {
+ let channelMediaOption = AgoraRtcChannelMediaOptions()
+ channelMediaOption.token = chorusChannelRtcToken
+ apiConfig?.engine?.updateChannelEx(with: channelMediaOption, connection: subChorusConnection)
+ }
+ }
+
+ func fetchMusicCharts(completion: @escaping MusicChartCallBacks) {
+ sendCustomMessage(with: "fetchMusicCharts", dict: [:])
+ agoraPrint("fetchMusicCharts")
+ let requestId = mcc!.getMusicCharts()
+ musicChartDict[requestId] = completion
+ }
+
+ func searchMusic(musicChartId: Int,
+ page: Int,
+ pageSize: Int,
+ jsonOption: String,
+ completion:@escaping (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void) {
+ agoraPrint("searchMusic with musicChartId: \(musicChartId)")
+ let dict: [String: Any] = [
+ "musicChartId":musicChartId,
+ "page": page,
+ "pageSize": pageSize,
+ "jsonOption": jsonOption
+ ]
+ sendCustomMessage(with: "searchMusic", dict: dict)
+ let requestId = mcc!.getMusicCollection(musicChartId: musicChartId, page: page, pageSize: pageSize, jsonOption: jsonOption)
+ musicSearchDict[requestId] = completion
+ }
+
+ func searchMusic(keyword: String,
+ page: Int,
+ pageSize: Int,
+ jsonOption: String,
+ completion: @escaping (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void) {
+ agoraPrint("searchMusic with keyword: \(keyword)")
+ let dict: [String: Any] = [
+ "keyword": keyword,
+ "page": page,
+ "pageSize": pageSize,
+ "jsonOption": jsonOption
+ ]
+ sendCustomMessage(with: "searchMusic", dict: dict)
+ let requestId = mcc!.searchMusic(keyWord: keyword, page: page, pageSize: pageSize, jsonOption: jsonOption)
+ musicSearchDict[requestId] = completion
+ }
+
+// func switchSingerRole(newRole: KTVSingRole, onSwitchRoleState: @escaping (KTVSwitchRoleState, KTVSwitchRoleFailReason) -> Void) {
+// let oldRole = singerRole
+// self.switchSingerRole(oldRole: oldRole, newRole: newRole, token: apiConfig?.chorusChannelToken ?? "", stateCallBack: onSwitchRoleState)
+// }
+
+ /**
+ * 恢复播放
+ */
+ @objc public func resumeSing() {
+ sendCustomMessage(with: "resumeSing", dict: [:])
+ agoraPrint("resumeSing")
+ if mediaPlayer?.getPlayerState() == .paused {
+ mediaPlayer?.resume()
+ } else {
+ let ret = mediaPlayer?.play()
+ agoraPrint("resumeSing ret: \(ret ?? -1)")
+ }
+ }
+
+ /**
+ * 暂停播放
+ */
+ @objc public func pauseSing() {
+ sendCustomMessage(with: "pauseSing", dict: [:])
+ agoraPrint("pauseSing")
+ mediaPlayer?.pause()
+ }
+
+ /**
+ * 调整进度
+ */
+ @objc public func seekSing(time: NSInteger) {
+ sendCustomMessage(with: "seekSing", dict: ["time":time])
+ agoraPrint("seekSing")
+ mediaPlayer?.seek(toPosition: time)
+ }
+
+ /**
+ * 选择音轨,原唱、伴唱
+ */
+// @objc public func selectPlayerTrackMode(mode: KTVPlayerTrackMode) {
+// apiConfig?.engine.selectAudioTrack(mode == .original ? 0 : 1)
+// }
+
+ /**
+ * 设置当前mic开关状态
+ */
+ @objc public func muteMic(muteStatus: Bool) {
+ sendCustomMessage(with: "setMicStatus", dict: ["muteStatus":muteStatus])
+ agoraPrint("setMicStatus status:\(muteStatus)")
+ self.isNowMicMuted = muteStatus
+ if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
+ apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
+ } else {
+ apiConfig?.engine?.muteLocalAudioStream(muteStatus)
+ }
+ }
+
+ @objc public func removeMusic(songCode: Int) {
+ sendCustomMessage(with: "removeMusic", dict: ["songCode": songCode])
+ agoraPrint("removeMusic:\(songCode)")
+ let ret: Int = mcc?.removeCache(songCode: songCode) ?? 0
+ if ret < 0 {
+ agoraPrint("removeMusic failed: ret:\(ret)")
+ }
+ }
+
+ private func agoraPrint(_ message: String) {
+ apiRepoter?.writeLog(content: message, level: .info)
+ }
+
+ private func agoraPrintError(_ message: String) {
+ apiRepoter?.writeLog(content: message, level: .error)
+ }
+
+}
+
+// 主要是角色切换,加入合唱,加入多频道,退出合唱,退出多频道
+extension KTVGiantChorusApiImpl {
+// private func switchSingerRole(oldRole: KTVSingRole, newRole: KTVSingRole, token: String, stateCallBack:@escaping ISwitchRoleStateListener) {
+// // agoraPrint("switchSingerRole oldRole: \(oldRole.rawValue), newRole: \(newRole.rawValue)")
+// if oldRole == .audience && newRole == .soloSinger {
+// // 1、KTVSingRoleAudience -》KTVSingRoleMainSinger
+// singerRole = newRole
+// becomeSoloSinger()
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .audience, newRole: .soloSinger)
+// }
+//
+// stateCallBack(.success, .none)
+// } else if oldRole == .audience && newRole == .leadSinger {
+// becomeSoloSinger()
+// joinChorus(role: newRole, token: token, joinExChannelCallBack: {[weak self] flag, status in
+// guard let self = self else {return}
+// //还原临时变量为观众
+// self.joinChorusNewRole = .audience
+//
+// if flag == true {
+// self.singerRole = newRole
+// self.getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .audience, newRole: .leadSinger)
+// }
+// stateCallBack(.success, .none)
+// } else {
+// self.leaveChorus(role: .leadSinger)
+// stateCallBack(.fail, .joinChannelFail)
+// }
+// })
+//
+// } else if oldRole == .soloSinger && newRole == .audience {
+// stopSing()
+// singerRole = newRole
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .soloSinger, newRole: .audience)
+// }
+//
+// stateCallBack(.success, .none)
+// } else if oldRole == .audience && newRole == .coSinger {
+// joinChorus(role: newRole, token: token, joinExChannelCallBack: {[weak self] flag, status in
+// guard let self = self else {return}
+// //还原临时变量为观众
+// self.joinChorusNewRole = .audience
+// if flag == true {
+// self.singerRole = newRole
+// //TODO(chenpan):如果观众变成伴唱,需要重置state,防止同步主唱state因为都是playing不会修改
+// //后面建议改成remote state(通过data stream获取)和local state(通过player didChangedToState获取)
+// self.playerState = self.mediaPlayer?.getPlayerState() ?? .idle
+// self.getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .audience, newRole: .coSinger)
+// }
+// stateCallBack(.success, .none)
+// } else {
+// self.leaveChorus(role: .coSinger)
+// stateCallBack(.fail, .joinChannelFail)
+// }
+// })
+// } else if oldRole == .coSinger && newRole == .audience {
+// leaveChorus(role: .coSinger)
+// singerRole = newRole
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .coSinger, newRole: .audience)
+// }
+//
+// stateCallBack(.success, .none)
+// } else if oldRole == .soloSinger && newRole == .leadSinger {
+// joinChorus(role: newRole, token: token, joinExChannelCallBack: {[weak self] flag, status in
+// guard let self = self else {return}
+// //还原临时变量为观众
+// self.joinChorusNewRole = .audience
+// if flag == true {
+// self.singerRole = newRole
+// self.getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .soloSinger, newRole: .leadSinger)
+// }
+// stateCallBack(.success, .none)
+// } else {
+// self.leaveChorus(role: .leadSinger)
+// stateCallBack(.fail, .joinChannelFail)
+// }
+// })
+// } else if oldRole == .leadSinger && newRole == .soloSinger {
+// leaveChorus(role: .leadSinger)
+// singerRole = newRole
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .leadSinger, newRole: .soloSinger)
+// }
+//
+// stateCallBack(.success, .none)
+// } else if oldRole == .leadSinger && newRole == .audience {
+// leaveChorus(role: .leadSinger)
+// stopSing()
+// singerRole = newRole
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .leadSinger, newRole: .audience)
+// }
+//
+// stateCallBack(.success, .none)
+// } else {
+// stateCallBack(.fail, .noPermission)
+// agoraPrint("Error!You can not switch role from \(oldRole.rawValue) to \(newRole.rawValue)!")
+// }
+//
+// }
+
+ func switchSingerRole(newRole: KTVSingRole, onSwitchRoleState: @escaping (KTVSwitchRoleState, KTVSwitchRoleFailReason) -> Void) {
+
+ agoraPrint("switchSingerRole oldRole: \(singerRole), newRole: \(newRole)")
+ let oldRole = singerRole
+
+ if singerRole == .audience && newRole == .leadSinger {
+ // 1、Audience -》LeadSinger
+ // 离开观众频道
+ apiConfig?.engine?.leaveChannelEx(AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0))
+ joinChorus(newRole: newRole)
+ self.singerRole = newRole
+ self.getEventHander { delegate in
+ delegate.onSingerRoleChanged(oldRole: .audience, newRole: .leadSinger)
+ }
+ onSwitchRoleState(.success, .none)
+ } else if singerRole == .audience && newRole == .coSinger {
+ // 2、Audience -》CoSinger
+ // 离开观众频道
+ apiConfig?.engine?.leaveChannelEx(AgoraRtcConnection( channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0))
+ joinChorus(newRole: newRole)
+ singerRole = newRole
+ self.getEventHander { delegate in
+ delegate.onSingerRoleChanged(oldRole: .audience, newRole: .coSinger)
+ }
+ onSwitchRoleState(.success, .none)
+ } else if singerRole == .coSinger && newRole == .audience {
+ // 3、CoSinger -》Audience
+ leaveChorus2(role: singerRole)
+ // 加入观众频道
+ apiConfig?.engine?.joinChannelEx(byToken: apiConfig?.audienceChannelToken,
+ connection: AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0),
+ delegate: self,
+ mediaOptions: AgoraRtcChannelMediaOptions(),
+ joinSuccess: {[weak self] _,_, _ in
+ })
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0))
+ self.singerRole = newRole
+ self.getEventHander { delegate in
+ delegate.onSingerRoleChanged(oldRole: oldRole, newRole: newRole)
+ }
+ onSwitchRoleState(.success, .none)
+ } else if singerRole == .leadSinger && newRole == .audience {
+ // 4、LeadSinger -》Audience
+ stopSing()
+ leaveChorus2(role: singerRole)
+ // 加入观众频道
+ apiConfig?.engine?.joinChannelEx(byToken: apiConfig?.audienceChannelToken,
+ connection: AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0),
+ delegate: self,
+ mediaOptions: AgoraRtcChannelMediaOptions(),
+ joinSuccess: {[weak self] _,_, _ in
+ })
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0))
+ self.singerRole = newRole
+ self.getEventHander { delegate in
+ delegate.onSingerRoleChanged(oldRole: oldRole, newRole: newRole)
+ }
+ onSwitchRoleState(.success, .none)
+ } else {
+ onSwitchRoleState(.fail, .noPermission)
+ print("Error! You can not switch role from \(singerRole) to \(newRole)!")
+ }
+ }
+
+ private func becomeSoloSinger() {
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 80000}")
+ apiConfig?.engine?.setAudioScenario(.chorus)
+ agoraPrint("becomeSoloSinger")
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ mediaOption.autoSubscribeAudio = true
+ //mediaOption.autoSubscribeVideo = true
+ if apiConfig?.musicType == .mcc {
+ mediaOption.publishMediaPlayerId = Int(mediaPlayer?.getMediaPlayerId() ?? 0)
+ } else {
+ mediaOption.publishMediaPlayerId = Int(mediaPlayer?.getMediaPlayerId() ?? 0)
+ }
+ mediaOption.publishMediaPlayerAudioTrack = true
+ apiConfig?.engine?.updateChannel(with: mediaOption)
+ }
+
+ /**
+ * 加入合唱
+ */
+ private func joinChorus(role: KTVSingRole, token: String, joinExChannelCallBack: @escaping JoinExChannelCallBack) {
+ self.onJoinExChannelCallBack = joinExChannelCallBack
+ if role == .leadSinger {
+ agoraPrint("joinChorus: KTVSingRoleMainSinger")
+ joinChorus2ndChannel(newRole: role, token: token)
+ } else if role == .coSinger {
+
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ mediaOption.autoSubscribeAudio = true
+ // mediaOption.autoSubscribeVideo = true
+ mediaOption.publishMediaPlayerAudioTrack = false
+ apiConfig?.engine?.updateChannel(with: mediaOption)
+
+ if apiConfig?.musicType == .mcc {
+ (mediaPlayer as? AgoraMusicPlayerProtocol)?.openMedia(songCode: self.songCode , startPos: 0)
+ } else {
+ mediaPlayer?.open(self.songUrl, startPos: 0)
+ }
+
+ joinChorus2ndChannel(newRole: role, token: token)
+
+ } else if role == .audience {
+ agoraPrint("joinChorus fail!")
+ }
+ }
+
+ private func joinChorus2ndChannel(newRole: KTVSingRole, token: String) {
+ let role = newRole
+ if role == .soloSinger || role == .audience {
+ agoraPrint("joinChorus2ndChannel with wrong role")
+ return
+ }
+
+ agoraPrint("joinChorus2ndChannel role: \(role.rawValue)")
+ if newRole == .coSinger {
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ apiConfig?.engine?.setAudioScenario(.chorus)
+ }
+
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ // main singer do not subscribe 2nd channel
+ // co singer auto sub
+ mediaOption.autoSubscribeAudio = role != .leadSinger
+ // mediaOption.autoSubscribeVideo = false
+ mediaOption.publishMicrophoneTrack = newRole == .leadSinger
+ mediaOption.enableAudioRecordingOrPlayout = role != .leadSinger
+ mediaOption.clientRoleType = .broadcaster
+
+ let rtcConnection = AgoraRtcConnection()
+ rtcConnection.channelId = apiConfig?.chorusChannelName ?? ""
+ rtcConnection.localUid = UInt(apiConfig?.localUid ?? 0)
+ subChorusConnection = rtcConnection
+
+ joinChorusNewRole = role
+ let ret = apiConfig?.engine?.joinChannelEx(byToken: token, connection: rtcConnection, delegate: self, mediaOptions: mediaOption, joinSuccess: nil)
+ agoraPrint("joinChannelEx ret: \(ret ?? -999)")
+ if newRole == .coSinger {
+ let uid = UInt(songConfig?.mainSingerUid ?? 0)
+ let ret =
+ apiConfig?.engine?.muteRemoteAudioStreamEx(uid, mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ agoraPrint("muteRemoteAudioStream: \(uid), ret: \(ret ?? -1)")
+ }
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: rtcConnection)
+
+ }
+
+ private func leaveChorus2ndChannel(_ role: KTVSingRole) {
+ guard let config = songConfig else {return}
+ guard let subConn = subChorusConnection else {return}
+ if (role == .leadSinger) {
+ apiConfig?.engine?.leaveChannelEx(subConn)
+ } else if (role == .coSinger) {
+ apiConfig?.engine?.leaveChannelEx(subConn)
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(config.mainSingerUid), mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ }
+
+ /**
+ * 离开合唱
+ */
+
+ private func leaveChorus(role: KTVSingRole) {
+ agoraPrint("leaveChorus role: \(singerRole.rawValue)")
+ if role == .leadSinger {
+ mainSingerHasJoinChannelEx = false
+ leaveChorus2ndChannel(role)
+ } else if role == .coSinger {
+ mediaPlayer?.stop()
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ // mediaOption.autoSubscribeAudio = true
+ // mediaOption.autoSubscribeVideo = false
+ mediaOption.publishMediaPlayerAudioTrack = false
+ apiConfig?.engine?.updateChannel(with: mediaOption)
+ leaveChorus2ndChannel(role)
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ apiConfig?.engine?.setAudioScenario(.gameStreaming)
+ } else if role == .audience {
+ agoraPrint("joinChorus: KTVSingRoleAudience does not need to leaveChorus!")
+ }
+ }
+
+}
+
+extension KTVGiantChorusApiImpl {
+
+ private func getEventHander(callBack:((KTVApiEventHandlerDelegate)-> Void)) {
+ for obj in eventHandlers.allObjects {
+ if obj is KTVApiEventHandlerDelegate {
+ callBack(obj as! KTVApiEventHandlerDelegate)
+ }
+ }
+ }
+
+ private func _loadMusic(config: KTVSongConfiguration, mode: KTVLoadMusicMode, onMusicLoadStateListener: IMusicLoadStateListener){
+
+ songConfig = config
+ lastReceivedPosition = 0
+ localPosition = 0
+
+ if (config.mode == .loadNone) {
+ return
+ }
+
+ if mode == .loadLrcOnly {
+ loadLyric(with: songCode) { [weak self] url in
+ guard let self = self else { return }
+ agoraPrint("loadLrcOnly: songCode:\(self.songCode) ulr:\(String(describing: url))")
+// if self.songCode != songCode {
+// onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .cancled)
+// return
+// }
+ if let urlPath = url, !urlPath.isEmpty {
+ self.lyricUrlMap[String(self.songCode)] = urlPath
+ self.setLyric(with: urlPath) { lyricUrl in
+ onMusicLoadStateListener.onMusicLoadSuccess(songCode: self.songCode, lyricUrl: urlPath)
+ }
+ } else {
+ onMusicLoadStateListener.onMusicLoadFail(songCode: self.songCode, reason: .noLyricUrl)
+ }
+
+// if (config.autoPlay) {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+//
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
+ }
+ } else {
+ loadMusicListeners.setObject(onMusicLoadStateListener, forKey: "\(self.songCode)" as NSString)
+ onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, state: .preloading, msg: "", lyricUrl: "")
+ // TODO: 只有未缓存时才显示进度条
+ if mcc?.isPreloaded(songCode: songCode) != 0 {
+ onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, state: .preloading, msg: "", lyricUrl: "")
+ }
+
+ preloadMusic(with: songCode) { [weak self] status, songCode in
+ guard let self = self else { return }
+ if self.songCode != songCode {
+ onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .cancled)
+ return
+ }
+ if status == .OK {
+ if mode == .loadMusicAndLrc {
+ // 需要加载歌词
+ self.loadLyric(with: songCode) { url in
+ self.agoraPrint("loadMusicAndLrc: songCode:\(songCode) status:\(status.rawValue) ulr:\(String(describing: url))")
+ if self.songCode != songCode {
+ onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .cancled)
+ return
+ }
+ if let urlPath = url, !urlPath.isEmpty {
+ self.lyricUrlMap[String(songCode)] = urlPath
+ self.setLyric(with: urlPath) { lyricUrl in
+ onMusicLoadStateListener.onMusicLoadSuccess(songCode: songCode, lyricUrl: urlPath)
+ }
+ } else {
+ onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .noLyricUrl)
+ }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+//
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
+ }
+ } else if mode == .loadMusicOnly {
+ agoraPrint("loadMusicOnly: songCode:\(songCode) load success")
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+//
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
+ onMusicLoadStateListener.onMusicLoadSuccess(songCode: songCode, lyricUrl: "")
+ }
+ } else {
+ agoraPrint("load music failed songCode:\(songCode)")
+ onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .musicPreloadFail)
+ }
+ }
+ }
+ }
+
+ private func loadLyric(with songCode: NSInteger, callBack:@escaping LyricCallback) {
+ agoraPrint("loadLyric songCode: \(songCode)")
+ let requestId: String = self.mcc?.getLyric(songCode: songCode, lyricType: 0) ?? ""
+ self.lyricCallbacks.updateValue(callBack, forKey: requestId)
+ }
+
+ private func preloadMusic(with songCode: Int, callback: @escaping LoadMusicCallback) {
+ agoraPrint("preloadMusic songCode: \(songCode)")
+ if self.mcc?.isPreloaded(songCode: songCode) == 0 {
+ musicCallbacks.removeValue(forKey: String(songCode))
+ callback(.OK, songCode)
+ return
+ }
+ let err = self.mcc?.preload(songCode: songCode)
+ if err == nil {
+ musicCallbacks.removeValue(forKey: String(songCode))
+ callback(.error, songCode)
+ return
+ }
+ musicCallbacks.updateValue(callback, forKey: String(songCode))
+ }
+
+ private func setLyric(with url: String, callBack: @escaping LyricCallback) {
+ agoraPrint("setLyric url: (url)")
+ self.lrcControl?.onDownloadLrcData(url: url)
+ callBack(url)
+ }
+
+ func startSing(songCode: Int, startPos: Int) {
+ let dict: [String: Any] = [
+ "songCode": songCode,
+ "startPos": startPos
+ ]
+ sendCustomMessage(with: "startSing", dict: dict)
+ let role = singerRole
+ agoraPrint("startSing role: \(role.rawValue)")
+ if self.songCode != songCode {
+ agoraPrint("startSing failed: canceled")
+ return
+ }
+ mediaPlayer?.setPlayerOption("enable_multi_audio_track", value: 1)
+ apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
+ let ret = (mediaPlayer as? AgoraMusicPlayerProtocol)?.openMedia(songCode: songCode, startPos: startPos)
+ mediaPlayer?.setLoopCount(-1)
+ agoraPrint("startSing->openMedia(\(songCode) fail: \(ret ?? -1)")
+ }
+
+ func startSing(url: String, startPos: Int) {
+ let dict: [String: Any] = [
+ "url": url,
+ "startPos": startPos
+ ]
+ sendCustomMessage(with: "startSing", dict: dict)
+ let role = singerRole
+ agoraPrint("startSing role: \(role.rawValue)")
+ if self.songUrl != songUrl {
+ agoraPrint("startSing failed: canceled")
+ return
+ }
+ apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
+ let ret = mediaPlayer?.open(url, startPos: startPos)
+ agoraPrint("startSing->openMedia(\(url) fail: \(ret ?? -1)")
+ }
+
+ /**
+ * 停止播放歌曲
+ */
+ @objc public func stopSing() {
+ agoraPrint("stopSing")
+ sendCustomMessage(with: "stopSing", dict: [:])
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ // mediaOption.autoSubscribeAudio = true
+ // mediaOption.autoSubscribeVideo = true
+ mediaOption.publishMediaPlayerAudioTrack = false
+ apiConfig?.engine?.updateChannelEx(with: mediaOption, connection: singChannelConnection ?? AgoraRtcConnection())
+
+ if mediaPlayer?.getPlayerState() != .stopped {
+ mediaPlayer?.stop()
+ }
+
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ apiConfig?.engine?.setAudioScenario(.gameStreaming)
+ }
+
+ @objc public func setSingingScore(score: Int) {
+ self.singingScore = score
+ }
+
+ @objc func setAudienceStreamMessage(dict: [String: Any]) {
+ sendStreamMessageWithDict(dict) { _ in
+
+ }
+ }
+
+ @objc public func setAudioPlayoutDelay(audioPlayoutDelay: Int) {
+ self.audioPlayoutDelay = audioPlayoutDelay
+ }
+
+ @objc func enableProfessionalStreamerMode(_ enable: Bool) {
+ if self.isPublishAudio == false {return}
+ self.enableProfessional = enable
+ //专业非专业还需要根据是否佩戴耳机来判断是否开启3A
+ apiConfig?.engine?.setAudioProfile(enable ? .musicHighQualityStereo : .musicStandardStereo)
+ apiConfig?.engine?.setParameters("{\"che.audio.aec.enable\":\((enable && isWearingHeadPhones) ? false : true)}")
+ apiConfig?.engine?.setParameters("{\"che.audio.agc.enable\":\((enable && isWearingHeadPhones) ? false : true)}")
+ apiConfig?.engine?.setParameters("{\"che.audio.ans.enable\":\((enable && isWearingHeadPhones) ? false : true)}")
+ apiConfig?.engine?.setParameters("{\"che.audio.md.enable\": false}")
+ }
+
+ func joinChorus(newRole: KTVSingRole) {
+ agoraPrint("joinChorus: \(newRole)")
+ let singChannelMediaOptions = AgoraRtcChannelMediaOptions()
+ singChannelMediaOptions.autoSubscribeAudio = true
+ singChannelMediaOptions.publishMicrophoneTrack = true
+ singChannelMediaOptions.clientRoleType = .broadcaster
+// singChannelMediaOptions.parameters = "{\"che.audio.max_mixed_participants\": 8}"
+ if newRole == .leadSinger {
+ // 主唱不参加TopN
+ singChannelMediaOptions.isAudioFilterable = false
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\":\(apiConfig?.routeSelectionConfig.streamNum ?? 0)}")
+ } else {
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\":\((apiConfig?.routeSelectionConfig.streamNum ?? 0) - 1)}")
+ }
+
+ guard let token = apiConfig?.chorusChannelToken, let singConnection = singChannelConnection else {return}
+
+
+ // 加入演唱频道
+ let ret = apiConfig?.engine?.joinChannelEx(byToken: token, connection: singConnection, delegate: self, mediaOptions: singChannelMediaOptions)
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: singConnection)
+ if apiConfig?.routeSelectionConfig.type == .topN || apiConfig?.routeSelectionConfig.type == .byDelayAndTopN {
+ if newRole == .leadSinger {
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\":\(apiConfig?.routeSelectionConfig.streamNum)}")
+ } else {
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\":\((apiConfig?.routeSelectionConfig.streamNum ?? 0) - 1)}")
+ }
+ } else {
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\": 0}")
+ }
+
+ let res = apiConfig?.engine?.enableAudioVolumeIndicationEx(50, smooth: 10, reportVad: true, connection: singConnection)
+ switch newRole {
+ case .leadSinger:
+ // 更新音频配置
+ apiConfig?.engine?.setAudioScenario(.chorus)
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 80000}")
+
+ // mpk流加入频道
+ let options = AgoraRtcChannelMediaOptions()
+ options.autoSubscribeAudio = false
+ options.autoSubscribeVideo = false
+ options.publishMicrophoneTrack = false
+ options.publishMediaPlayerAudioTrack = true
+ options.publishMediaPlayerId = Int(mediaPlayer?.getMediaPlayerId() ?? 0)
+ options.clientRoleType = .broadcaster
+ // 防止主唱和合唱听见mpk流的声音
+ options.enableAudioRecordingOrPlayout = false
+
+ let rtcConnection = AgoraRtcConnection()
+ rtcConnection.channelId = apiConfig?.chorusChannelName ?? ""
+ rtcConnection.localUid = UInt(apiConfig?.musicStreamUid ?? 0)
+ mpkConnection = rtcConnection
+
+ // 加入演唱频道
+ let delegate = NSObject()
+ let ret = apiConfig?.engine?.joinChannelEx(byToken: apiConfig?.musicChannelToken, connection: mpkConnection ?? AgoraRtcConnection(), delegate: nil, mediaOptions: options)
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: mpkConnection ?? AgoraRtcConnection())
+
+
+ case .coSinger:
+ // 防止主唱和合唱听见mpk流的声音
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(apiConfig?.musicStreamUid ?? 0), mute: true, connection: singChannelConnection ?? AgoraRtcConnection())
+
+ // 更新音频配置
+ apiConfig?.engine?.setAudioScenario(.chorus)
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+
+ // 预加载歌曲成功
+ // 导唱
+ mediaPlayer?.setPlayerOption("enable_multi_audio_track", value: 1)
+ if apiConfig?.musicType == .mcc {
+ (mediaPlayer as? AgoraMusicPlayerProtocol)?.openMedia(songCode: self.songCode , startPos: 0) // TODO open failed
+ } else {
+ mediaPlayer?.open(songUrl, startPos: 0) // TODO open failed
+ }
+ default:
+ agoraPrintError("JoinChorus with Wrong role: \(singerRole)")
+ }
+
+
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(apiConfig?.musicStreamUid ?? 0), mute: true, connection: singChannelConnection ?? AgoraRtcConnection())
+ // 加入演唱频道后,创建data stream
+ renewInnerDataStreamId()
+ }
+
+ func leaveChorus2(role: KTVSingRole) {
+ agoraPrint("leaveChorus: \(role)")
+ switch role {
+ case .leadSinger:
+ apiConfig?.engine?.leaveChannelEx(mpkConnection ?? AgoraRtcConnection())
+ case .coSinger:
+ mediaPlayer?.stop()
+
+ // 更新音频配置
+ apiConfig?.engine?.setAudioScenario(.gameStreaming)
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ default:
+ agoraPrint("JoinChorus with wrong role: \(singerRole)")
+ }
+ apiConfig?.engine?.leaveChannelEx(singChannelConnection ?? AgoraRtcConnection())
+ }
+
+}
+
+// rtc的代理回调
+extension KTVGiantChorusApiImpl: AgoraRtcEngineDelegate {
+
+ public func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) {
+ agoraPrint("didJoinChannel channel:\(channel) uid: \(uid)")
+ if joinChorusNewRole == .leadSinger {
+ mainSingerHasJoinChannelEx = true
+ onJoinExChannelCallBack?(true, nil)
+ }
+ if joinChorusNewRole == .coSinger {
+ self.onJoinExChannelCallBack?(true, nil)
+ }
+ if let subChorusConnection = subChorusConnection {
+ apiConfig?.engine?.enableAudioVolumeIndicationEx(50, smooth: 10, reportVad: true, connection: subChorusConnection)
+ }
+ }
+
+ public func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
+ if errorCode != .joinChannelRejected {return}
+ agoraPrintError("join ex channel failed")
+ engine.setAudioScenario(.gameStreaming)
+ if joinChorusNewRole == .leadSinger {
+ mainSingerHasJoinChannelEx = false
+ onJoinExChannelCallBack?(false, .joinChannelFail)
+ }
+
+ if joinChorusNewRole == .coSinger {
+ self.onJoinExChannelCallBack?(false, .joinChannelFail)
+ }
+ }
+
+ //合唱频道的声音回调
+ public func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) {
+ getEventHander { delegate in
+ delegate.onChorusChannelAudioVolumeIndication(speakers: speakers, totalVolume: totalVolume)
+ }
+ didKTVAPIReceiveAudioVolumeIndication(with: speakers, totalVolume: totalVolume)
+ }
+
+ public func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) {
+ didKTVAPIReceiveStreamMessageFrom(uid: NSInteger(uid), streamId: streamId, data: data)
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, audioMetadataReceived uid: UInt, metadata: Data) {
+ guard let time: LrcTime = try? LrcTime(serializedData: metadata) else {return}
+ if time.type == .lrcTime && self.singerRole == .audience {
+ self.setProgress(with: Int(time.ts))
+ }
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
+ guard let musicId = apiConfig?.musicStreamUid,let mainSingerId = songConfig?.mainSingerUid else {return}
+ if uid != musicId && subScribeSingerMap.count < 8 {
+ apiConfig?.engine?.muteRemoteAudioStreamEx(uid, mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ if uid != mainSingerId {
+ subScribeSingerMap[Int(uid)] = 0
+ }
+ } else if uid != musicId && subScribeSingerMap.count == 8 {
+ apiConfig?.engine?.muteRemoteAudioStreamEx(uid, mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ if uid != musicId && uid != mainSingerId {
+ singerList.append(Int(uid))
+ }
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) {
+ subScribeSingerMap.removeAll()
+ singerList.removeAll()
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
+ subScribeSingerMap.removeValue(forKey: Int(uid))
+ if let index = singerList.firstIndex(of: Int(uid)) {
+ singerList.remove(at: index)
+ }
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) {
+ guard let musicId = apiConfig?.musicStreamUid,let mainSingerId = songConfig?.mainSingerUid else {return}
+ if apiConfig?.routeSelectionConfig.type == .random || apiConfig?.routeSelectionConfig.type == .topN { return }
+ let uid = stats.uid
+ if uid == mainSingerId {
+ mainSingerDelay = stats.e2eDelay
+ }
+ if uid != mainSingerId && uid != musicId && subScribeSingerMap[Int(uid)] != nil {
+ subScribeSingerMap[Int(uid)] = stats.e2eDelay
+ }
+ }
+}
+
+//需要外部转发的方法 主要是dataStream相关的
+extension KTVGiantChorusApiImpl {
+
+ @objc func didAudioPublishStateChange(newState: AgoraStreamPublishState) {
+ self.isPublishAudio = newState == .published
+ enableProfessionalStreamerMode(self.enableProfessional)
+ agoraPrint("PublishStateChange:\(newState)")
+ }
+
+ @objc func didAudioRouteChanged( routing: AgoraAudioOutputRouting) {
+ agoraPrint("Route changed:\(routing)")
+ self.audioRouting = routing.rawValue
+ let headPhones: [AgoraAudioOutputRouting] = [.headset, .bluetoothDeviceHfp, .bluetoothDeviceA2dp, .headsetNoMic]
+ let wearHeadPhone: Bool = headPhones.contains(routing)
+ if wearHeadPhone == self.isWearingHeadPhones {
+ return
+ }
+ self.isWearingHeadPhones = wearHeadPhone
+ enableProfessionalStreamerMode(self.enableProfessional)
+ }
+
+ @objc public func didKTVAPIReceiveStreamMessageFrom(uid: NSInteger, streamId: NSInteger, data: Data) {
+ let role = singerRole
+ if isRelease {return}
+ guard let dict = dataToDictionary(data: data), let cmd = dict["cmd"] as? String else { return }
+ agoraPrint("recv dict:\(dict)")
+ switch cmd {
+ case "setLrcTime":
+ handleSetLrcTimeCommand(dict: dict, role: role)
+ case "PlayerState":
+ handlePlayerStateCommand(dict: dict, role: role)
+ case "setVoicePitch":
+ handleSetVoicePitchCommand(dict: dict, role: role)
+ default:
+ break
+ }
+ }
+
+ private func handleSetLrcTimeCommand(dict: [String: Any], role: KTVSingRole) {
+ guard let position = dict["time"] as? Int64,
+ let duration = dict["duration"] as? Int64,
+ let realPosition = dict["realTime"] as? Int64,
+ // let songCode = dict["songCode"] as? Int64,
+ let mainSingerState = dict["playerState"] as? Int,
+ let ntpTime = dict["ntp"] as? Int,
+ let songId = dict["songIdentifier"] as? String
+ else { return }
+ #if DUBUG
+ print("realTime:\(realPosition) position:\(position) lastNtpTime:\(lastNtpTime) ntpTime:\(ntpTime) ntpGap:\(ntpTime - self.lastNtpTime) ")
+ #endif
+ //如果接收到的歌曲和自己本地的歌曲不一致就不更新进度
+// guard songCode == self.songCode else {
+// agoraPrint("local songCode[\(songCode)] is not equal to recv songCode[\(self.songCode)] role: \(singerRole.rawValue)")
+// return
+// }
+
+ self.lastNtpTime = ntpTime
+ self.remotePlayerDuration = TimeInterval(duration)
+
+ let state = AgoraMediaPlayerState(rawValue: mainSingerState) ?? .stopped
+// self.lastMainSingerUpdateTime = Date().milListamp
+// self.remotePlayerPosition = TimeInterval(realPosition)
+ if self.playerState != state {
+ #if DUBUG
+ print("[setLrcTime] recv state: \(self.playerState.rawValue)->\(state.rawValue) role: \(singerRole.rawValue) role: \(singerRole.rawValue)")
+ #endif
+ if state == .playing, singerRole == .coSinger, playerState == .openCompleted {
+ //如果是伴唱等待主唱开始播放,seek 到指定位置开始播放保证歌词显示位置准确
+ self.localPlayerPosition = self.lastMainSingerUpdateTime - Double(position)
+ print("localPlayerPosition:playerKit:handleSetLrcTimeCommand \(localPlayerPosition)")
+ agoraPrint("seek toPosition: \(position)")
+ mediaPlayer?.seek(toPosition: Int(position))
+ }
+
+ syncPlayStateFromRemote(state: state, needDisplay: false)
+ }
+
+ if role == .coSinger {
+ self.lastMainSingerUpdateTime = Date().milListamp
+ self.remotePlayerPosition = TimeInterval(realPosition)
+ handleCoSingerRole(dict: dict)
+ } else if role == .audience {
+ if dict.keys.contains("ver") {
+ recvFromDataStream = false
+ } else {
+ recvFromDataStream = true
+ if self.songIdentifier == songId {
+ self.lastMainSingerUpdateTime = Date().milListamp
+ self.remotePlayerPosition = TimeInterval(realPosition)
+ } else {
+ self.lastMainSingerUpdateTime = 0
+ self.remotePlayerPosition = 0
+ }
+ handleAudienceRole(dict: dict)
+ }
+ }
+ }
+
+ private func handlePlayerStateCommand(dict: [String: Any], role: KTVSingRole) {
+ let mainSingerState: Int = dict["state"] as? Int ?? 0
+ let state = AgoraMediaPlayerState(rawValue: mainSingerState) ?? .idle
+
+// if state == .playing, singerRole == .coSinger, playerState == .openCompleted {
+// //如果是伴唱等待主唱开始播放,seek 到指定位置开始播放保证歌词显示位置准确
+// self.localPlayerPosition = getPlayerCurrentTime()
+// print("localPlayerPosition:playerKit:handlePlayerStateCommand \(localPlayerPosition)")
+// agoraPrint("seek toPosition: \(self.localPlayerPosition)")
+// mediaPlayer?.seek(toPosition: Int(self.localPlayerPosition))
+// }
+
+ agoraPrint("recv state with MainSinger: \(state.rawValue)")
+ syncPlayStateFromRemote(state: state, needDisplay: true)
+ }
+
+ private func handleSetVoicePitchCommand(dict: [String: Any], role: KTVSingRole) {
+ if role == .audience, let voicePitch = dict["pitch"] as? Double {
+ self.pitch = voicePitch
+ }
+ }
+
+ private func handleCoSingerRole(dict: [String: Any]) {
+
+ if mediaPlayer?.getPlayerState() == .playing {
+ let localNtpTime = getNtpTimeInMs()
+ let localPosition = localNtpTime - Int(localPlayerSystemTime) + localPosition
+ let expectPosition = Int(dict["time"] as? Int64 ?? 0) + localNtpTime - Int(dict["ntp"] as? Int64 ?? 0) + self.audioPlayoutDelay
+ let threshold = expectPosition - Int(localPosition)
+ let ntpTime = dict["ntp"] as? Int ?? 0
+ let time = dict["time"] as? Int64 ?? 0
+ #if DUBUG
+ agoraPrint("checkNtp, diff:\(threshold), localNtp:\(getNtpTimeInMs()), localPosition:\(localPosition), audioPlayoutDelay:\(audioPlayoutDelay), remoteDiff:\(String(describing: ntpTime - Int(time)))")
+ #endif
+ if abs(threshold) > 50 {
+ agoraPrint("need seek, time:\(threshold)")
+ mediaPlayer?.seek(toPosition: expectPosition)
+ }
+ }
+
+ }
+
+ private func handleAudienceRole(dict: [String: Any]) {
+ // do something for audience role
+ guard let position = dict["time"] as? Int64,
+ let duration = dict["duration"] as? Int64,
+ let realPosition = dict["realTime"] as? Int64,
+ let songCode = dict["songCode"] as? Int64,
+ let mainSingerState = dict["playerState"] as? Int
+ else { return }
+ }
+
+ @objc public func didKTVAPIReceiveAudioVolumeIndication(with speakers: [AgoraRtcAudioVolumeInfo], totalVolume: NSInteger) {
+ if playerState != .playing {return}
+ if singerRole == .audience {return}
+
+ guard var pitch: Double = speakers.first?.voicePitch else {return}
+ pitch = isNowMicMuted ? 0 : pitch
+ //如果mpk不是playing状态 pitch = 0
+ if mediaPlayer?.getPlayerState() != .playing {pitch = 0}
+ self.pitch = pitch
+ //将主唱的pitch同步到观众
+// if isMainSinger() {
+// let dict: [String: Any] = [ "cmd": "setVoicePitch",
+// "pitch": pitch,
+// ]
+// sendStreamMessageWithDict(dict, success: nil)
+// }
+ }
+
+ @objc public func didKTVAPILocalAudioStats(stats: AgoraRtcLocalAudioStats) {
+ if useCustomAudioSource == true {return}
+ audioPlayoutDelay = Int(stats.audioPlayoutDelay)
+ }
+
+ @objc func didAudioMetadataReceived( uid: UInt, metadata: Data) {
+ guard let time: LrcTime = try? LrcTime(serializedData: metadata) else {return}
+ if time.type == .lrcTime && self.singerRole == .audience {
+ self.setProgress(with: Int(time.ts))
+ }
+ }
+
+}
+
+//private method
+extension KTVGiantChorusApiImpl {
+
+ private func initTimer() {
+
+ guard timer == nil else { return }
+
+ timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: {[weak self] timer in
+ guard let self = self else {
+ timer.invalidate()
+ return
+ }
+
+ var current = self.getPlayerCurrentTime()
+ if self.singerRole == .audience && (Date().milListamp - (self.lastMainSingerUpdateTime )) > 1000 {
+ return
+ }
+
+ if self.singerRole != .audience && (Date().milListamp - (self.lastReceivedPosition )) > 1000 {
+ return
+ }
+
+ if self.oldPitch == self.pitch && (self.oldPitch != 0 && self.pitch != 0) {
+ self.pitch = -1
+ }
+
+ if self.singerRole != .audience {
+ current = Date().milListamp - self.lastReceivedPosition + Double(self.localPosition)
+ }
+ if self.singerRole == .audience && !recvFromDataStream {
+
+ } else {
+ if self.singerRole != .audience {
+ current = Date().milListamp - self.lastReceivedPosition + Double(self.localPosition)
+ if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
+ var time: LrcTime = LrcTime()
+ time.forward = true
+ time.ts = Int64(current) + Int64(self.startHighTime)
+ time.songID = songIdentifier
+ time.type = .lrcTime
+ //大合唱的uid是musicuid
+ time.uid = Int32(Int(apiConfig?.musicStreamUid ?? 0))
+ sendMetaMsg(with: time)
+ }
+ }
+ self.setProgress(with: Int(current) + Int(self.startHighTime))
+ }
+ self.oldPitch = self.pitch
+ })
+ }
+
+ private func setPlayerState(with state: AgoraMediaPlayerState) {
+ playerState = state
+ updateRemotePlayBackVolumeIfNeed()
+ updateTimer(with: state)
+ }
+
+ private func updateRemotePlayBackVolumeIfNeed() {
+ let role = singerRole
+ if role == .audience {
+ apiConfig?.engine?.adjustPlaybackSignalVolume(100)
+ return
+ }
+
+ let vol = self.playerState == .playing ? remoteVolume : 100
+ apiConfig?.engine?.adjustPlaybackSignalVolume(Int(vol))
+ }
+
+ private func updateTimer(with state: AgoraMediaPlayerState) {
+ DispatchQueue.main.async {
+ if state == .paused || state == .stopped {
+ self.pauseTimer()
+ } else if state == .playing {
+ self.startTimer()
+ }
+ }
+ }
+
+ //timer method
+ private func startTimer() {
+ guard let timer = self.timer else {return}
+ if isPause == false {
+ RunLoop.current.add(timer, forMode: .common)
+ self.timer?.fire()
+ } else {
+ resumeTimer()
+ }
+ }
+
+ private func resumeTimer() {
+ if isPause == false {return}
+ isPause = false
+ timer?.fireDate = Date()
+ }
+
+ private func pauseTimer() {
+ if isPause == true {return}
+ isPause = true
+ timer?.fireDate = Date.distantFuture
+ }
+
+ private func freeTimer() {
+ guard let _ = self.timer else {return}
+ self.timer?.invalidate()
+ self.timer = nil
+ }
+
+ private func getPlayerCurrentTime() -> TimeInterval {
+ let role = singerRole
+ if role == .soloSinger || role == .leadSinger{
+ let time = Date().milListamp - localPlayerPosition
+ return time
+ } else if role == .coSinger {
+ if playerState == .playing || playerState == .paused {
+ let time = Date().milListamp - localPlayerPosition
+ return time
+ }
+ }
+
+ var position = Date().milListamp - self.lastMainSingerUpdateTime + remotePlayerPosition
+ if playerState != .playing {
+ position = remotePlayerPosition
+ }
+ return position
+ }
+
+ private func syncPlayStateFromRemote(state: AgoraMediaPlayerState, needDisplay: Bool) {
+ let role = singerRole
+ if role == .coSinger {
+ if state == .stopped {
+ // stopSing()
+ } else if state == .paused {
+ pausePlay()
+ } else if state == .playing {
+ resumeSing()
+ } else if (state == .playBackAllLoopsCompleted && needDisplay == true) {
+ getEventHander { delegate in
+ delegate.onMusicPlayerStateChanged(state: state, reason: .none, isLocal: true)
+ }
+ }
+ } else {
+ self.playerState = state
+ getEventHander { delegate in
+ delegate.onMusicPlayerStateChanged(state: self.playerState, reason: .none, isLocal: false)
+ }
+ }
+ }
+
+ private func pausePlay() {
+ mediaPlayer?.pause()
+ }
+
+ private func dataToDictionary(data: Data) -> [String: Any]? {
+ do {
+ let json = try JSONSerialization.jsonObject(with: data, options: [])
+ return json as? [String: Any]
+ } catch {
+ print("Error decoding data: (error.localizedDescription)")
+ return nil
+ }
+ }
+
+ private func compactDictionaryToData(_ dict: [String: Any]) -> Data? {
+ do {
+ let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
+ return jsonData
+ } catch {
+ print("Error encoding data: (error.localizedDescription)")
+ return nil
+ }
+ }
+
+ private func getNtpTimeInMs() -> Int {
+ var localNtpTime: Int = Int(apiConfig?.engine?.getNtpWallTimeInMs() ?? 0)
+
+ if localNtpTime != 0 {
+ localNtpTime = localNtpTime + 2208988800 * 1000
+ }
+
+ return localNtpTime
+ }
+
+ private func syncPlayState(state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
+ let dict: [String: Any] = ["cmd": "PlayerState", "userId": apiConfig?.localUid as Any, "state": state.rawValue, "reason": "\(reason.rawValue)"]
+ sendStreamMessageWithDict(dict, success: nil)
+ }
+
+// private func sendCustomMessage(with event: String, label: String) {
+// apiConfig?.engine?.sendCustomReportMessage(messageId, category: version, event: event, label: label, value: 0)
+// apiRepoter?.reportFuncEvent(name: event, value: <#T##[String : Any]#>, ext: <#T##[String : Any]#>)
+// }
+
+ private func sendCustomMessage(with event: String, dict: [String: Any]) {
+ apiRepoter?.reportFuncEvent(name: event, value: dict, ext: [:])
+ }
+
+ private func sendStreamMessageWithDict(_ dict: [String: Any], success: ((_ success: Bool) -> Void)?) {
+ let messageData = compactDictionaryToData(dict as [String: Any])
+ let code = apiConfig?.engine?.sendStreamMessageEx(dataStreamId, data: messageData ?? Data(), connection: singChannelConnection ?? AgoraRtcConnection())
+ if code == 0 && success != nil { success!(true) }
+ if code != 0 {
+ print("sendStreamMessage fail: \(String(describing: code))")
+ }
+ }
+
+ private func syncPlayState(_ state: AgoraMediaPlayerState) {
+ let dict: [String: Any] = [ "cmd": "PlayerState", "userId": apiConfig?.localUid as Any, "state": "\(state.rawValue)" ]
+ sendStreamMessageWithDict(dict, success: nil)
+ }
+
+ private func setProgress(with pos: Int) {
+ lrcControl?.onUpdatePitch(pitch: Float(self.pitch))
+ lrcControl?.onUpdateProgress(progress: pos > 200 ? pos - 200 : pos)
+ }
+
+ private func sendMetaMsg(with time: LrcTime) {
+ let data: Data? = try? time.serializedData()
+ let code = apiConfig?.engine?.sendAudioMetadataEx(mpkConnection ?? AgoraRtcConnection(), metadata: data ?? Data())
+ if code != 0 {
+ // agoraPrint("sendStreamMessage fail: \(String(describing: code))")
+ }
+ }
+}
+
+//主要是MPK的回调
+extension KTVGiantChorusApiImpl: AgoraRtcMediaPlayerDelegate {
+
+ func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol, didChangedTo position_ms: Int, atTimestamp timestamp_ms: TimeInterval) {
+ self.lastReceivedPosition = Date().milListamp
+ self.localPosition = Int(position_ms)
+ self.localPlayerSystemTime = timestamp_ms
+ self.localPlayerPosition = Date().milListamp - Double(position_ms)
+ if isMainSinger() && getPlayerCurrentTime() > TimeInterval(self.audioPlayoutDelay) {
+ let dict: [String: Any] = [ "cmd": "setLrcTime",
+ "duration": self.playerDuration,
+ "time": position_ms - audioPlayoutDelay,
+ "realTime":position_ms,
+ "ntp": timestamp_ms,
+ "playerState": self.playerState.rawValue,
+ "songIdentifier": songIdentifier,
+ "forward": true,
+ "ver":2,
+ ]
+ #if DEBUG
+ print("position_ms:\(position_ms), ntp:\(getNtpTimeInMs()), delta:\(self.getNtpTimeInMs() - position_ms), autoPlayoutDelay:\(self.audioPlayoutDelay), state:\(self.playerState.rawValue)")
+ #endif
+ sendStreamMessageWithDict(dict, success: nil)
+ }
+ }
+
+ func AgoraRtcMediaPlayer(_ playerKit: any AgoraRtcMediaPlayerProtocol, didChangedTo state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
+ agoraPrint("agoraRtcMediaPlayer didChangedToState: \(state.rawValue) \(self.songCode)")
+ if isRelease {return}
+ self.playerState = state
+ if state == .openCompleted {
+ self.localPlayerPosition = Date().milListamp
+ self.playerDuration = TimeInterval(mediaPlayer?.getDuration() ?? 0)
+ playerKit.selectMultiAudioTrack(1, publishTrackIndex: 1)
+ if isMainSinger() { //主唱播放,通过同步消息“setLrcTime”通知伴唱play
+ playerKit.play()
+ }
+ self.startProcessDelay()
+ } else if state == .stopped {
+ apiConfig?.engine?.adjustPlaybackSignalVolume(100)
+ self.localPlayerPosition = Date().milListamp
+ self.playerDuration = 0
+ }
+ else if state == .paused {
+ apiConfig?.engine?.adjustPlaybackSignalVolume(100)
+ } else if state == .playing {
+ apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
+ self.localPlayerPosition = Date().milListamp - Double(mediaPlayer?.getPosition() ?? 0)
+ } else if state == .stopped {
+ self.stopProcessDelay()
+ }
+
+ if isMainSinger() {
+ syncPlayState(state: state, reason: reason)
+ }
+ agoraPrint("recv state with player callback : \(state.rawValue)")
+ if state == .playBackAllLoopsCompleted && singerRole == .coSinger {//可能存在伴唱不返回allloopbackComplete状态 这个状态通过主唱的playerState来同步
+ return
+ }
+ getEventHander { delegate in
+ delegate.onMusicPlayerStateChanged(state: state, reason: .none, isLocal: true)
+ }
+ }
+
+ private func isMainSinger() -> Bool {
+ return singerRole == .soloSinger || singerRole == .leadSinger
+ }
+}
+
+//主要是MCC的回调
+extension KTVGiantChorusApiImpl: AgoraMusicContentCenterEventDelegate {
+
+ func onSongSimpleInfoResult(_ requestId: String, songCode: Int, simpleInfo: String?, reason: AgoraMusicContentCenterStateReason) {
+ if let jsonData = simpleInfo?.data(using: .utf8) {
+ do {
+ let jsonMsg = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any]
+ let format = jsonMsg["format"] as! [String: Any]
+ let highPart = format["highPart"] as! [[String: Any]]
+ let highStartTime = highPart[0]["highStartTime"] as! Int
+ let highEndTime = highPart[0]["highEndTime"] as! Int
+ let time = highStartTime
+ startHighTime = time
+ self.lrcControl?.onHighPartTime(highStartTime: highStartTime, highEndTime: highEndTime)
+ } catch {
+ agoraPrintError("Error while parsing JSON: \(error.localizedDescription)")
+ }
+ }
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ }
+
+ func onMusicChartsResult(_ requestId: String, result: [AgoraMusicChartInfo], reason: AgoraMusicContentCenterStateReason) {
+ guard let callback = musicChartDict[requestId] else {return}
+ callback(requestId, reason, result)
+ musicChartDict.removeValue(forKey: requestId)
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ }
+
+ func onMusicCollectionResult(_ requestId: String, result: AgoraMusicCollection, reason: AgoraMusicContentCenterStateReason) {
+ guard let callback = musicSearchDict[requestId] else {return}
+ callback(requestId, reason, result)
+ musicSearchDict.removeValue(forKey: requestId)
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ }
+
+ func onLyricResult(_ requestId: String, songCode: Int, lyricUrl: String?, reason: AgoraMusicContentCenterStateReason) {
+ guard let lrcUrl = lyricUrl else {return}
+ let callback = self.lyricCallbacks[requestId]
+ guard let lyricCallback = callback else { return }
+ self.lyricCallbacks.removeValue(forKey: requestId)
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ if lrcUrl.isEmpty {
+ lyricCallback(nil)
+ return
+ }
+ lyricCallback(lrcUrl)
+ }
+
+ func onPreLoadEvent(_ requestId: String, songCode: Int, percent: Int, lyricUrl: String?, state: AgoraMusicContentCenterPreloadState, reason: AgoraMusicContentCenterStateReason) {
+ if let listener = self.loadMusicListeners.object(forKey: "\(songCode)" as NSString) as? IMusicLoadStateListener {
+ listener.onMusicLoadProgress(songCode: songCode, percent: percent, state: state, msg: String(reason.rawValue), lyricUrl: lyricUrl)
+ }
+ if (state == .preloading) { return }
+ let SongCode = "\(songCode)"
+ guard let block = self.musicCallbacks[SongCode] else { return }
+ self.musicCallbacks.removeValue(forKey: SongCode)
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ block(state, songCode)
+ }
+
+}
+
+extension KTVGiantChorusApiImpl {
+
+ private func sendSyncPitch(_ pitch: Double) {
+ var msg: [String:Any] = [:]
+ msg["cmd"] = "setVoicePitch"
+ msg["pitch"] = pitch
+ sendStreamMessageWithDict(msg) { _ in
+
+ }
+ }
+
+ private func startSyncPitch() {
+ print("startSyncPitch")
+ mStopSyncPitch = false
+ let queue = DispatchQueue(label: "com.example.syncpitch")
+ mSyncPitchTimer = DispatchSource.makeTimerSource(queue: queue)
+ mSyncPitchTimer?.schedule(deadline: .now(), repeating: .milliseconds(50))
+ mSyncPitchTimer?.setEventHandler { [weak self] in
+ guard let self = self else { return }
+ if !self.mStopSyncPitch &&
+ playerState == .playing &&
+ (singerRole == .leadSinger || singerRole == .soloSinger) {
+ self.sendSyncPitch(pitch)
+ }
+ }
+ mSyncPitchTimer?.resume()
+ }
+
+ private func stopSyncPitch() {
+ print("stopSyncPitch")
+ mStopSyncPitch = true
+ pitch = 0.0
+
+ mSyncPitchTimer?.cancel()
+ mSyncPitchTimer = nil
+ }
+
+ private func sendSyncScore() {
+ print("sendSyncScore")
+ var dictionary: [String: Any] = [:]
+ dictionary["service"] = "audio_smart_mixer"
+ dictionary["version"] = "V1"
+ var payload: [String: Any] = [:]
+ payload["cname"] = apiConfig?.chorusChannelName
+ payload["uid"] = String(apiConfig?.localUid ?? 0)
+ payload["uLv"] = -1
+ payload["specialLabel"] = 0
+ payload["audioRoute"] = audioRouting
+ payload["vocalScore"] = singingScore
+ dictionary["payload"] = payload
+ sendStreamMessageWithDict(dictionary) { _ in
+
+ }
+ }
+
+ private func startSyncScore() {
+ print("startSyncScore")
+ mStopSyncScore = false
+ let queue = DispatchQueue(label: "com.example.syncscore")
+ mSyncScoreTimer = DispatchSource.makeTimerSource(queue: queue)
+ mSyncScoreTimer?.schedule(deadline: .now(), repeating: .milliseconds(3000))
+ mSyncScoreTimer?.setEventHandler { [weak self] in
+ guard let self = self else { return }
+ if !self.mStopSyncScore &&
+ playerState == .playing &&
+ (singerRole == .leadSinger || singerRole == .coSinger) {
+ self.sendSyncScore()
+ }
+ }
+ mSyncScoreTimer?.resume()
+ }
+
+ private func stopSyncScore() {
+ print("stopSyncScore")
+ mStopSyncScore = true
+ singingScore = 0
+
+ mSyncScoreTimer?.cancel()
+ mSyncScoreTimer = nil
+ }
+
+ // -1: unknown,0:非K歌状态,1:K歌播放状态,2:K歌暂停状态)
+ private func getCloudConvergenceStatus() -> Int {
+ var status = -1
+ switch playerState {
+ case .playing:
+ status = 1
+ case .paused:
+ status = 2
+ default:
+ break
+ }
+ return status
+ }
+
+ private func sendSyncCloudConvergenceStatus() {
+ print("sendSyncCloudConvergenceStatus")
+ var dictionary: [String: Any] = [:]
+ dictionary["service"] = "audio_smart_mixer_status"
+ dictionary["version"] = "V1"
+ var payload: [String: Any] = [:]
+ payload["Ts"] = getNtpTimeInMs()
+ payload["cname"] = apiConfig?.chorusChannelName
+ payload["status"] = getCloudConvergenceStatus()
+ payload["bgmUID"] = mpkConnection?.localUid
+ payload["leadsingerUID"] = String(songConfig?.mainSingerUid ?? 0)
+ dictionary["payload"] = payload
+ sendStreamMessageWithDict(dictionary) { _ in
+
+ }
+ }
+
+ private func startSyncCloudConvergenceStatus() {
+ print("startSyncCloudConvergenceStatus")
+ mStopSyncCloudConvergenceStatus = false
+ let queue = DispatchQueue(label: "com.example.synccloudconvergencestatus")
+ mSyncCloudConvergenceStatusTimer = DispatchSource.makeTimerSource(queue: queue)
+ mSyncCloudConvergenceStatusTimer?.schedule(deadline: .now(), repeating: .milliseconds(200))
+ mSyncCloudConvergenceStatusTimer?.setEventHandler { [weak self] in
+ guard let self = self else { return }
+ if !self.mStopSyncCloudConvergenceStatus &&
+ singerRole == .leadSinger {
+ self.sendSyncCloudConvergenceStatus()
+ }
+ }
+ mSyncCloudConvergenceStatusTimer?.resume()
+ }
+
+ private func stopSyncCloudConvergenceStatus() {
+ print("stopSyncCloudConvergenceStatus")
+ mStopSyncCloudConvergenceStatus = true
+
+ mSyncCloudConvergenceStatusTimer?.cancel()
+ mSyncCloudConvergenceStatusTimer = nil
+ }
+
+}
+
+extension KTVGiantChorusApiImpl {
+
+ private func processDelayTask() {
+ if !mStopProcessDelay && singerRole != .audience {
+ let n = singerRole == .leadSinger ? apiConfig?.routeSelectionConfig.streamNum : (apiConfig?.routeSelectionConfig.streamNum ?? 1) - 1
+ let sortedEntries = subScribeSingerMap.sorted(by: { $0.value < $1.value })
+ let other = Array(sortedEntries.dropFirst(3))
+ var drop = [Int]()
+
+ if n ?? 3 > 3 {
+ for (uid, _) in other.dropLast(n! - 3) {
+ drop.append(uid)
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(uid), mute: true, connection: singChannelConnection ?? AgoraRtcConnection())
+ subScribeSingerMap.removeValue(forKey: uid)
+ }
+ }
+
+ agoraPrint("选路重新订阅, drop:\(drop)")
+
+ let filteredList = singerList.filter { !subScribeSingerMap.keys.contains($0) }
+ let filteredList2 = filteredList.filter { !drop.contains($0) }
+ let shuffledList = filteredList2.shuffled()
+
+ if subScribeSingerMap.count < 8 {
+ let randomSingers = Array(shuffledList.prefix(8 - subScribeSingerMap.count))
+ agoraPrintError("选路重新订阅, newSingers:\(randomSingers)")
+
+ for singer in randomSingers {
+ subScribeSingerMap[singer] = 0
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(singer), mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ }
+
+ agoraPrint("选路重新订阅, newSubScribeSingerMap:\(subScribeSingerMap)")
+ }
+ }
+
+ private func processSubscribeTask() {
+ if !mStopProcessDelay && singerRole != .audience {
+ let n = singerRole == .leadSinger ? apiConfig?.routeSelectionConfig.streamNum : (apiConfig?.routeSelectionConfig.streamNum ?? 0) - 1
+ let sortedEntries = subScribeSingerMap.sorted(by: { $0.value < $1.value })
+ let mustToHave = Array(sortedEntries.prefix(3))
+
+ for (uid, _) in mustToHave {
+ apiConfig?.engine?.adjustUserPlaybackSignalVolumeEx(UInt(uid), volume: 100, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+
+ let other = Array(sortedEntries.dropFirst(3))
+
+ if n ?? 3 > 3 {
+ for (uid, delay) in Array(other.prefix(n! - 3)) {
+ if delay > 300 {
+ apiConfig?.engine?.adjustUserPlaybackSignalVolumeEx(UInt(uid), volume: 0, connection: singChannelConnection ?? AgoraRtcConnection())
+ } else {
+ apiConfig?.engine?.adjustUserPlaybackSignalVolumeEx(UInt(uid), volume: 100, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ }
+
+ for (uid, _) in Array(other.dropFirst(n! - 3)) {
+ apiConfig?.engine?.adjustUserPlaybackSignalVolumeEx(UInt(uid), volume: 0, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ }
+
+ agoraPrint("选路排序+调整播放音量, mustToHave:\(mustToHave), other:\(other)")
+ }
+ }
+
+ private func startProcessDelay() {
+ guard apiConfig?.routeSelectionConfig.type != .topN && apiConfig?.routeSelectionConfig.type != .random else { return }
+
+ mStopProcessDelay = false
+
+ // 创建并配置 processDelayTimer
+ processDelayFuture = DispatchSource.makeTimerSource()
+ processDelayFuture?.schedule(deadline: .now() + .seconds(10), repeating: .seconds(20))
+ processDelayFuture?.setEventHandler { [weak self] in
+ // 执行 mProcessDelayTask
+ self?.processDelayTask()
+ }
+ processDelayFuture?.resume()
+
+ // 创建并配置 processSubscribeTimer
+ processSubscribeFuture = DispatchSource.makeTimerSource()
+ processSubscribeFuture?.schedule(deadline: .now() + .seconds(15), repeating: .seconds(20))
+ processSubscribeFuture?.setEventHandler { [weak self] in
+ // 执行 mProcessSubscribeTask
+ self?.processSubscribeTask()
+ }
+ processSubscribeFuture?.resume()
+ }
+
+ private func stopProcessDelay() {
+ mStopProcessDelay = true
+
+ processDelayFuture?.cancel()
+ processDelayFuture = nil
+ processSubscribeFuture?.cancel()
+ processSubscribeFuture = nil
+ }
+}
+
+
+extension Date {
+ /// 获取当前 秒级 时间戳 - 10位
+ ///
+ var timeStamp : TimeInterval {
+ let timeInterval: TimeInterval = self.timeIntervalSince1970
+ return timeInterval
+ }
+ /// 获取当前 毫秒级 时间戳 - 13位
+ var milListamp : TimeInterval {
+ let timeInterval: TimeInterval = self.timeIntervalSince1970
+ let millisecond = CLongLong(round(timeInterval*1000))
+ return TimeInterval(millisecond)
+ }
+}
+
diff --git a/KTVAPI/iOS/Classes/LrcTime.pb.swift b/KTVAPI/iOS/Classes/LrcTime.pb.swift
new file mode 100644
index 0000000..b790fb2
--- /dev/null
+++ b/KTVAPI/iOS/Classes/LrcTime.pb.swift
@@ -0,0 +1,140 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: LrcTime.proto
+//
+// For information on using the generated types, please see the documentation:
+// https://github.com/apple/swift-protobuf/
+
+import Foundation
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+ struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+ typealias Version = _2
+}
+
+enum MsgType: SwiftProtobuf.Enum {
+ typealias RawValue = Int
+ case unknownType // = 0
+ case lrcTime // = 1001
+ case UNRECOGNIZED(Int)
+
+ init() {
+ self = .unknownType
+ }
+
+ init?(rawValue: Int) {
+ switch rawValue {
+ case 0: self = .unknownType
+ case 1001: self = .lrcTime
+ default: self = .UNRECOGNIZED(rawValue)
+ }
+ }
+
+ var rawValue: Int {
+ switch self {
+ case .unknownType: return 0
+ case .lrcTime: return 1001
+ case .UNRECOGNIZED(let i): return i
+ }
+ }
+
+ // The compiler won't synthesize support with the UNRECOGNIZED case.
+ static let allCases: [MsgType] = [
+ .unknownType,
+ .lrcTime,
+ ]
+
+}
+
+struct LrcTime: Sendable {
+ // SwiftProtobuf.Message conformance is added in an extension below. See the
+ // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+ // methods supported on all messages.
+
+ var type: MsgType = .unknownType
+
+ var forward: Bool = false
+
+ var ts: Int64 = 0
+
+ var songID: String = String()
+
+ var uid: Int32 = 0
+
+ var unknownFields = SwiftProtobuf.UnknownStorage()
+
+ init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension MsgType: SwiftProtobuf._ProtoNameProviding {
+ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+ 0: .same(proto: "UNKNOWN_TYPE"),
+ 1001: .same(proto: "LRC_TIME"),
+ ]
+}
+
+extension LrcTime: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+ static let protoMessageName: String = "LrcTime"
+ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+ 1: .same(proto: "type"),
+ 2: .same(proto: "forward"),
+ 3: .same(proto: "ts"),
+ 4: .same(proto: "songId"),
+ 5: .same(proto: "uid"),
+ ]
+
+ mutating func decodeMessage(decoder: inout D) throws {
+ while let fieldNumber = try decoder.nextFieldNumber() {
+ // The use of inline closures is to circumvent an issue where the compiler
+ // allocates stack space for every case branch when no optimizations are
+ // enabled. https://github.com/apple/swift-protobuf/issues/1034
+ switch fieldNumber {
+ case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }()
+ case 2: try { try decoder.decodeSingularBoolField(value: &self.forward) }()
+ case 3: try { try decoder.decodeSingularInt64Field(value: &self.ts) }()
+ case 4: try { try decoder.decodeSingularStringField(value: &self.songID) }()
+ case 5: try { try decoder.decodeSingularInt32Field(value: &self.uid) }()
+ default: break
+ }
+ }
+ }
+
+ func traverse(visitor: inout V) throws {
+ if self.type != .unknownType {
+ try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1)
+ }
+ if self.forward != false {
+ try visitor.visitSingularBoolField(value: self.forward, fieldNumber: 2)
+ }
+ if self.ts != 0 {
+ try visitor.visitSingularInt64Field(value: self.ts, fieldNumber: 3)
+ }
+ if !self.songID.isEmpty {
+ try visitor.visitSingularStringField(value: self.songID, fieldNumber: 4)
+ }
+ if self.uid != 0 {
+ try visitor.visitSingularInt32Field(value: self.uid, fieldNumber: 5)
+ }
+ try unknownFields.traverse(visitor: &visitor)
+ }
+
+ static func ==(lhs: LrcTime, rhs: LrcTime) -> Bool {
+ if lhs.type != rhs.type {return false}
+ if lhs.forward != rhs.forward {return false}
+ if lhs.ts != rhs.ts {return false}
+ if lhs.songID != rhs.songID {return false}
+ if lhs.uid != rhs.uid {return false}
+ if lhs.unknownFields != rhs.unknownFields {return false}
+ return true
+ }
+}
diff --git a/KTVAPI/iOS/Classes/LrcTime.proto b/KTVAPI/iOS/Classes/LrcTime.proto
new file mode 100644
index 0000000..084175b
--- /dev/null
+++ b/KTVAPI/iOS/Classes/LrcTime.proto
@@ -0,0 +1,14 @@
+syntax = "proto3";
+
+enum MsgType {
+ UNKNOWN_TYPE = 0;
+ LRC_TIME = 1001;
+}
+
+message LrcTime {
+ MsgType type = 1;
+ bool forward = 2;
+ int64 ts = 3;
+ string songId = 4;
+ int32 uid = 5;
+}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo.xcodeproj/project.pbxproj b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo.xcodeproj/project.pbxproj
index 39ac526..41ef2c7 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo.xcodeproj/project.pbxproj
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo.xcodeproj/project.pbxproj
@@ -7,7 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
- E3CB4D602A935EBD00322389 /* 成都.xml in Resources */ = {isa = PBXBuildFile; fileRef = E3CB4D5F2A935EBD00322389 /* 成都.xml */; };
+ E38BDE782B6F7A78007A2834 /* KTVGiantChorusApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38BDE772B6F7A77007A2834 /* KTVGiantChorusApiImpl.swift */; };
+ E38BDE7A2B6F7ABD007A2834 /* ApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38BDE792B6F7ABD007A2834 /* ApiManager.swift */; };
+ E39BAF762B6CC695002C692F /* LrcTime.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E39BAF742B6CC695002C692F /* LrcTime.pb.swift */; };
+ E39BAF772B6CC695002C692F /* LrcTime.proto in Sources */ = {isa = PBXBuildFile; fileRef = E39BAF752B6CC695002C692F /* LrcTime.proto */; };
+ E39BAF792B6CCC65002C692F /* 不如跳舞.xml in Resources */ = {isa = PBXBuildFile; fileRef = E39BAF782B6CCC65002C692F /* 不如跳舞.xml */; };
+ E39BAF7B2B6CCC74002C692F /* 不如跳舞.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = E39BAF7A2B6CCC74002C692F /* 不如跳舞.mp4 */; };
+ E3EC073C2BD8BB8600CB8279 /* APIReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EC073B2BD8BB8600CB8279 /* APIReporter.swift */; };
E3ED270B2A822E9D0087B7AA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3ED270A2A822E9D0087B7AA /* AppDelegate.swift */; };
E3ED270D2A822E9D0087B7AA /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3ED270C2A822E9D0087B7AA /* SceneDelegate.swift */; };
E3ED270F2A822E9D0087B7AA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3ED270E2A822E9D0087B7AA /* ViewController.swift */; };
@@ -28,7 +34,6 @@
E3ED27392A8312120087B7AA /* AgoraStringExtention.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3ED27312A8312120087B7AA /* AgoraStringExtention.swift */; };
E3ED273A2A8312120087B7AA /* AgoraURLExtention.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3ED27322A8312120087B7AA /* AgoraURLExtention.swift */; };
E3ED273B2A8312120087B7AA /* AgoraDownLoadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3ED27332A8312120087B7AA /* AgoraDownLoadManager.swift */; };
- E3FE65332B20638D001D6BF9 /* 成都.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = E3FE65322B20638D001D6BF9 /* 成都.mp3 */; };
F33427D772BC45C43FBC8F23 /* Pods_KTVApiDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AFBE8E6CA2314F17EF33468 /* Pods_KTVApiDemo.framework */; };
/* End PBXBuildFile section */
@@ -36,7 +41,13 @@
0AFBE8E6CA2314F17EF33468 /* Pods_KTVApiDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KTVApiDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
91649F2302F7D5A73F303630 /* Pods-KTVApiDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KTVApiDemo.release.xcconfig"; path = "Target Support Files/Pods-KTVApiDemo/Pods-KTVApiDemo.release.xcconfig"; sourceTree = ""; };
AFA8596CCA93FAF74AD1D2D5 /* Pods-KTVApiDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KTVApiDemo.debug.xcconfig"; path = "Target Support Files/Pods-KTVApiDemo/Pods-KTVApiDemo.debug.xcconfig"; sourceTree = ""; };
- E3CB4D5F2A935EBD00322389 /* 成都.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "成都.xml"; sourceTree = ""; };
+ E38BDE772B6F7A77007A2834 /* KTVGiantChorusApiImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KTVGiantChorusApiImpl.swift; sourceTree = ""; };
+ E38BDE792B6F7ABD007A2834 /* ApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiManager.swift; sourceTree = ""; };
+ E39BAF742B6CC695002C692F /* LrcTime.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LrcTime.pb.swift; sourceTree = ""; };
+ E39BAF752B6CC695002C692F /* LrcTime.proto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.protobuf; path = LrcTime.proto; sourceTree = ""; };
+ E39BAF782B6CCC65002C692F /* 不如跳舞.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "不如跳舞.xml"; sourceTree = ""; };
+ E39BAF7A2B6CCC74002C692F /* 不如跳舞.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = "不如跳舞.mp4"; sourceTree = ""; };
+ E3EC073B2BD8BB8600CB8279 /* APIReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIReporter.swift; sourceTree = ""; };
E3ED27072A822E9D0087B7AA /* KTVApiDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KTVApiDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
E3ED270A2A822E9D0087B7AA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
E3ED270C2A822E9D0087B7AA /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
@@ -59,7 +70,6 @@
E3ED27312A8312120087B7AA /* AgoraStringExtention.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraStringExtention.swift; sourceTree = ""; };
E3ED27322A8312120087B7AA /* AgoraURLExtention.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraURLExtention.swift; sourceTree = ""; };
E3ED27332A8312120087B7AA /* AgoraDownLoadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraDownLoadManager.swift; sourceTree = ""; };
- E3FE65322B20638D001D6BF9 /* 成都.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "成都.mp3"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -115,13 +125,14 @@
E3ED27232A8230A00087B7AA /* KeyCenter.swift */,
E3ED270A2A822E9D0087B7AA /* AppDelegate.swift */,
E3ED270C2A822E9D0087B7AA /* SceneDelegate.swift */,
- E3CB4D5F2A935EBD00322389 /* 成都.xml */,
- E3FE65322B20638D001D6BF9 /* 成都.mp3 */,
E3ED270E2A822E9D0087B7AA /* ViewController.swift */,
+ E39BAF782B6CCC65002C692F /* 不如跳舞.xml */,
+ E39BAF7A2B6CCC74002C692F /* 不如跳舞.mp4 */,
E3ED27252A8236750087B7AA /* KTVViewController.swift */,
E3ED27292A826B0D0087B7AA /* KTVLyricView.swift */,
E3ED272B2A8312120087B7AA /* FileDownloadCache */,
E3ED27272A8243480087B7AA /* NetworkManager.swift */,
+ E38BDE792B6F7ABD007A2834 /* ApiManager.swift */,
E3ED271E2A822ED30087B7AA /* KTVAPI */,
E3ED27102A822E9D0087B7AA /* Main.storyboard */,
E3ED27132A822E9E0087B7AA /* Assets.xcassets */,
@@ -134,8 +145,12 @@
E3ED271E2A822ED30087B7AA /* KTVAPI */ = {
isa = PBXGroup;
children = (
+ E3EC073B2BD8BB8600CB8279 /* APIReporter.swift */,
E3ED271F2A822ED30087B7AA /* KTVApiImpl.swift */,
E3ED27202A822ED30087B7AA /* KTVApi.swift */,
+ E38BDE772B6F7A77007A2834 /* KTVGiantChorusApiImpl.swift */,
+ E39BAF742B6CC695002C692F /* LrcTime.pb.swift */,
+ E39BAF752B6CC695002C692F /* LrcTime.proto */,
);
path = KTVAPI;
sourceTree = "";
@@ -217,9 +232,9 @@
files = (
E3ED27172A822E9E0087B7AA /* LaunchScreen.storyboard in Resources */,
E3ED27142A822E9E0087B7AA /* Assets.xcassets in Resources */,
- E3FE65332B20638D001D6BF9 /* 成都.mp3 in Resources */,
- E3CB4D602A935EBD00322389 /* 成都.xml in Resources */,
E3ED27122A822E9D0087B7AA /* Main.storyboard in Resources */,
+ E39BAF7B2B6CCC74002C692F /* 不如跳舞.mp4 in Resources */,
+ E39BAF792B6CCC65002C692F /* 不如跳舞.xml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -275,11 +290,15 @@
E3ED27392A8312120087B7AA /* AgoraStringExtention.swift in Sources */,
E3ED273B2A8312120087B7AA /* AgoraDownLoadManager.swift in Sources */,
E3ED27212A822ED30087B7AA /* KTVApiImpl.swift in Sources */,
+ E3EC073C2BD8BB8600CB8279 /* APIReporter.swift in Sources */,
+ E39BAF772B6CC695002C692F /* LrcTime.proto in Sources */,
E3ED27222A822ED30087B7AA /* KTVApi.swift in Sources */,
E3ED270F2A822E9D0087B7AA /* ViewController.swift in Sources */,
E3ED270B2A822E9D0087B7AA /* AppDelegate.swift in Sources */,
E3ED27342A8312120087B7AA /* AgoraMiguXmlLrcParse.swift in Sources */,
+ E39BAF762B6CC695002C692F /* LrcTime.pb.swift in Sources */,
E3ED27282A8243480087B7AA /* NetworkManager.swift in Sources */,
+ E38BDE7A2B6F7ABD007A2834 /* ApiManager.swift in Sources */,
E3ED27242A8230A00087B7AA /* KeyCenter.swift in Sources */,
E3ED27382A8312120087B7AA /* AgoraCacheFileHandle.swift in Sources */,
E3ED272A2A826B0D0087B7AA /* KTVLyricView.swift in Sources */,
@@ -288,6 +307,7 @@
E3ED27352A8312120087B7AA /* AgoraLrcParse.swift in Sources */,
E3ED273A2A8312120087B7AA /* AgoraURLExtention.swift in Sources */,
E3ED27372A8312120087B7AA /* AgoraLrcModel.swift in Sources */,
+ E38BDE782B6F7A78007A2834 /* KTVGiantChorusApiImpl.swift in Sources */,
E3ED27362A8312120087B7AA /* AgoraRequestTask.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo.xcodeproj/xcuserdata/cp.xcuserdatad/xcschemes/xcschememanagement.plist b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo.xcodeproj/xcuserdata/cp.xcuserdatad/xcschemes/xcschememanagement.plist
index f426412..6d4c9a4 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo.xcodeproj/xcuserdata/cp.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo.xcodeproj/xcuserdata/cp.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,7 +7,7 @@
KTVApiDemo.xcscheme_^#shared#^_
orderHint
- 6
+ 8
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/ApiManager.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/ApiManager.swift
new file mode 100644
index 0000000..989b0c8
--- /dev/null
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/ApiManager.swift
@@ -0,0 +1,233 @@
+import Foundation
+class ApiManager {
+ static let shared = ApiManager()
+
+ private let domain = "https://api.sd-rtn.com"
+ //private let domain: String = "http://218.205.37.50:16000"
+ //private let testIp: String = "218.205.37.50"
+
+ private let TAG = "ApiManager"
+
+ private var tokenName = ""
+ private var taskId = ""
+
+ private lazy var session: URLSession = {
+ let configuration = URLSessionConfiguration.default
+ configuration.timeoutIntervalForRequest = 30
+ configuration.timeoutIntervalForResource = 30
+
+ return URLSession(configuration: configuration)
+ }()
+
+ func fetchCloudToken() -> String? {
+ var token: String? = nil
+
+ do {
+ let timeInterval: TimeInterval = Date().timeIntervalSince1970
+ let millisecond = CLongLong(round(timeInterval*1000))
+ let acquireOjb = try JSONSerialization.data(withJSONObject: [
+ "instanceId": "\(Int(millisecond))"
+ ])
+
+ let url = getTokenUrl(domain: domain, appId: KeyCenter.AppId)
+ guard let requestUrl = URL(string: url) else {return ""}
+ var request = URLRequest(url: requestUrl)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue(getBasicAuth(), forHTTPHeaderField: "Authorization")
+ request.httpBody = acquireOjb
+
+ let semaphore = DispatchSemaphore(value: 0)
+
+ let task = session.dataTask(with: request) { (data, response, error) in
+ if let error = error {
+ print("getToken error: \(error.localizedDescription)")
+ token = nil
+ // VLToast.toast("ktv_merge_failed_and create".toSceneLocalization() as String)
+ } else if let data = data {
+ do {
+ guard let responseDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let tokenName = responseDict["tokenName"] as? String else {
+ return
+ }
+
+ token = tokenName
+ } catch {
+ print("getToken error: \(error.localizedDescription)")
+ // VLToast.toast("ktv_merge_failed_and create".toSceneLocalization() as String)
+ token = nil
+ }
+ }
+
+ semaphore.signal()
+ }
+
+ task.resume()
+ semaphore.wait()
+
+ } catch {
+ print("getToken error: \(error.localizedDescription)")
+ //VLToast.toast("ktv_merge_failed_and create")
+ token = nil
+ }
+
+ return token
+ }
+
+ func fetchStartCloud(mainChannel: String, cloudRtcUid: Int, inputToken: String, outputToken: String, completion: @escaping ((Bool)->Void)) {
+ let token = fetchCloudToken()
+
+ if token == nil {
+ print("云端合流uid 请求报错 token is null")
+ completion(false)
+ return
+ } else {
+ tokenName = token!
+ }
+
+ do {
+ let inputRetObj: [String: Any] = [
+ "rtcUid": 0,
+ "rtcToken": inputToken,
+ "rtcChannel": mainChannel
+ ]
+
+ let intObj: [String: Any] = ["rtc": inputRetObj]
+
+ let audioOptionObj: [String: Any] = [
+ "profileType": "AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO",
+ "fullChannelMixer": "native-mixer-weighted"
+ ]
+
+ let outputRetObj: [String: Any] = [
+ "rtcUid": cloudRtcUid,
+ "rtcToken": outputToken,
+ "rtcChannel": "\(mainChannel)_ad"
+ ]
+
+ let dataStreamObj: [String: Any] = [
+ "source": ["audioMetaData": true],
+ "sink": [:]
+ ]
+
+ let outputsObj: [String: Any] = [
+ "audioOption": audioOptionObj,
+ "rtc": outputRetObj,
+ "metaDataOption": dataStreamObj
+ ]
+
+ let transcoderObj: [String: Any] = [
+ "audioInputs": [intObj],
+ "idleTimeout": 300,
+ "outputs": [outputsObj]
+ ]
+
+ let postBody: [String: Any] = [
+ "services": [
+ "cloudTranscoder": [
+ "serviceType": "cloudTranscoderV2",
+ "config": [
+ "transcoder": transcoderObj
+ ]
+ ]
+ ]
+ ]
+
+ let url = startTaskUrl(domain: domain, appId: KeyCenter.AppId, tokenName: tokenName)
+ guard let requestUrl = URL(string: url) else {return}
+ var request = URLRequest(url: requestUrl)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue(getBasicAuth(), forHTTPHeaderField: "Authorization")
+ request.httpBody = try JSONSerialization.data(withJSONObject: postBody, options: [])
+
+ // let semaphore = DispatchSemaphore(value: 0)
+
+ let task = session.dataTask(with: request) { (data, response, error) in
+ if let error = error {
+ print("云端合流uid 请求报错: \(error.localizedDescription)")
+ completion(false)
+ // VLToast.toast("ktv_merge_failed_and create".toSceneLocalization() as String)
+ } else if let data = data {
+ do {
+ guard let responseDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let taskId = responseDict["taskId"] as? String else {
+ completion(false)
+ return
+ }
+
+ self.taskId = taskId
+ completion(true)
+ // VLToast.toast("ktv_merge_success".toSceneLocalization() as String)
+ print("合流成功")
+ } catch {
+ print("云端合流uid 请求报错: \(error.localizedDescription)")
+ completion(false)
+ // VLToast.toast("ktv_merge_failed_and create".toSceneLocalization() as String)
+ }
+ }
+
+ // semaphore.signal()
+ }
+
+ task.resume()
+ // semaphore.wait()
+
+ } catch {
+ print("云端合流uid 请求报错: \(error.localizedDescription)")
+ completion(false)
+ // VLToast.toast("ktv_merge_failed_and create".toSceneLocalization() as String)
+ }
+ }
+
+ func fetchStopCloud() {
+ if taskId.isEmpty || tokenName.isEmpty {
+ print("云端合流任务停止失败 taskId || tokenName is null")
+ return
+ }
+
+ do {
+ let url = deleteTaskUrl(domain: domain, appid: KeyCenter.AppId, taskid: taskId, tokenName: tokenName)
+ guard let requestUrl = URL(string: url) else {return}
+ var request = URLRequest(url: requestUrl)
+ request.httpMethod = "DELETE"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue(getBasicAuth(), forHTTPHeaderField: "Authorization")
+
+ // let semaphore = DispatchSemaphore(value: 0)
+
+ let task = session.dataTask(with: request) { (data, response, error) in
+ // Handle response
+
+ // semaphore.signal()
+ }
+
+ task.resume()
+ // semaphore.wait()
+
+ } catch {
+ print("云端合流任务停止失败: \(error.localizedDescription)")
+ }
+ }
+
+ private func getTokenUrl(domain: String, appId: String) -> String {
+ return String(format: "%@/v1/projects/%@/rtsc/cloud-transcoder/builderTokens", domain, appId)
+ }
+
+ private func startTaskUrl(domain: String, appId: String, tokenName: String) -> String {
+ return String(format: "%@/v1/projects/%@/rtsc/cloud-transcoder/tasks?builderToken=%@", domain, appId, tokenName)
+ }
+
+ private func deleteTaskUrl(domain: String, appid: String, taskid: String, tokenName: String) -> String {
+ return String(format: "%@/v1/projects/%@/rtsc/cloud-transcoder/tasks/%@?builderToken=%@", domain, appid, taskid, tokenName)
+ }
+
+ private func getBasicAuth() -> String {
+ // 拼接客户 ID 和客户密钥并使用 base64 编码
+ let plainCredentials = "\(KeyCenter.RestfulApiKey!):\(KeyCenter.RestfulApiSecret!)"
+ guard let base64Credentials = plainCredentials.data(using: .utf8)?.base64EncodedString() else {
+ return ""
+ }
+ // 创建 authorization header
+ return "Basic \(base64Credentials)"
+ }
+
+}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/Base.lproj/Main.storyboard b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/Base.lproj/Main.storyboard
index 554f906..eb88c14 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/Base.lproj/Main.storyboard
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/Base.lproj/Main.storyboard
@@ -1,9 +1,9 @@
-
+
-
+
@@ -40,29 +40,22 @@
-
+
-
-
+
-
+
@@ -96,7 +89,7 @@
-
+
@@ -105,13 +98,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -124,6 +129,7 @@
+
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/Info.plist b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/Info.plist
index 8e6b5a8..79d3033 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/Info.plist
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/Info.plist
@@ -2,6 +2,11 @@
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/APIReporter.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/APIReporter.swift
new file mode 100644
index 0000000..936e59b
--- /dev/null
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/APIReporter.swift
@@ -0,0 +1,159 @@
+//
+// APIReporter.swift
+// CallAPI
+//
+// Created by wushengtao on 2024/4/8.
+//
+
+import AgoraRtcKit
+
+
+/// 场景化类型
+public enum APIType: Int {
+ case ktv = 1 //K歌
+ case call = 2 //呼叫
+ case beauty = 3 //美颜
+ case videoLoader = 4 //秒开/秒切
+ case pk = 5 //团战
+ case vitualSpace = 6 //
+ case screenSpace = 7 //屏幕共享
+ case audioScenario = 8 //音频scenario
+}
+
+enum APIEventType: Int {
+ case api = 0 //api事件
+ case cost //耗时事件
+ case custom //自定义事件
+}
+
+struct ApiEventKey {
+ static let type = "type"
+ static let desc = "desc"
+ static let apiValue = "apiValue"
+ static let ts = "ts"
+ static let ext = "ext"
+}
+
+struct APICostEvent {
+ static let channelUsage = "channelUsage" //频道使用耗时
+ static let firstFrameActual = "firstFrameActual" //首帧实际耗时
+ static let firstFramePerceived = "firstFramePerceived" //首帧感官耗时
+}
+
+let formatter = DateFormatter()
+func debugApiPrint(_ message: String) {
+#if DEBUG
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
+ let timeString = formatter.string(from: Date())
+ print("\(timeString) \(message)")
+#endif
+}
+
+@objcMembers
+public class APIReporter: NSObject {
+ private var engine: AgoraRtcEngineKit
+ private let messsageId: String = "agora:scenarioAPI"
+ private var category: String
+ private var durationEventStartMap: [String: Int64] = [:]
+
+ //MARK: public
+ public init(type: APIType, version: String, engine: AgoraRtcEngineKit) {
+ self.category = "\(type.rawValue)_iOS_\(version)"
+ self.engine = engine
+ super.init()
+
+ configParameters()
+ }
+
+ public func reportFuncEvent(name: String, value: [String: Any], ext: [String: Any]) {
+ let content = "[APIReporter]reportFuncEvent: \(name) value: \(value) ext: \(ext)"
+ debugApiPrint(content)
+ let eventMap: [String: Any] = [ApiEventKey.type: APIEventType.api.rawValue, ApiEventKey.desc: name]
+ let labelMap: [String: Any] = [ApiEventKey.apiValue: value, ApiEventKey.ts: getCurrentTs(), ApiEventKey.ext: ext]
+ let event = convertToJSONString(eventMap) ?? ""
+ let label = convertToJSONString(labelMap) ?? ""
+ engine.sendCustomReportMessage(messsageId,
+ category: category,
+ event: event,
+ label: label,
+ value: 0)
+ }
+
+ public func startDurationEvent(name: String) {
+ durationEventStartMap[name] = getCurrentTs()
+ }
+
+ public func endDurationEvent(name: String, ext: [String: Any]) {
+ guard let beginTs = durationEventStartMap[name] else {return}
+ durationEventStartMap.removeValue(forKey: name)
+ let ts = getCurrentTs()
+ let cost = Int(ts - beginTs)
+
+ reportCostEvent(ts: ts, name: name, cost: cost, ext: ext)
+ }
+
+ public func reportCostEvent(name: String, cost: Int, ext: [String: Any]) {
+ durationEventStartMap.removeValue(forKey: name)
+ reportCostEvent(ts: getCurrentTs(), name: name, cost: cost, ext: ext)
+ }
+
+ public func reportCustomEvent(name: String, ext: [String: Any]) {
+ let content = "[APIReporter]reportCustomEvent: \(name) ext: \(ext)"
+ debugApiPrint(content)
+ let eventMap: [String: Any] = [ApiEventKey.type: APIEventType.custom.rawValue, ApiEventKey.desc: name]
+ let labelMap: [String: Any] = [ApiEventKey.ts: getCurrentTs(), ApiEventKey.ext: ext]
+ let event = convertToJSONString(eventMap) ?? ""
+ let label = convertToJSONString(labelMap) ?? ""
+ engine.sendCustomReportMessage(messsageId,
+ category: category,
+ event: event,
+ label: label,
+ value: 0)
+ }
+
+ public func writeLog(content: String, level: AgoraLogLevel) {
+ engine.writeLog(level, content: content)
+ }
+
+ public func cleanCache() {
+ durationEventStartMap.removeAll()
+ }
+
+ //MARK: private
+ private func reportCostEvent(ts: Int64, name: String, cost: Int, ext: [String: Any]) {
+ let content = "[APIReporter]reportCostEvent: \(name) cost: \(cost) ms ext: \(ext)"
+ debugApiPrint(content)
+ writeLog(content: content, level: .info)
+ let eventMap: [String: Any] = [ApiEventKey.type: APIEventType.cost.rawValue, ApiEventKey.desc: name]
+ let labelMap: [String: Any] = [ApiEventKey.ts: ts, ApiEventKey.ext: ext]
+ let event = convertToJSONString(eventMap) ?? ""
+ let label = convertToJSONString(labelMap) ?? ""
+ engine.sendCustomReportMessage(messsageId,
+ category: category,
+ event: event,
+ label: label,
+ value: cost)
+ }
+
+ private func configParameters() {
+// engine.setParameters("{\"rtc.qos_for_test_purpose\": true}")
+ engine.setParameters("{\"rtc.direct_send_custom_event\": true}")
+ engine.setParameters("{\"rtc.log_external_input\": true}")
+ }
+
+ private func getCurrentTs() -> Int64 {
+ return Int64(round(Date().timeIntervalSince1970 * 1000.0))
+ }
+
+ private func convertToJSONString(_ dictionary: [String: Any]) -> String? {
+ do {
+ let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: [])
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
+ return jsonString
+ }
+ } catch {
+ writeLog(content: "[APIReporter]convert to json fail: \(error) dictionary: \(dictionary)", level: .warn)
+ }
+ return nil
+ }
+}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVApi.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVApi.swift
index d1fca98..16395b8 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVApi.swift
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVApi.swift
@@ -65,16 +65,13 @@ import AgoraRtcKit
/// 加入合唱失败原因
@objc public enum KTVJoinChorusFailReason: Int {
- case musicPreloadFail //歌曲预加载失败
case musicOpenFail //歌曲打开失败
case joinChannelFail //加入ex频道失败
- case musicPreloadFailAndJoinChannelFail
}
@objc public enum KTVType: Int {
case normal
case singbattle
- case cantata
case singRelay
}
@@ -88,7 +85,7 @@ import AgoraRtcKit
/// - status: <#status description#>
/// - msg: <#msg description#>
/// - lyricUrl: <#lyricUrl description#>
- func onMusicLoadProgress(songCode: Int, percent: Int, status: AgoraMusicContentCenterPreloadStatus, msg: String?, lyricUrl: String?)
+ func onMusicLoadProgress(songCode: Int, percent: Int, state: AgoraMusicContentCenterPreloadState, msg: String?, lyricUrl: String?)
/// 歌曲加载成功
/// - Parameters:
@@ -105,17 +102,6 @@ import AgoraRtcKit
func onMusicLoadFail(songCode: Int, reason: KTVLoadSongFailReason)
}
-
-//public protocol KTVJoinChorusStateListener: NSObjectProtocol {
-//
-// /// 加入合唱成功
-// func onJoinChorusSuccess()
-//
-// /// 加入合唱失败
-// /// - Parameter reason: 失败原因
-// func onJoinChorusFail(reason: KTVJoinChorusFailReason)
-//}
-
@objc public protocol KTVLrcViewDelegate: NSObjectProtocol {
func onUpdatePitch(pitch: Float)
func onUpdateProgress(progress: Int)
@@ -131,7 +117,7 @@ import AgoraRtcKit
/// - error: <#error description#>
/// - isLocal: <#isLocal description#>
func onMusicPlayerStateChanged(state: AgoraMediaPlayerState,
- error: AgoraMediaPlayerError,
+ reason: AgoraMediaPlayerReason,
isLocal: Bool)
@@ -160,6 +146,73 @@ import AgoraRtcKit
func onMusicPlayerProgressChanged(with progress: Int)
}
+// 大合唱中演唱者互相收听对方音频流的选路策略
+enum GiantChorusRouteSelectionType: Int {
+ case random = 0 // 随机选取几条流
+ case byDelay = 1 // 根据延迟选择最低的几条流
+ case topN = 2 // 根据音强选流
+ case byDelayAndTopN = 3 // 同时开始延迟选路和音强选流
+}
+
+// 大合唱中演唱者互相收听对方音频流的选路配置
+@objc public class GiantChorusRouteSelectionConfig: NSObject {
+ let type: GiantChorusRouteSelectionType // 选路策略
+ let streamNum: Int // 最大选取的流个数(推荐6)
+
+ init(type: GiantChorusRouteSelectionType, streamNum: Int) {
+ self.type = type
+ self.streamNum = streamNum
+ }
+}
+
+@objc open class GiantChorusConfiguration: NSObject {
+ var appId: String
+ var rtmToken: String
+ weak var engine: AgoraRtcEngineKit?
+ var channelName: String
+ var localUid: Int = 0
+ var chorusChannelName: String
+ var chorusChannelToken: String
+ var maxCacheSize: Int = 10
+ var musicType: loadMusicType = .mcc
+ var audienceChannelToken: String = ""
+ var musicStreamUid: Int = 0
+ var musicChannelToken: String = ""
+ var routeSelectionConfig: GiantChorusRouteSelectionConfig = GiantChorusRouteSelectionConfig(type: .byDelay, streamNum: 6)
+ var mccDomain: String?
+ @objc public
+ init(appId: String,
+ rtmToken: String,
+ engine: AgoraRtcEngineKit,
+ localUid: Int,
+ audienceChannelName: String,
+ audienceChannelToken: String,
+ chorusChannelName: String,
+ chorusChannelToken: String,
+ musicStreamUid: Int,
+ musicChannelToken: String,
+ maxCacheSize: Int,
+ musicType: loadMusicType,
+ routeSelectionConfig: GiantChorusRouteSelectionConfig,
+ mccDomain: String?
+ ) {
+ self.appId = appId
+ self.rtmToken = rtmToken
+ self.engine = engine
+ self.channelName = audienceChannelName
+ self.localUid = localUid
+ self.chorusChannelName = chorusChannelName
+ self.chorusChannelToken = chorusChannelToken
+ self.maxCacheSize = maxCacheSize
+ self.musicType = musicType
+ self.audienceChannelToken = audienceChannelToken
+ self.musicStreamUid = musicStreamUid
+ self.musicChannelToken = musicChannelToken
+ self.routeSelectionConfig = routeSelectionConfig
+ self.mccDomain = mccDomain
+ }
+}
+
@objc open class KTVApiConfig: NSObject{
var appId: String
var rtmToken: String
@@ -171,7 +224,7 @@ import AgoraRtcKit
var type: KTVType = .normal
var maxCacheSize: Int = 10
var musicType: loadMusicType = .mcc
- var isDebugMode: Bool = false
+ var mccDomain: String?
@objc public
init(appId: String,
rtmToken: String,
@@ -181,9 +234,9 @@ import AgoraRtcKit
chorusChannelName: String,
chorusChannelToken: String,
type: KTVType,
- maxCacheSize: Int,
musicType: loadMusicType,
- isDebugMode: Bool
+ maxCacheSize: Int,
+ mccDomain: String?
) {
self.appId = appId
self.rtmToken = rtmToken
@@ -195,49 +248,49 @@ import AgoraRtcKit
self.type = type
self.maxCacheSize = maxCacheSize
self.musicType = musicType
- self.isDebugMode = isDebugMode
+ self.mccDomain = mccDomain
}
+
+
}
/// 歌曲加载配置信息
@objcMembers open class KTVSongConfiguration: NSObject {
public var songIdentifier: String = ""
- public var autoPlay: Bool = false //是否加载完成自动播放
public var mainSingerUid: Int = 0 //主唱uid
public var mode: KTVLoadMusicMode = .loadMusicAndLrc
-
- func printObjectContent() -> String {
- var content = ""
-
- let mirror = Mirror(reflecting: self)
- for child in mirror.children {
- if let propertyName = child.label {
- if let propertyValue = child.value as? CustomStringConvertible {
- content += "\(propertyName): \(propertyValue)\n"
- } else {
- content += "\(propertyName): \(child.value)\n"
- }
- }
- }
-
- return content
- }
+ public var songCutter: Bool = false
+// func printObjectContent() -> String {
+// var content = ""
+//
+// let mirror = Mirror(reflecting: self)
+// for child in mirror.children {
+// if let propertyName = child.label {
+// if let propertyValue = child.value as? CustomStringConvertible {
+// content += "\(propertyName): \(propertyValue)\n"
+// } else {
+// content += "\(propertyName): \(child.value)\n"
+// }
+// }
+// }
+//
+// return content
+// }
}
public typealias LyricCallback = ((String?) -> Void)
-public typealias LoadMusicCallback = ((AgoraMusicContentCenterPreloadStatus, NSInteger) -> Void)
+public typealias LoadMusicCallback = ((AgoraMusicContentCenterPreloadState, NSInteger) -> Void)
public typealias ISwitchRoleStateListener = (KTVSwitchRoleState, KTVSwitchRoleFailReason) -> Void
-public typealias MusicChartCallBacks = (String, AgoraMusicContentCenterStatusCode, [AgoraMusicChartInfo]?) -> Void
-public typealias MusicResultCallBacks = (String, AgoraMusicContentCenterStatusCode, AgoraMusicCollection) -> Void
+public typealias MusicChartCallBacks = (String, AgoraMusicContentCenterStateReason, [AgoraMusicChartInfo]?) -> Void
+public typealias MusicResultCallBacks = (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void
public typealias JoinExChannelCallBack = ((Bool, KTVJoinChorusFailReason?)-> Void)
@objc public protocol KTVApiDelegate: NSObjectProtocol {
- /// 初始化
- /// - Parameter config: <#config description#>
- init(config: KTVApiConfig)
+ @objc optional func createKtvApi(config: KTVApiConfig) //小合唱必选
+ @objc optional func createKTVGiantChorusApi(config: GiantChorusConfiguration) //大合唱必选
/// 订阅KTVApi事件
/// - Parameter ktvApiEventHandler: <#ktvApiEventHandler description#>
@@ -410,4 +463,6 @@ public typealias JoinExChannelCallBack = ((Bool, KTVJoinChorusFailReason?)-> Voi
*/
func removeMusic(songCode: Int)
+
+ @objc func didAudioMetadataReceived( uid: UInt, metadata: Data)
}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVApiImpl.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVApiImpl.swift
index f76082f..60d71d8 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVApiImpl.swift
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVApiImpl.swift
@@ -7,25 +7,21 @@
import Foundation
import AgoraRtcKit
-
+import SwiftProtobuf
/// 加载歌曲状态
-@objc public enum KTVLoadSongState: Int {
+@objc fileprivate enum KTVLoadSongState: Int {
case idle = -1 //空闲
case ok = 0 //成功
case failed //失败
case inProgress //加载中
}
-enum KTVSongMode: Int {
+fileprivate enum KTVSongMode: Int {
case songCode
case songUrl
}
-private func agoraPrint(_ message: String) {
- print(message)
-}
-
-@objc class KTVApiImpl: NSObject{
+@objc class KTVApiImpl: NSObject, KTVApiDelegate{
private var apiConfig: KTVApiConfig?
@@ -55,6 +51,7 @@ private func agoraPrint(_ message: String) {
private var startHighTime: Int = 0
private var isRelease: Bool = false
private var songUrl2: String = ""
+ private var enableMultipathing = true
private var playerState: AgoraMediaPlayerState = .idle {
didSet {
agoraPrint("playerState did changed: \(oldValue.rawValue)->\(playerState.rawValue)")
@@ -83,6 +80,11 @@ private func agoraPrint(_ message: String) {
private var songUrl: String = ""
private var songCode: Int = 0
private var songIdentifier: String = ""
+
+ private let tag = "KTV_API_LOG"
+ private let messageId = "agora:scenarioAPI"
+ private let version = "5.0.0"
+ private let lyricSyncVersion = 2
private var singerRole: KTVSingRole = .audience {
didSet {
@@ -93,24 +95,29 @@ private func agoraPrint(_ message: String) {
private var timer: Timer?
private var isPause: Bool = false
-
+ private var recvFromDataStream = false
public var remoteVolume: Int = 30
private var joinChorusNewRole: KTVSingRole = .audience
private var oldPitch: Double = 0
private var isWearingHeadPhones: Bool = false
private var enableProfessional: Bool = false
private var isPublishAudio: Bool = false
+ private var preludeDuration: Int64 = 0
private lazy var apiDelegateHandler = KTVApiRTCDelegateHandler(with: self)
+
+ private var totalSize: Int = 0
+
+ private var apiRepoter: APIReporter?
+
deinit {
mcc?.register(nil)
agoraPrint("deinit KTVApiImpl")
}
-
- @objc required init(config: KTVApiConfig) {
- super.init()
- agoraPrint("init KTVApiImpl")
+
+ @objc func createKtvApi(config: KTVApiConfig) {
self.apiConfig = config
+ apiRepoter = APIReporter(type: .ktv, version: version, engine: apiConfig?.engine ?? AgoraRtcEngineKit())
setParams()
if config.musicType == .mcc {
@@ -121,11 +128,14 @@ private func agoraPrint(_ message: String) {
contentCenterConfiguration.token = config.rtmToken
contentCenterConfiguration.rtcEngine = config.engine
contentCenterConfiguration.maxCacheSize = UInt(config.maxCacheSize)
- if config.isDebugMode {
- //如果这一块报错为contentCenterConfiguration没有mccDomain这个属性 说明该版本不支持这个 可以注释掉这行代码。完全不影响
- contentCenterConfiguration.mccDomain = "api-test.agora.io"
+ if let domain = config.mccDomain {
+ contentCenterConfiguration.mccDomain = domain
}
mcc = AgoraMusicContentCenter.sharedContentCenter(config: contentCenterConfiguration)
+ if mcc == nil {
+ agoraPrint("mcc create fail")
+// assert(mcc != nil, "mcc == nil")
+ }
mcc?.register(self)
// ------------------ 初始化音乐播放器实例 ------------------
mediaPlayer = mcc?.createMusicPlayer(delegate: self)
@@ -137,8 +147,11 @@ private func agoraPrint(_ message: String) {
mediaPlayer?.adjustPlayoutVolume(50)
mediaPlayer?.adjustPublishSignalVolume(50)
}
+
apiConfig?.engine?.addDelegate(apiDelegateHandler)
+ mediaPlayer?.setPlayerOption("play_pos_change_callback", value: 100)
initTimer()
+ agoraPrint("init KTVApiImpl")
}
private func setParams() {
@@ -153,13 +166,24 @@ private func agoraPrint(_ message: String) {
engine.setParameters("{\"che.audio.neteq.prebuffer_max_delay\": 600}")
engine.setParameters("{\"che.audio.max_mixed_participants\": 8}")
engine.setParameters("{\"che.audio.custom_bitrate\": 48000}")
- engine.setParameters("{\"che.audio.direct.uplink_process\": false}")
engine.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
engine.setParameters("{\"che.audio.neteq.targetlevel_offset\": 20}")
engine.setParameters("{\"che.audio.ans.noise_gate\": 20}")
+ engine.setParameters("{\"rtc.use_audio4\": true}")
if apiConfig?.type == .singRelay {
engine.setParameters("{\"che.audio.aiaec.working_mode\": 1}")
}
+
+ //4.3.0 add
+ enableMultipathing = true
+// engine.setParameters("{\"rtc.enable_tds_request_on_join\": true}")
+// engine.setParameters("{\"rtc.remote_path_scheduling_strategy\": 0}")
+ engine.setParameters("{\"rtc.path_scheduling_strategy\": 0}")
+ // engine.setParameters("{\"rtc.enableMultipath\": true}")
+ engine.setParameters("{\"rtc.log_external_input\":true}")
+ // 数据上报
+ engine.setParameters("{\"rtc.direct_send_custom_event\": true}")
+ // engine.setParameters("{\"rtc.qos_for_test_purpose\": true}")
}
func renewInnerDataStreamId() {
@@ -167,19 +191,58 @@ private func agoraPrint(_ message: String) {
dataStreamConfig.ordered = false
dataStreamConfig.syncWithAudio = true
self.apiConfig?.engine?.createDataStream(&dataStreamId, config: dataStreamConfig)
- sendCustomMessage(with: "renewInnerDataStreamId", label: "")
+ sendCustomMessage(with: "renewInnerDataStreamId", dict: [:])
+ agoraPrint("renewInnerDataStreamId")
}
}
//MARK: KTVApiDelegate
-extension KTVApiImpl: KTVApiDelegate {
+extension KTVApiImpl {
+
+ func objectContent(of object: Any) -> [String: Any] {
+ var content = [String: Any]()
+
+ let mirror = Mirror(reflecting: object)
+ for child in mirror.children {
+ if let propertyName = child.label {
+ if let convertibleValue = convertToJSONSerializable(child.value) {
+ content[propertyName] = convertibleValue
+ }
+ }
+ }
+
+ return content
+ }
+
+ func convertToJSONSerializable(_ value: Any) -> Any? {
+ switch value {
+ case let value as String:
+ return value
+ case let value as Int:
+ return value
+ case let value as Double:
+ return value
+ case let value as Bool:
+ return value
+ case let value as Int?:
+ return value
+ case let value as Double?:
+ return value
+ case let value as Bool?:
+ return value
+ case let value as String?:
+ return value
+ default:
+ return nil
+ }
+ }
func getMusicContentCenter() -> AgoraMusicContentCenter? {
return mcc
}
func setLrcView(view: KTVLrcViewDelegate) {
- sendCustomMessage(with: "renewInnerDataStreamId", label: "view:\(view.description)")
+ sendCustomMessage(with: "setLrcView", dict: [:])
lrcControl = view
}
@@ -192,15 +255,14 @@ extension KTVApiImpl: KTVApiDelegate {
self.songUrl = url1
self.songUrl2 = url2
- if config.autoPlay {
- // 主唱自动播放歌曲
- if self.singerRole != .leadSinger {
- switchSingerRole(newRole: .soloSinger) { state, failRes in
-
- }
- }
- startSing(url: url1, startPos: 0)
- }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// switchSingerRole(newRole: .soloSinger) { state, failRes in
+// }
+// }
+// startSing(url: url1, startPos: 0)
+ // }
}
//主要针对本地歌曲播放的主唱伴奏切换的 MCC直接忽视这个方法
@@ -218,8 +280,8 @@ extension KTVApiImpl: KTVApiDelegate {
}
func loadMusic(songCode: Int, config: KTVSongConfiguration, onMusicLoadStateListener: IMusicLoadStateListener) {
- sendCustomMessage(with: "loadMusic", label: "config:\(config.printObjectContent())")
- agoraPrint("loadMusic songCode:\(songCode) ")
+ sendCustomMessage(with: "loadMusic", dict: objectContent(of: config))
+ agoraPrint("loadMusic songCode:\(songCode) mode:\(config.mode.rawValue)")
self.songMode = .songCode
self.songCode = songCode
self.songIdentifier = config.songIdentifier
@@ -227,28 +289,27 @@ extension KTVApiImpl: KTVApiDelegate {
}
func loadMusic(config: KTVSongConfiguration, url: String) {
- sendCustomMessage(with: "loadMusic", label: "config:\(config.printObjectContent()), url:\(url)")
+ sendCustomMessage(with: "loadMusicWithUrl:\(url)", dict: objectContent(of: config))
self.songMode = .songUrl
self.songUrl = url
self.songIdentifier = config.songIdentifier
- if config.autoPlay {
- // 主唱自动播放歌曲
- if singerRole != .leadSinger {
- switchSingerRole(newRole: .soloSinger) { _, _ in
-
- }
- }
- startSing(url: url, startPos: 0)
- }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if singerRole != .leadSinger {
+// switchSingerRole(newRole: .soloSinger) { _, _ in
+// }
+// }
+// startSing(url: url, startPos: 0)
+// }
}
func getMusicPlayer() -> AgoraRtcMediaPlayerProtocol? {
- sendCustomMessage(with: "getMusicPlayer", label: "")
+ sendCustomMessage(with: "getMusicPlayer", dict: [:])
return mediaPlayer
}
func addEventHandler(ktvApiEventHandler: KTVApiEventHandlerDelegate) {
- sendCustomMessage(with: "addEventHandler", label: "")
+ sendCustomMessage(with: "addEventHandler", dict: [:])
if eventHandlers.contains(ktvApiEventHandler) {
return
}
@@ -256,12 +317,12 @@ extension KTVApiImpl: KTVApiDelegate {
}
func removeEventHandler(ktvApiEventHandler: KTVApiEventHandlerDelegate) {
- sendCustomMessage(with: "removeEventHandler", label: "")
+ sendCustomMessage(with: "removeEventHandler", dict: [:])
eventHandlers.remove(ktvApiEventHandler)
}
func cleanCache() {
- sendCustomMessage(with: "cleanCache", label: "")
+ sendCustomMessage(with: "cleanCache", dict: [:])
isRelease = true
freeTimer()
agoraPrint("cleanCache")
@@ -282,7 +343,12 @@ extension KTVApiImpl: KTVApiDelegate {
}
func renewToken(rtmToken: String, chorusChannelRtcToken: String) {
- sendCustomMessage(with: "renewToken", label: "rtmToken:\(rtmToken), chorusChannelRtcToken:\(chorusChannelRtcToken)")
+
+ let dict: [String: Any] = [
+ "rtmToken":rtmToken,
+ "chorusChannelRtcToken":chorusChannelRtcToken
+ ]
+ sendCustomMessage(with: "renewToken", dict: dict)
// 更新RtmToken
mcc?.renewToken(rtmToken)
// 更新合唱频道RtcToken
@@ -294,7 +360,7 @@ extension KTVApiImpl: KTVApiDelegate {
}
func fetchMusicCharts(completion: @escaping MusicChartCallBacks) {
- sendCustomMessage(with: "fetchMusicCharts", label: "")
+ sendCustomMessage(with: "fetchMusicCharts", dict: [:])
agoraPrint("fetchMusicCharts")
let requestId = mcc!.getMusicCharts()
musicChartDict[requestId] = completion
@@ -304,9 +370,15 @@ extension KTVApiImpl: KTVApiDelegate {
page: Int,
pageSize: Int,
jsonOption: String,
- completion:@escaping (String, AgoraMusicContentCenterStatusCode, AgoraMusicCollection) -> Void) {
+ completion:@escaping (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void) {
agoraPrint("searchMusic with musicChartId: \(musicChartId)")
- sendCustomMessage(with: "searchMusic", label: "musicChartId:\(musicChartId), page:\(page), pageSize:\(pageSize), jsonOption:\(jsonOption)")
+ let dict: [String: Any] = [
+ "musicChartId":musicChartId,
+ "page": page,
+ "pageSize": pageSize,
+ "jsonOption": jsonOption
+ ]
+ sendCustomMessage(with: "searchMusic", dict: dict)
let requestId = mcc!.getMusicCollection(musicChartId: musicChartId, page: page, pageSize: pageSize, jsonOption: jsonOption)
musicSearchDict[requestId] = completion
}
@@ -315,25 +387,38 @@ extension KTVApiImpl: KTVApiDelegate {
page: Int,
pageSize: Int,
jsonOption: String,
- completion: @escaping (String, AgoraMusicContentCenterStatusCode, AgoraMusicCollection) -> Void) {
+ completion: @escaping (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void) {
agoraPrint("searchMusic with keyword: \(keyword)")
- sendCustomMessage(with: "searchMusic", label: "keyword:\(keyword), page:\(page), pageSize:\(pageSize), jsonOption:\(jsonOption)")
+ let dict: [String: Any] = [
+ "keyword": keyword,
+ "page": page,
+ "pageSize": pageSize,
+ "jsonOption": jsonOption
+ ]
+ sendCustomMessage(with: "searchMusic", dict: dict)
let requestId = mcc!.searchMusic(keyWord: keyword, page: page, pageSize: pageSize, jsonOption: jsonOption)
musicSearchDict[requestId] = completion
}
func switchSingerRole(newRole: KTVSingRole, onSwitchRoleState: @escaping (KTVSwitchRoleState, KTVSwitchRoleFailReason) -> Void) {
let oldRole = singerRole
- sendCustomMessage(with: "switchSingerRole", label: "oldRole:\(oldRole.rawValue), newRole: \(newRole.rawValue)")
+
+ let dict: [String: Any] = [
+ "oldRole": oldRole.rawValue,
+ "newRole": newRole.rawValue
+ ]
+ sendCustomMessage(with: "switchSingerRole", dict: dict)
agoraPrint("switchSingerRole oldRole:\(oldRole.rawValue), newRole: \(newRole.rawValue)")
- if ((oldRole == .leadSinger || oldRole == .soloSinger) && (newRole == .coSinger || newRole == .audience) && isNowMicMuted) {
- apiConfig?.engine?.muteLocalAudioStream(true)
- apiConfig?.engine?.adjustRecordingSignalVolume(100)
- } else if ((oldRole == .audience || oldRole == .coSinger) && (newRole == .leadSinger || newRole == .soloSinger) && isNowMicMuted) {
- apiConfig?.engine?.adjustRecordingSignalVolume(0)
- apiConfig?.engine?.muteLocalAudioStream(false)
- }
+// if (apiConfig?.type != .singRelay) {
+// if ((oldRole == .leadSinger || oldRole == .soloSinger) && (newRole == .coSinger || newRole == .audience) && isNowMicMuted) {
+// apiConfig?.engine?.muteLocalAudioStream(true)
+// apiConfig?.engine?.adjustRecordingSignalVolume(100)
+// } else if ((oldRole == .audience || oldRole == .coSinger) && (newRole == .leadSinger || newRole == .soloSinger) && isNowMicMuted) {
+// apiConfig?.engine?.adjustRecordingSignalVolume(0)
+// apiConfig?.engine?.muteLocalAudioStream(false)
+// }
+// }
self.switchSingerRole(oldRole: oldRole, newRole: newRole, token: apiConfig?.chorusChannelToken ?? "", stateCallBack: onSwitchRoleState)
}
@@ -342,7 +427,7 @@ extension KTVApiImpl: KTVApiDelegate {
* 恢复播放
*/
@objc public func resumeSing() {
- sendCustomMessage(with: "resumeSing", label: "")
+ sendCustomMessage(with: "resumeSing", dict: [:])
agoraPrint("resumeSing")
if mediaPlayer?.getPlayerState() == .paused {
mediaPlayer?.resume()
@@ -356,7 +441,7 @@ extension KTVApiImpl: KTVApiDelegate {
* 暂停播放
*/
@objc public func pauseSing() {
- sendCustomMessage(with: "pauseSing", label: "")
+ sendCustomMessage(with: "pauseSing", dict: [:])
agoraPrint("pauseSing")
mediaPlayer?.pause()
}
@@ -365,7 +450,7 @@ extension KTVApiImpl: KTVApiDelegate {
* 调整进度
*/
@objc public func seekSing(time: NSInteger) {
- sendCustomMessage(with: "seekSing", label: "")
+ sendCustomMessage(with: "seekSing", dict: ["time":time])
agoraPrint("seekSing")
mediaPlayer?.seek(toPosition: time)
}
@@ -381,25 +466,60 @@ extension KTVApiImpl: KTVApiDelegate {
* 设置当前mic开关状态
*/
@objc public func muteMic(muteStatus: Bool) {
- sendCustomMessage(with: "setMicStatus", label: "\(muteStatus)")
+ sendCustomMessage(with: "setMicStatus", dict: ["muteStatus":muteStatus])
self.isNowMicMuted = muteStatus
- if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
- apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
+ if (apiConfig?.type != .singRelay) {
+ if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
+ apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
+ } else {
+// let channelMediaOptions = AgoraRtcChannelMediaOptions()
+// channelMediaOptions.publishMicrophoneTrack = !muteStatus
+// channelMediaOptions.clientRoleType = .broadcaster
+// apiConfig?.engine?.updateChannel(with: channelMediaOptions)
+// apiConfig?.engine?.muteLocalAudioStream(muteStatus)
+
+ apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
+ }
} else {
- apiConfig?.engine?.muteLocalAudioStream(muteStatus)
+ apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
}
}
@objc public func removeMusic(songCode: Int) {
- sendCustomMessage(with: "removeMusic", label: "songCode:\(songCode)")
+ sendCustomMessage(with: "removeMusic", dict: ["songCode": songCode])
let ret: Int = mcc?.removeCache(songCode: songCode) ?? 0
if ret < 0 {
agoraPrint("removeMusic failed: ret:\(ret)")
}
}
+
+ @objc public func enableMutipath(enable: Bool) {
+ sendCustomMessage(with: "enableMutipath", dict: ["enable":enable])
+ agoraPrint("enableMutipath:\(enable)")
+ enableMultipathing = enable
+ if singerRole == .coSinger || singerRole == .leadSinger {
+ if let subChorusConnection = subChorusConnection {
+ apiConfig?.engine?.setParametersEx("{\"rtc.enableMultipath\": \(enable), \"rtc.path_scheduling_strategy\": 0, \"rtc.remote_path_scheduling_strategy\": 0}", connection: subChorusConnection)
+ }
+ }
+ }
+ private func agoraPrint(_ message: String) {
+ #if DEBUG
+ print("[KTVAPI]\(message)")
+ #endif
+ apiRepoter?.writeLog(content: "[KTVAPI]\(message)", level: .info)
+ }
+
+ private func agoraPrintError(_ message: String) {
+ #if DEBUG
+ print("[KTVAPI][Error]\(message)")
+ #endif
+ apiRepoter?.writeLog(content: "[KTVAPI][Error]\(message)", level: .error)
+ }
}
+
// 主要是角色切换,加入合唱,加入多频道,退出合唱,退出多频道
extension KTVApiImpl {
private func switchSingerRole(oldRole: KTVSingRole, newRole: KTVSingRole, token: String, stateCallBack:@escaping ISwitchRoleStateListener) {
@@ -606,10 +726,10 @@ extension KTVApiImpl {
let rtcConnection = AgoraRtcConnection()
rtcConnection.channelId = apiConfig?.chorusChannelName ?? ""
rtcConnection.localUid = UInt(apiConfig?.localUid ?? 0)
- subChorusConnection = rtcConnection
+ subChorusConnection = rtcConnection
joinChorusNewRole = role
- let ret = apiConfig?.engine?.joinChannelEx(byToken: token, connection: rtcConnection, delegate: self, mediaOptions: mediaOption, joinSuccess: nil)
+ let ret = apiConfig?.engine?.joinChannelEx(byToken: token, connection: rtcConnection, delegate: self, mediaOptions: mediaOption, joinSuccess: nil)
agoraPrint("joinChannelEx ret: \(ret ?? -999)")
if newRole == .coSinger {
let uid = UInt(songConfig?.mainSingerUid ?? 0)
@@ -617,6 +737,10 @@ extension KTVApiImpl {
apiConfig?.engine?.muteRemoteAudioStream(uid, mute: true)
agoraPrint("muteRemoteAudioStream: \(uid), ret: \(ret ?? -1)")
}
+ if enableMultipathing {
+ apiConfig?.engine?.setParametersEx("{\"rtc.path_scheduling_strategy\":0, \"rtc.enableMultipath\": true, \"rtc.remote_path_scheduling_strategy\": 0}", connection: rtcConnection)
+ }
+ apiConfig?.engine?.setParameters("{\"rtc.use_audio4\": true}")
}
private func leaveChorus2ndChannel(_ role: KTVSingRole) {
@@ -666,7 +790,6 @@ extension KTVApiImpl {
}
private func _loadMusic(config: KTVSongConfiguration, mode: KTVLoadMusicMode, onMusicLoadStateListener: IMusicLoadStateListener){
-
songConfig = config
lastReceivedPosition = 0
localPosition = 0
@@ -676,6 +799,7 @@ extension KTVApiImpl {
}
if (config.mode == .loadNone) {
+ agoraPrint("load music none")
return
}
@@ -696,23 +820,23 @@ extension KTVApiImpl {
onMusicLoadStateListener.onMusicLoadFail(songCode: self.songCode, reason: .noLyricUrl)
}
- if (config.autoPlay) {
- // 主唱自动播放歌曲
- if self.singerRole != .leadSinger {
- self.switchSingerRole(newRole: .soloSinger) { _, _ in
-
- }
- }
- self.startSing(songCode: self.songCode, startPos: 0)
- }
+// if (config.autoPlay) {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
}
} else {
loadMusicListeners.setObject(onMusicLoadStateListener, forKey: "\(self.songCode)" as NSString)
- onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, status: .preloading, msg: "", lyricUrl: "")
+ // onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, status: .preloading, msg: "", lyricUrl: "")
// TODO: 只有未缓存时才显示进度条
if mcc?.isPreloaded(songCode: songCode) != 0 {
- onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, status: .preloading, msg: "", lyricUrl: "")
+ onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, state: .preloading, msg: "", lyricUrl: "")
}
+
preloadMusic(with: songCode) { [weak self] status, songCode in
guard let self = self else { return }
if self.songCode != songCode {
@@ -723,7 +847,6 @@ extension KTVApiImpl {
if mode == .loadMusicAndLrc {
// 需要加载歌词
self.loadLyric(with: songCode) { url in
- agoraPrint("loadMusicAndLrc: songCode:\(songCode) status:\(status.rawValue) ulr:\(String(describing: url))")
if self.songCode != songCode {
onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .cancled)
return
@@ -732,35 +855,35 @@ extension KTVApiImpl {
self.lyricUrlMap[String(songCode)] = urlPath
self.setLyric(with: urlPath) { lyricUrl in
onMusicLoadStateListener.onMusicLoadSuccess(songCode: songCode, lyricUrl: urlPath)
+ self.agoraPrint("loadMusicAndLrc: songCode:\(songCode) status:\(status.rawValue) ulr:\(String(describing: url))")
}
} else {
onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .noLyricUrl)
+ self.agoraPrint("loadMusicAndLrc: songCode:\(songCode) status:\(status.rawValue) ulr:\(String(describing: url))")
}
- if config.autoPlay {
- // 主唱自动播放歌曲
- if self.singerRole != .leadSinger {
- self.switchSingerRole(newRole: .soloSinger) { _, _ in
-
- }
- }
- self.startSing(songCode: self.songCode, startPos: 0)
- }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
}
} else if mode == .loadMusicOnly {
agoraPrint("loadMusicOnly: songCode:\(songCode) load success")
- if config.autoPlay {
- // 主唱自动播放歌曲
- if self.singerRole != .leadSinger {
- self.switchSingerRole(newRole: .soloSinger) { _, _ in
-
- }
- }
- self.startSing(songCode: self.songCode, startPos: 0)
- }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
onMusicLoadStateListener.onMusicLoadSuccess(songCode: songCode, lyricUrl: "")
}
} else {
- agoraPrint("load music failed songCode:\(songCode)")
+ agoraPrintError("load music failed songCode:\(songCode)")
onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .musicPreloadFail)
}
}
@@ -769,18 +892,28 @@ extension KTVApiImpl {
private func loadLyric(with songCode: NSInteger, callBack:@escaping LyricCallback) {
agoraPrint("loadLyric songCode: \(songCode)")
- let requestId: String = self.mcc?.getLyric(songCode: songCode, lyricType: 0) ?? ""
+ guard let mcc = self.mcc else {
+ agoraPrint("loadLyric songCode: \(songCode) fail")
+ callBack(nil)
+ return
+ }
+ let requestId: String = mcc.getLyric(songCode: songCode, lyricType: 0)
self.lyricCallbacks.updateValue(callBack, forKey: requestId)
}
private func preloadMusic(with songCode: Int, callback: @escaping LoadMusicCallback) {
agoraPrint("preloadMusic songCode: \(songCode)")
- if self.mcc?.isPreloaded(songCode: songCode) == 0 {
+ guard let mcc = self.mcc else {
+ agoraPrint("preloadMusic songCode: \(songCode) fail")
+ callback(.error, songCode)
+ return
+ }
+ if mcc.isPreloaded(songCode: songCode) == 0 {
musicCallbacks.removeValue(forKey: String(songCode))
callback(.OK, songCode)
return
}
- let err = self.mcc?.preload(songCode: songCode, jsonOption: nil)
+ let err = mcc.preload(songCode: songCode, jsonOption: nil)
if err != 0 {
musicCallbacks.removeValue(forKey: String(songCode))
callback(.error, songCode)
@@ -796,11 +929,15 @@ extension KTVApiImpl {
}
func startSing(songCode: Int, startPos: Int) {
- sendCustomMessage(with: "startSing", label: "songCode:\(songCode), startPos: \(startPos)")
+ let dict: [String: Any] = [
+ "songCode": songCode,
+ "startPos": startPos
+ ]
+ sendCustomMessage(with: "startSing", dict: dict)
let role = singerRole
agoraPrint("startSing role: \(role.rawValue)")
if self.songCode != songCode {
- agoraPrint("startSing failed: canceled")
+ agoraPrintError("startSing failed: canceled")
return
}
@@ -809,20 +946,24 @@ extension KTVApiImpl {
}
apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
let ret = (mediaPlayer as? AgoraMusicPlayerProtocol)?.openMedia(songCode: songCode, startPos: startPos)
- agoraPrint("startSing->openMedia(\(songCode) fail: \(ret ?? -1)")
+ agoraPrintError("startSing->openMedia(\(songCode) fail: \(ret ?? -1)")
}
func startSing(url: String, startPos: Int) {
- sendCustomMessage(with: "startSing", label: "url:\(url), startPos: \(startPos)")
+ let dict: [String: Any] = [
+ "url": url,
+ "startPos": startPos
+ ]
+ sendCustomMessage(with: "startSing", dict: dict)
let role = singerRole
agoraPrint("startSing role: \(role.rawValue)")
if self.songUrl != songUrl {
- agoraPrint("startSing failed: canceled")
+ agoraPrintError("startSing failed: canceled")
return
}
apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
let ret = mediaPlayer?.open(url, startPos: 0)
- agoraPrint("startSing->openMedia(\(url) fail: \(ret ?? -1)")
+ agoraPrintError("startSing->openMedia(\(url) fail: \(ret ?? -1)")
}
/**
@@ -830,7 +971,7 @@ extension KTVApiImpl {
*/
@objc public func stopSing() {
agoraPrint("stopSing")
- sendCustomMessage(with: "stopSing", label: "")
+ sendCustomMessage(with: "stopSing", dict: [:])
let mediaOption = AgoraRtcChannelMediaOptions()
mediaOption.publishMediaPlayerAudioTrack = false
apiConfig?.engine?.updateChannel(with: mediaOption)
@@ -850,6 +991,7 @@ extension KTVApiImpl {
@objc func enableProfessionalStreamerMode(_ enable: Bool) {
if self.isPublishAudio == false {return}
+ agoraPrint("enableProfessionalStreamerMode enable:\(enable)")
self.enableProfessional = enable
//专业非专业还需要根据是否佩戴耳机来判断是否开启3A
apiConfig?.engine?.setAudioProfile(enable ? .musicHighQualityStereo : .musicStandardStereo)
@@ -868,6 +1010,7 @@ extension KTVApiImpl {
}
}
+
}
// rtc的子频道代理回调
@@ -888,9 +1031,8 @@ extension KTVApiImpl: AgoraRtcEngineDelegate {
}
public func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
- agoraPrint("didOccurError: \(errorCode.rawValue)")
+ agoraPrintError("didOccurError: \(errorCode.rawValue)")
if errorCode != .joinChannelRejected {return}
- agoraPrint("join ex channel failed")
engine.setAudioScenario(.gameStreaming)
if joinChorusNewRole == .leadSinger {
mainSingerHasJoinChannelEx = false
@@ -928,27 +1070,17 @@ extension KTVApiImpl {
let ntpTime = dict["ntp"] as? Int,
let songId = dict["songIdentifier"] as? String
else { return }
- agoraPrint("realTime:\(realPosition) position:\(position) lastNtpTime:\(lastNtpTime) ntpTime:\(ntpTime) ntpGap:\(ntpTime - self.lastNtpTime) ")
- //如果接收到的歌曲和自己本地的歌曲不一致就不更新进度
-// guard songCode == self.songCode else {
-// agoraPrint("local songCode[\(songCode)] is not equal to recv songCode[\(self.songCode)] role: \(singerRole.rawValue)")
-// return
-// }
self.lastNtpTime = ntpTime
self.remotePlayerDuration = TimeInterval(duration)
let state = AgoraMediaPlayerState(rawValue: mainSingerState) ?? .stopped
-// self.lastMainSingerUpdateTime = Date().milListamp
-// self.remotePlayerPosition = TimeInterval(realPosition)
if self.playerState != state {
agoraPrint("[setLrcTime] recv state: \(self.playerState.rawValue)->\(state.rawValue) role: \(singerRole.rawValue) role: \(singerRole.rawValue)")
if state == .playing, singerRole == .coSinger, playerState == .openCompleted {
//如果是伴唱等待主唱开始播放,seek 到指定位置开始播放保证歌词显示位置准确
self.localPlayerPosition = self.lastMainSingerUpdateTime - Double(position)
- print("localPlayerPosition:playerKit:handleSetLrcTimeCommand \(localPlayerPosition)")
- agoraPrint("seek toPosition: \(position)")
mediaPlayer?.seek(toPosition: Int(position))
}
@@ -960,28 +1092,25 @@ extension KTVApiImpl {
self.remotePlayerPosition = TimeInterval(realPosition)
handleCoSingerRole(dict: dict)
} else if role == .audience {
- if self.songIdentifier == songId {
- self.lastMainSingerUpdateTime = Date().milListamp
- self.remotePlayerPosition = TimeInterval(realPosition)
+ if dict.keys.contains("ver") {
+ recvFromDataStream = false
} else {
- self.lastMainSingerUpdateTime = 0
- self.remotePlayerPosition = 0
+ recvFromDataStream = true
+ if self.songIdentifier == songId {
+ self.lastMainSingerUpdateTime = Date().milListamp
+ self.remotePlayerPosition = TimeInterval(realPosition)
+ } else {
+ self.lastMainSingerUpdateTime = 0
+ self.remotePlayerPosition = 0
+ }
+ handleAudienceRole(dict: dict)
}
- handleAudienceRole(dict: dict)
}
}
private func handlePlayerStateCommand(dict: [String: Any], role: KTVSingRole) {
let mainSingerState: Int = dict["state"] as? Int ?? 0
let state = AgoraMediaPlayerState(rawValue: mainSingerState) ?? .idle
-//
-// if state == .playing, singerRole == .coSinger, playerState == .openCompleted {
-// //如果是伴唱等待主唱开始播放,seek 到指定位置开始播放保证歌词显示位置准确
-// self.localPlayerPosition = getPlayerCurrentTime()
-// print("localPlayerPosition:playerKit:handlePlayerStateCommand \(localPlayerPosition)")
-// agoraPrint("seek toPosition: \(self.localPlayerPosition)")
-// mediaPlayer?.seek(toPosition: Int(self.localPlayerPosition))
-// }
agoraPrint("recv state with MainSinger: \(state.rawValue)")
syncPlayStateFromRemote(state: state, needDisplay: true)
@@ -1010,9 +1139,9 @@ extension KTVApiImpl {
let threshold = expectPosition - Int(localPosition)
let ntpTime = dict["ntp"] as? Int ?? 0
let time = dict["time"] as? Int64 ?? 0
- agoraPrint("checkNtp, diff:\(threshold), localNtp:\(getNtpTimeInMs()), localPosition:\(localPosition), audioPlayoutDelay:\(audioPlayoutDelay), remoteDiff:\(String(describing: ntpTime - Int(time)))")
+ // agoraPrint("checkNtp, diff:\(threshold), localNtp:\(getNtpTimeInMs()), localPosition:\(localPosition), audioPlayoutDelay:\(audioPlayoutDelay), remoteDiff:\(String(describing: ntpTime - Int(time)))")
if abs(threshold) > 50 {
- print("expectPosition:\(expectPosition)")
+ agoraPrint("expectPosition:\(expectPosition)")
mediaPlayer?.seek(toPosition: expectPosition)
}
}
@@ -1026,7 +1155,7 @@ extension KTVApiImpl {
let mainSingerUid = dict["uid"] as? Int ?? 0
songConfig?.mainSingerUid = mainSingerUid
let ret = apiConfig?.engine?.muteRemoteAudioStream(UInt(mainSingerUid), mute: true)
- print("ret:\(ret)")
+ agoraPrint("handleCosingerToLeadSinger:ret:\(String(describing: ret))")
}
}
}
@@ -1073,7 +1202,31 @@ extension KTVApiImpl {
if self.singerRole != .audience {
current = Date().milListamp - self.lastReceivedPosition + Double(self.localPosition)
}
- self.setProgress(with: Int(current) + Int(self.startHighTime))
+
+ if self.singerRole == .audience && !recvFromDataStream {
+
+ } else {
+ var curTime:Int64 = Int64(current) + Int64(self.startHighTime)
+ if songConfig?.songCutter == true {
+ curTime = curTime - preludeDuration > 0 ? curTime - preludeDuration : curTime
+ }
+ if self.singerRole != .audience {
+ current = Date().milListamp - self.lastReceivedPosition + Double(self.localPosition)
+
+ if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
+ var time: LrcTime = LrcTime()
+ time.forward = true
+ time.ts = curTime
+ time.songID = songIdentifier
+ time.type = .lrcTime
+ //大合唱的uid是musicuid
+ time.uid = Int32(apiConfig?.localUid ?? 0)
+ sendMetaMsg(with: time)
+ }
+ }
+ self.setProgress(with: Int(curTime))
+ }
+
self.oldPitch = self.pitch
})
}
@@ -1163,13 +1316,13 @@ extension KTVApiImpl {
resumeSing()
} else if (state == .playBackAllLoopsCompleted && needDisplay == true) {
getEventHander { delegate in
- delegate.onMusicPlayerStateChanged(state: state, error: .none, isLocal: true)
+ delegate.onMusicPlayerStateChanged(state: state, reason: .none, isLocal: true)
}
}
} else {
self.playerState = state
getEventHander { delegate in
- delegate.onMusicPlayerStateChanged(state: self.playerState, error: .none, isLocal: false)
+ delegate.onMusicPlayerStateChanged(state: self.playerState, reason: .none, isLocal: false)
}
}
}
@@ -1208,22 +1361,25 @@ extension KTVApiImpl {
return localNtpTime
}
- private func syncPlayState(state: AgoraMediaPlayerState, error: AgoraMediaPlayerError) {
- let dict: [String: Any] = ["cmd": "PlayerState", "userId": apiConfig?.localUid as Any, "state": state.rawValue, "error": "\(error.rawValue)"]
+ private func syncPlayState(state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
+ let dict: [String: Any] = ["cmd": "PlayerState", "userId": apiConfig?.localUid as Any, "state": state.rawValue, "error": "\(reason.rawValue)"]
sendStreamMessageWithDict(dict, success: nil)
}
- private func sendCustomMessage(with event: String, label: String) {
- apiConfig?.engine?.sendCustomReportMessage("scenarioAPI", category: "1_ios_4.0.0", event: event, label: label, value: 0)
+ private func sendCustomMessage(with event: String, dict: [String: Any]) {
+ apiRepoter?.reportFuncEvent(name: event, value: dict, ext: [:])
}
private func sendStreamMessageWithDict(_ dict: [String: Any], success: ((_ success: Bool) -> Void)?) {
let messageData = compactDictionaryToData(dict as [String: Any])
+ let sizeInBits = (messageData ?? Data()).count * 8
+ totalSize += sizeInBits
let code = apiConfig?.engine?.sendStreamMessage(dataStreamId, data: messageData ?? Data())
if code == 0 && success != nil { success!(true) }
if code != 0 {
agoraPrint("sendStreamMessage fail: \(String(describing: code))")
}
+// print("totalSize:\(totalSize)")
}
private func syncPlayState(_ state: AgoraMediaPlayerState) {
@@ -1235,6 +1391,14 @@ extension KTVApiImpl {
lrcControl?.onUpdatePitch(pitch: Float(self.pitch))
lrcControl?.onUpdateProgress(progress: pos > 200 ? pos - 200 : pos)
}
+
+ private func sendMetaMsg(with time: LrcTime) {
+ let data: Data? = try? time.serializedData()
+ let code = apiConfig?.engine?.sendAudioMetadata(data ?? Data())
+ if code != 0 {
+ agoraPrintError("sendStreamMessage fail: \(String(describing: code))")
+ }
+ }
}
//主要是MPK的回调
@@ -1253,11 +1417,10 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
"realTime":position_ms,
"ntp": timestamp_ms,
"playerState": self.playerState.rawValue,
- "songIdentifier": songIdentifier
- // "songCode": self.songCode
+ "songIdentifier": songIdentifier,
+ "ver":2,
]
- agoraPrint("position_ms:\(position_ms), ntp:\(getNtpTimeInMs()), delta:\(self.getNtpTimeInMs() - position_ms), autoPlayoutDelay:\(self.audioPlayoutDelay)")
- print("autoPlayoutDelay:\(self.audioPlayoutDelay)")
+ // agoraPrint("position_ms:\(position_ms), ntp:\(getNtpTimeInMs()), delta:\(self.getNtpTimeInMs() - position_ms), autoPlayoutDelay:\(self.audioPlayoutDelay)")
sendStreamMessageWithDict(dict) { _ in
@@ -1275,12 +1438,12 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
}
- func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol, didChangedTo state: AgoraMediaPlayerState, error: AgoraMediaPlayerError) {
+ func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol, didChangedTo state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
agoraPrint("agoraRtcMediaPlayer didChangedToState: \(state.rawValue) \(self.songCode)")
if isRelease {return}
if state == .openCompleted {
self.localPlayerPosition = Date().milListamp
- print("localPlayerPosition:playerKit:openCompleted \(localPlayerPosition)")
+ agoraPrint("localPlayerPosition:playerKit:openCompleted \(localPlayerPosition)")
self.playerDuration = TimeInterval(mediaPlayer?.getDuration() ?? 0)
if isMainSinger() { //主唱播放,通过同步消息“setLrcTime”通知伴唱play
playerKit.play()
@@ -1298,11 +1461,11 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
} else if state == .playing {
apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
self.localPlayerPosition = Date().milListamp - Double(mediaPlayer?.getPosition() ?? 0)
- print("localPlayerPosition:playerKit:playing \(localPlayerPosition)")
+ agoraPrint("localPlayerPosition:playerKit:playing \(localPlayerPosition)")
}
if isMainSinger() {
- syncPlayState(state: state, error: error)
+ syncPlayState(state: state, reason: reason)
}
self.playerState = state
agoraPrint("recv state with player callback : \(state.rawValue)")
@@ -1310,10 +1473,10 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
return
}
getEventHander { delegate in
- delegate.onMusicPlayerStateChanged(state: state, error: .none, isLocal: true)
+ delegate.onMusicPlayerStateChanged(state: state, reason: .none, isLocal: true)
}
}
-
+
private func isMainSinger() -> Bool {
return singerRole == .soloSinger || singerRole == .leadSinger
}
@@ -1322,7 +1485,7 @@ extension KTVApiImpl: AgoraRtcMediaPlayerDelegate {
//主要是MCC的回调
extension KTVApiImpl: AgoraMusicContentCenterEventDelegate {
- func onSongSimpleInfoResult(_ requestId: String, songCode: Int, simpleInfo: String?, errorCode: AgoraMusicContentCenterStatusCode) {
+ func onSongSimpleInfoResult(_ requestId: String, songCode: Int, simpleInfo: String?, reason: AgoraMusicContentCenterStateReason) {
if let jsonData = simpleInfo?.data(using: .utf8) {
do {
let jsonMsg = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any]
@@ -1330,96 +1493,102 @@ extension KTVApiImpl: AgoraMusicContentCenterEventDelegate {
let highPart = format["highPart"] as! [[String: Any]]
let highStartTime = highPart[0]["highStartTime"] as! Int
let highEndTime = highPart[0]["highEndTime"] as! Int
+ if highPart[0].keys.contains("preludeDuration") {
+ self.preludeDuration = highPart[0]["preludeDuration"] as! Int64
+ }
let time = highStartTime
startHighTime = time
self.lrcControl?.onHighPartTime(highStartTime: highStartTime, highEndTime: highEndTime)
} catch {
- print("Error while parsing JSON: \(error.localizedDescription)")
+ agoraPrintError("Error while parsing JSON: \(error.localizedDescription)")
}
}
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
}
-
- func onMusicChartsResult(_ requestId: String, result: [AgoraMusicChartInfo], errorCode: AgoraMusicContentCenterStatusCode) {
+
+ func onMusicChartsResult(_ requestId: String, result: [AgoraMusicChartInfo], reason: AgoraMusicContentCenterStateReason) {
guard let callback = musicChartDict[requestId] else {return}
- callback(requestId, errorCode, result)
+ callback(requestId, reason, result)
musicChartDict.removeValue(forKey: requestId)
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
}
- func onMusicCollectionResult(_ requestId: String, result: AgoraMusicCollection, errorCode: AgoraMusicContentCenterStatusCode) {
+ func onMusicCollectionResult(_ requestId: String, result: AgoraMusicCollection, reason: AgoraMusicContentCenterStateReason) {
guard let callback = musicSearchDict[requestId] else {return}
- callback(requestId, errorCode, result)
+ callback(requestId, reason, result)
musicSearchDict.removeValue(forKey: requestId)
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
}
- func onLyricResult(_ requestId: String, songCode: Int, lyricUrl: String?, errorCode: AgoraMusicContentCenterStatusCode) {
+ func onLyricResult(_ requestId: String, songCode: Int, lyricUrl: String?, reason: AgoraMusicContentCenterStateReason) {
+ agoraPrint("onLyricResult requestId: \(requestId) songCode: \(songCode) lyricUrl: \(lyricUrl ?? "") reason: \(reason.rawValue)")
guard let lrcUrl = lyricUrl else {return}
let callback = self.lyricCallbacks[requestId]
guard let lyricCallback = callback else { return }
self.lyricCallbacks.removeValue(forKey: requestId)
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
if lrcUrl.isEmpty {
lyricCallback(nil)
+ agoraPrintError("onLyricResult: lrcUrl.isEmpty")
return
}
lyricCallback(lrcUrl)
+ agoraPrint("onLyricResult: lrcUrl is \(lrcUrl)")
}
- func onPreLoadEvent(_ requestId: String, songCode: Int, percent: Int, lyricUrl: String?, status: AgoraMusicContentCenterPreloadStatus, errorCode: AgoraMusicContentCenterStatusCode) {
+ func onPreLoadEvent(_ requestId: String, songCode: Int, percent: Int, lyricUrl: String?, state: AgoraMusicContentCenterPreloadState, reason: AgoraMusicContentCenterStateReason) {
if let listener = self.loadMusicListeners.object(forKey: "\(songCode)" as NSString) as? IMusicLoadStateListener {
- listener.onMusicLoadProgress(songCode: songCode, percent: percent, status: status, msg: String(errorCode.rawValue), lyricUrl: lyricUrl)
+ listener.onMusicLoadProgress(songCode: songCode, percent: percent, state: state, msg: String(reason.rawValue), lyricUrl: lyricUrl)
}
- if (status == .preloading) { return }
- agoraPrint("songCode:\(songCode), status:\(status.rawValue), code:\(errorCode.rawValue)")
+ if (state == .preloading) { return }
+ agoraPrint("songCode:\(songCode), status:\(state.rawValue), code:\(reason.rawValue)")
let SongCode = "\(songCode)"
guard let block = self.musicCallbacks[SongCode] else { return }
self.musicCallbacks.removeValue(forKey: SongCode)
- if (errorCode == .errorGateway) {
+ if (reason == .errorGateway) {
getEventHander { delegate in
delegate.onTokenPrivilegeWillExpire()
}
}
- block(status, songCode)
+ block(state, songCode)
}
}
-extension Date {
- /// 获取当前 秒级 时间戳 - 10位
- ///
- var timeStamp : TimeInterval {
- let timeInterval: TimeInterval = self.timeIntervalSince1970
- return timeInterval
- }
- /// 获取当前 毫秒级 时间戳 - 13位
- var milListamp : TimeInterval {
- let timeInterval: TimeInterval = self.timeIntervalSince1970
- let millisecond = CLongLong(round(timeInterval*1000))
- return TimeInterval(millisecond)
- }
-}
+//extension Date {
+// /// 获取当前 秒级 时间戳 - 10位
+// ///
+// var timeStamp : TimeInterval {
+// let timeInterval: TimeInterval = self.timeIntervalSince1970
+// return timeInterval
+// }
+// /// 获取当前 毫秒级 时间戳 - 13位
+// var milListamp : TimeInterval {
+// let timeInterval: TimeInterval = self.timeIntervalSince1970
+// let millisecond = CLongLong(round(timeInterval*1000))
+// return TimeInterval(millisecond)
+// }
+//}
extension KTVApiImpl: KTVApiRTCDelegate {
func didJoinChannel(channel: String, withUid uid: UInt, elapsed: Int) {
- print("ktvapi加入主频道成功")
+ agoraPrint("ktvapi加入主频道成功")
}
func didJoinedOfUid(uid: UInt, elapsed: Int) {
@@ -1454,11 +1623,12 @@ extension KTVApiImpl: KTVApiRTCDelegate {
func didAudioPublishStateChange(channelId: String, oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int32) {
self.isPublishAudio = newState == .published
enableProfessionalStreamerMode(self.enableProfessional)
- print("PublishStateChange:\(newState)")
+ agoraPrint("PublishStateChange:\(newState)")
}
func receiveStreamMessageFromUid(uid: UInt, streamId: Int, data: Data) {
let role = singerRole
+ if isRelease {return}
guard let dict = dataToDictionary(data: data), let cmd = dict["cmd"] as? String else { return }
switch cmd {
@@ -1481,8 +1651,8 @@ extension KTVApiImpl: KTVApiRTCDelegate {
}
func didRTCAudioRouteChanged(routing: AgoraAudioOutputRouting) {
- print("Route changed:\(routing)")
- let headPhones: [AgoraAudioOutputRouting] = [.headset, .headsetBluetooth, .headsetNoMic]
+ agoraPrint("Route changed:\(routing)")
+ let headPhones: [AgoraAudioOutputRouting] = [.headset, .bluetoothDeviceHfp, .bluetoothDeviceA2dp, .headsetNoMic]
let wearHeadPhone: Bool = headPhones.contains(routing)
if wearHeadPhone == self.isWearingHeadPhones {
return
@@ -1490,7 +1660,17 @@ extension KTVApiImpl: KTVApiRTCDelegate {
self.isWearingHeadPhones = wearHeadPhone
enableProfessionalStreamerMode(self.enableProfessional)
}
+
+ func audioMetadataReceived(uid: UInt, metadata: Data) {
+ guard let time: LrcTime = try? LrcTime(serializedData: metadata) else {return}
+ if time.type == .lrcTime && self.singerRole == .audience {
+ self.setProgress(with: Int(time.ts))
+ }
+ }
+ @objc func didAudioMetadataReceived( uid: UInt, metadata: Data) {
+
+ }
}
/*----这一块的代码主要是用来处理主频道的RTC代理事件,外部不再需要手动转代理,😁---*/
@@ -1502,6 +1682,7 @@ protocol KTVApiRTCDelegate: NSObjectProtocol {
func didAudioPublishStateChange(channelId: String, oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int32)
func receiveStreamMessageFromUid(uid: UInt, streamId: Int, data: Data)
func localAudioStats(stats: AgoraRtcLocalAudioStats)
+ func audioMetadataReceived( uid: UInt, metadata: Data)
}
class KTVApiRTCDelegateHandler: NSObject, AgoraRtcEngineDelegate {
@@ -1538,5 +1719,15 @@ class KTVApiRTCDelegateHandler: NSObject, AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) {
delegate.localAudioStats(stats: stats)
}
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, audioMetadataReceived uid: UInt, metadata: Data) {
+ delegate.audioMetadataReceived(uid: uid, metadata: metadata)
+ }
}
+
+extension KTVApiImpl {
+ @objc public func isSongLoading(songCode: String) -> Bool {
+ return musicCallbacks[songCode] == nil ? false : true
+ }
+}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVGiantChorusApiImpl.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVGiantChorusApiImpl.swift
new file mode 100644
index 0000000..cdc5458
--- /dev/null
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/KTVGiantChorusApiImpl.swift
@@ -0,0 +1,2059 @@
+//
+// KTVApiImpl.swift
+// AgoraEntScenarios
+//
+// Created by wushengtao on 2023/3/14.
+//
+
+import Foundation
+import AgoraRtcKit
+
+/// 加载歌曲状态
+@objc fileprivate enum KTVLoadSongState: Int {
+ case idle = -1 //空闲
+ case ok = 0 //成功
+ case failed //失败
+ case inProgress //加载中
+}
+
+fileprivate enum KTVSongMode: Int {
+ case songCode
+ case songUrl
+}
+
+@objc class KTVGiantChorusApiImpl: NSObject, KTVApiDelegate{
+
+ private var apiConfig: GiantChorusConfiguration?
+
+ private var songConfig: KTVSongConfiguration?
+ private var subChorusConnection: AgoraRtcConnection?
+ private var singChannelConnection: AgoraRtcConnection?
+ private var mpkConnection: AgoraRtcConnection?
+
+ private var eventHandlers: NSHashTable = NSHashTable.weakObjects()
+ private var loadMusicListeners: NSMapTable = NSMapTable(keyOptions: .copyIn, valueOptions: .weakMemory)
+
+ // private var musicPlayer: AgoraRtcMediaPlayerProtocol? //mcc
+ private var mediaPlayer: AgoraRtcMediaPlayerProtocol? //local
+ private var mcc: AgoraMusicContentCenter?
+
+ private var loadSongMap = Dictionary()
+ private var lyricUrlMap = Dictionary()
+ private var loadDict = Dictionary()
+ private var lyricCallbacks = Dictionary()
+ private var musicCallbacks = Dictionary()
+
+ private var hasSendPreludeEndPosition: Bool = false
+ private var hasSendEndPosition: Bool = false
+
+ //multipath
+ private var enableMultipathing: Bool = true
+
+ private var audioPlayoutDelay: NSInteger = 0
+ private var isNowMicMuted: Bool = false
+ private var loadSongState: KTVLoadSongState = .idle
+ private var lastNtpTime: Int = 0
+ private var startHighTime: Int = 0
+ private var isRelease: Bool = false
+ private var songUrl2: String = ""
+ private var playerState: AgoraMediaPlayerState = .idle {
+ didSet {
+ agoraPrint("playerState did changed: \(oldValue.rawValue)->\(playerState.rawValue)")
+ updateRemotePlayBackVolumeIfNeed()
+ updateTimer(with: playerState)
+ }
+ }
+ private var pitch: Double = 0
+ private var localPlayerPosition: TimeInterval = 0
+ private var remotePlayerPosition: TimeInterval = 0
+ private var remotePlayerDuration: TimeInterval = 0
+ private var localPlayerSystemTime: TimeInterval = 0
+ private var lastMainSingerUpdateTime: TimeInterval = 0
+ private var playerDuration: TimeInterval = 0
+ // private lazy var apiDelegateHandler = KTVApiRTCDelegateHandler(with: self)
+
+ private var musicChartDict: [String: MusicChartCallBacks] = [:]
+ private var musicSearchDict: Dictionary = Dictionary()
+ private var onJoinExChannelCallBack : JoinExChannelCallBack?
+ private var mainSingerHasJoinChannelEx: Bool = false
+ private var dataStreamId: Int = 0
+ private var lastReceivedPosition: TimeInterval = 0
+ private var localPosition: Int = 0
+
+ private var songMode: KTVSongMode = .songCode
+ private var useCustomAudioSource:Bool = false
+ private var songUrl: String = ""
+ private var songCode: Int = 0
+ private var songIdentifier: String = ""
+
+ private var singerRole: KTVSingRole = .audience {
+ didSet {
+ agoraPrint("singerRole changed: \(oldValue.rawValue)->\(singerRole.rawValue)")
+ }
+ }
+ private var lrcControl: KTVLrcViewDelegate?
+
+ private var timer: Timer?
+ private var isPause: Bool = false
+
+ private var singingScore: Int = 0
+
+ public var remoteVolume: Int = 30
+ private var joinChorusNewRole: KTVSingRole = .audience
+ private var oldPitch: Double = 0
+ private var isWearingHeadPhones: Bool = false
+ private var enableProfessional: Bool = false
+ private var isPublishAudio: Bool = false
+ private var audioRouting: Int = -1
+ private var recvFromDataStream = false
+ //大合唱独有
+ private var mStopSyncPitch = true
+ private var mSyncPitchTimer: DispatchSourceTimer?
+ private var mStopSyncScore = true
+ private var mSyncScoreTimer: DispatchSourceTimer?
+ private var mStopSyncCloudConvergenceStatus = true
+ private var mSyncCloudConvergenceStatusTimer: DispatchSourceTimer?
+ private var mStopProcessDelay = true
+ private var processDelayFuture: DispatchSourceTimer?
+ private var processSubscribeFuture: DispatchSourceTimer?
+ private var subScribeSingerMap = [Int: Int]() //
+ private var singerList = [Int]() //
+ private var mainSingerDelay = 0
+
+ private let tag = "KTV_API_LOG"
+ private let messageId = "agora:scenarioAPI"
+ private let version = "5.0.0"
+ private let lyricSyncVersion = 2
+
+ private var apiRepoter: APIReporter?
+
+ deinit {
+ mcc?.register(nil)
+ agoraPrint("deinit KTVApiImpl")
+ }
+
+ func createKTVGiantChorusApi(config: GiantChorusConfiguration) {
+ self.apiConfig = config
+ agoraPrint("createKTVGiantChorusApi")
+ self.singChannelConnection = AgoraRtcConnection(channelId: config.chorusChannelName, localUid: config.localUid)
+
+ setParams()
+
+ if config.musicType == .mcc {
+ // ------------------ 初始化内容中心 ------------------
+ let contentCenterConfiguration = AgoraMusicContentCenterConfig()
+ contentCenterConfiguration.appId = config.appId
+ contentCenterConfiguration.mccUid = config.localUid
+ contentCenterConfiguration.token = config.rtmToken
+ contentCenterConfiguration.rtcEngine = config.engine
+ contentCenterConfiguration.maxCacheSize = UInt(config.maxCacheSize)
+ if let domain = config.mccDomain {
+ contentCenterConfiguration.mccDomain = domain
+ }
+ mcc = AgoraMusicContentCenter.sharedContentCenter(config: contentCenterConfiguration)
+ mcc?.register(self)
+ // ------------------ 初始化音乐播放器实例 ------------------
+ mediaPlayer = mcc?.createMusicPlayer(delegate: self)
+ mediaPlayer?.adjustPlayoutVolume(50)
+ mediaPlayer?.adjustPublishSignalVolume(50)
+ } else {
+ mediaPlayer = apiConfig?.engine?.createMediaPlayer(with: self)
+ // 音量最佳实践调整
+ mediaPlayer?.adjustPlayoutVolume(50)
+ mediaPlayer?.adjustPublishSignalVolume(50)
+ }
+
+ apiRepoter = APIReporter(type: .ktv, version: version, engine: apiConfig?.engine ?? AgoraRtcEngineKit())
+
+ initTimer()
+ mediaPlayer?.setPlayerOption("play_pos_change_callback", value: 100)
+ apiConfig?.engine?.setDelegateEx(self, connection: mpkConnection ?? AgoraRtcConnection())
+ startSyncPitch()
+ startSyncScore()
+ startSyncCloudConvergenceStatus()
+ }
+
+ private func setParams() {
+ guard let engine = self.apiConfig?.engine else {return}
+ engine.setParameters("{\"rtc.enable_nasa2\": true}")
+ engine.setParameters("{\"rtc.ntp_delay_drop_threshold\": 1000}")
+ engine.setParameters("{\"rtc.video.enable_sync_render_ntp\": true}")
+ engine.setParameters("{\"rtc.net.maxS2LDelay\": 800}")
+ engine.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\": true}")
+ engine.setParameters("{\"rtc.net.maxS2LDelayBroadcast\": 400}")
+ engine.setParameters("{\"che.audio.neteq.prebuffer\": true}")
+ engine.setParameters("{\"che.audio.neteq.prebuffer_max_delay\": 600}")
+ engine.setParameters("{\"che.audio.max_mixed_participants\": 8}")
+ engine.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ engine.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
+ engine.setParameters("{\"che.audio.neteq.targetlevel_offset\": 20}")
+ engine.setParameters("{\"che.audio.uplink_apm_async_process\": true}")
+ // 标准音质
+ engine.setParameters("{\"che.audio.aec.split_srate_for_48k\": 16000}")
+ engine.setParameters("{\"che.audio.ans.noise_gate\": 20}")//
+ engine.setParameters("{\"rtc.use_audio4\": true}")
+
+ //4.3.0 add
+ // mutipath
+ enableMultipathing = true
+ engine.setParameters("{\"rtc.enable_tds_request_on_join\": true}")
+ engine.setParameters("{\"rtc.remote_path_scheduling_strategy\": 0}")
+ engine.setParameters("{\"rtc.path_scheduling_strategy\": 0}")
+ engine.setParameters("{\"rtc.enableMultipath\": true}")
+
+ // 数据上报
+ engine.setParameters("{\"rtc.direct_send_custom_event\": true}")
+ // engine.setParameters("{\"rtc.qos_for_test_purpose\": true}")
+ }
+
+ func renewInnerDataStreamId() {
+ let dataStreamConfig = AgoraDataStreamConfig()
+ dataStreamConfig.ordered = false
+ dataStreamConfig.syncWithAudio = true
+ self.apiConfig?.engine?.createDataStreamEx(&dataStreamId, config: dataStreamConfig, connection: singChannelConnection ?? AgoraRtcConnection())
+
+ sendCustomMessage(with: "renewInnerDataStreamId", dict: [:])
+ agoraPrint("renewInnerDataStreamId")
+ }
+}
+
+//MARK: KTVApiDelegate
+extension KTVGiantChorusApiImpl {
+
+ func objectContent(of object: Any) -> [String: Any] {
+ var content = [String: Any]()
+
+ let mirror = Mirror(reflecting: object)
+ for child in mirror.children {
+ if let propertyName = child.label {
+ if let convertibleValue = convertToJSONSerializable(child.value) {
+ content[propertyName] = convertibleValue
+ }
+ }
+ }
+
+ return content
+ }
+
+ func convertToJSONSerializable(_ value: Any) -> Any? {
+ switch value {
+ case let value as String:
+ return value
+ case let value as Int:
+ return value
+ case let value as Double:
+ return value
+ case let value as Bool:
+ return value
+ case let value as Int?:
+ return value
+ case let value as Double?:
+ return value
+ case let value as Bool?:
+ return value
+ case let value as String?:
+ return value
+ default:
+ return nil
+ }
+ }
+
+ func getMusicContentCenter() -> AgoraMusicContentCenter? {
+ return mcc
+ }
+
+ func setLrcView(view: KTVLrcViewDelegate) {
+ sendCustomMessage(with: "renewInnerDataStreamId", dict: [:])
+ lrcControl = view
+ }
+
+ //主要针对本地歌曲播放的主唱伴奏切换的 loadmusic MCC直接忽视这个方法
+ func load2Music(url1: String, url2: String, config: KTVSongConfiguration) {
+ agoraPrint("load2Music called: songUrl url1:(url1),url2:(url2)")
+ self.songMode = .songUrl
+ self.songConfig = config
+ self.songIdentifier = config.songIdentifier
+ self.songUrl = url1
+ self.songUrl2 = url2
+
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// switchSingerRole(newRole: .soloSinger) { state, failRes in
+//
+// }
+// }
+// startSing(url: url1, startPos: 0)
+// }
+ }
+
+ //主要针对本地歌曲播放的主唱伴奏切换的 MCC直接忽视这个方法
+ func switchPlaySrc(url: String, syncPts: Bool) {
+ agoraPrint("switchPlaySrc called: \(url)")
+
+ if self.songUrl != url && self.songUrl2 != url {
+ print("switchPlaySrc failed: canceled")
+ return
+ }
+
+ let curPlayPosition: Int = syncPts ? mediaPlayer?.getPosition() ?? 0 : 0
+ mediaPlayer?.stop()
+ startSing(url: url, startPos: curPlayPosition)
+ }
+
+ func loadMusic(songCode: Int, config: KTVSongConfiguration, onMusicLoadStateListener: IMusicLoadStateListener) {
+ sendCustomMessage(with: "loadMusicWithSongCode:\(songCode)", dict: objectContent(of: config))
+ agoraPrint("loadMusic songCode:\(songCode) ")
+ self.songMode = .songCode
+ self.songCode = songCode
+ self.songIdentifier = config.songIdentifier
+ _loadMusic(config: config, mode: config.mode, onMusicLoadStateListener: onMusicLoadStateListener)
+ }
+
+ func loadMusic(config: KTVSongConfiguration, url: String) {
+ sendCustomMessage(with: "loadMusicWithUrl:\(url)", dict: objectContent(of: config))
+ agoraPrint("loadMusic url:\(url)")
+ self.songMode = .songUrl
+ self.songUrl = url
+ self.songIdentifier = config.songIdentifier
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if singerRole != .leadSinger {
+// switchSingerRole(newRole: .soloSinger) { _, _ in
+//
+// }
+// }
+// startSing(url: url, startPos: 0)
+// }
+ }
+
+ func getMusicPlayer() -> AgoraRtcMediaPlayerProtocol? {
+ return mediaPlayer
+ }
+
+ func addEventHandler(ktvApiEventHandler: KTVApiEventHandlerDelegate) {
+ sendCustomMessage(with: "addEventHandler", dict: [:])
+ agoraPrint("addEventHandler")
+ if eventHandlers.contains(ktvApiEventHandler) {
+ return
+ }
+ eventHandlers.add(ktvApiEventHandler)
+ }
+
+ func removeEventHandler(ktvApiEventHandler: KTVApiEventHandlerDelegate) {
+ sendCustomMessage(with: "removeEventHandler", dict: [:])
+ agoraPrint("removeEventHandler")
+ eventHandlers.remove(ktvApiEventHandler)
+ }
+
+ func cleanCache() {
+ sendCustomMessage(with: "cleanCache", dict: [:])
+ isRelease = true
+ mediaPlayer?.stop()
+ freeTimer()
+ agoraPrint("cleanCache")
+ singerRole = .audience
+
+ stopSyncCloudConvergenceStatus()
+ stopSyncScore()
+ singingScore = 0
+ lrcControl = nil
+ lyricCallbacks.removeAll()
+ musicCallbacks.removeAll()
+ onJoinExChannelCallBack = nil
+ loadMusicListeners.removeAllObjects()
+ apiConfig?.engine?.destroyMediaPlayer(mediaPlayer)
+ mediaPlayer = nil
+ if apiConfig?.musicType == .mcc {
+ mcc?.register(nil)
+ mcc = nil
+ }
+ apiConfig = nil
+ AgoraMusicContentCenter.destroy()
+ self.eventHandlers.removeAllObjects()
+ }
+
+ @objc public func enableMutipath(enable: Bool) {
+ sendCustomMessage(with: "enableMutipath", dict: ["enable":enable])
+ agoraPrint("enableMutipath:\(enable)")
+ enableMultipathing = enable
+ if singerRole == .coSinger || singerRole == .leadSinger {
+ if let subChorusConnection = subChorusConnection {
+ apiConfig?.engine?.setParametersEx("{\"rtc.enableMultipath\": \(enable), \"rtc.path_scheduling_strategy\": 0, \"rtc.remote_path_scheduling_strategy\": 0}", connection: subChorusConnection)
+ }
+ }
+ }
+
+ func renewToken(rtmToken: String, chorusChannelRtcToken: String) {
+ let dict: [String: Any] = [
+ "rtmToken":rtmToken,
+ "chorusChannelRtcToken":chorusChannelRtcToken
+ ]
+ sendCustomMessage(with: "renewToken", dict: dict)
+ agoraPrint("renewToken rtmToken:\(rtmToken) chorusChannelRtcToken:\(chorusChannelRtcToken)")
+ // 更新RtmToken
+ mcc?.renewToken(rtmToken)
+ // 更新合唱频道RtcToken
+ if let subChorusConnection = subChorusConnection {
+ let channelMediaOption = AgoraRtcChannelMediaOptions()
+ channelMediaOption.token = chorusChannelRtcToken
+ apiConfig?.engine?.updateChannelEx(with: channelMediaOption, connection: subChorusConnection)
+ }
+ }
+
+ func fetchMusicCharts(completion: @escaping MusicChartCallBacks) {
+ sendCustomMessage(with: "fetchMusicCharts", dict: [:])
+ agoraPrint("fetchMusicCharts")
+ let requestId = mcc!.getMusicCharts()
+ musicChartDict[requestId] = completion
+ }
+
+ func searchMusic(musicChartId: Int,
+ page: Int,
+ pageSize: Int,
+ jsonOption: String,
+ completion:@escaping (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void) {
+ agoraPrint("searchMusic with musicChartId: \(musicChartId)")
+ let dict: [String: Any] = [
+ "musicChartId":musicChartId,
+ "page": page,
+ "pageSize": pageSize,
+ "jsonOption": jsonOption
+ ]
+ sendCustomMessage(with: "searchMusic", dict: dict)
+ let requestId = mcc!.getMusicCollection(musicChartId: musicChartId, page: page, pageSize: pageSize, jsonOption: jsonOption)
+ musicSearchDict[requestId] = completion
+ }
+
+ func searchMusic(keyword: String,
+ page: Int,
+ pageSize: Int,
+ jsonOption: String,
+ completion: @escaping (String, AgoraMusicContentCenterStateReason, AgoraMusicCollection) -> Void) {
+ agoraPrint("searchMusic with keyword: \(keyword)")
+ let dict: [String: Any] = [
+ "keyword": keyword,
+ "page": page,
+ "pageSize": pageSize,
+ "jsonOption": jsonOption
+ ]
+ sendCustomMessage(with: "searchMusic", dict: dict)
+ let requestId = mcc!.searchMusic(keyWord: keyword, page: page, pageSize: pageSize, jsonOption: jsonOption)
+ musicSearchDict[requestId] = completion
+ }
+
+// func switchSingerRole(newRole: KTVSingRole, onSwitchRoleState: @escaping (KTVSwitchRoleState, KTVSwitchRoleFailReason) -> Void) {
+// let oldRole = singerRole
+// self.switchSingerRole(oldRole: oldRole, newRole: newRole, token: apiConfig?.chorusChannelToken ?? "", stateCallBack: onSwitchRoleState)
+// }
+
+ /**
+ * 恢复播放
+ */
+ @objc public func resumeSing() {
+ sendCustomMessage(with: "resumeSing", dict: [:])
+ agoraPrint("resumeSing")
+ if mediaPlayer?.getPlayerState() == .paused {
+ mediaPlayer?.resume()
+ } else {
+ let ret = mediaPlayer?.play()
+ agoraPrint("resumeSing ret: \(ret ?? -1)")
+ }
+ }
+
+ /**
+ * 暂停播放
+ */
+ @objc public func pauseSing() {
+ sendCustomMessage(with: "pauseSing", dict: [:])
+ agoraPrint("pauseSing")
+ mediaPlayer?.pause()
+ }
+
+ /**
+ * 调整进度
+ */
+ @objc public func seekSing(time: NSInteger) {
+ sendCustomMessage(with: "seekSing", dict: ["time":time])
+ agoraPrint("seekSing")
+ mediaPlayer?.seek(toPosition: time)
+ }
+
+ /**
+ * 选择音轨,原唱、伴唱
+ */
+// @objc public func selectPlayerTrackMode(mode: KTVPlayerTrackMode) {
+// apiConfig?.engine.selectAudioTrack(mode == .original ? 0 : 1)
+// }
+
+ /**
+ * 设置当前mic开关状态
+ */
+ @objc public func muteMic(muteStatus: Bool) {
+ sendCustomMessage(with: "setMicStatus", dict: ["muteStatus":muteStatus])
+ agoraPrint("setMicStatus status:\(muteStatus)")
+ self.isNowMicMuted = muteStatus
+ if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
+ apiConfig?.engine?.adjustRecordingSignalVolume(muteStatus ? 0 : 100)
+ } else {
+ apiConfig?.engine?.muteLocalAudioStream(muteStatus)
+ }
+ }
+
+ @objc public func removeMusic(songCode: Int) {
+ sendCustomMessage(with: "removeMusic", dict: ["songCode": songCode])
+ agoraPrint("removeMusic:\(songCode)")
+ let ret: Int = mcc?.removeCache(songCode: songCode) ?? 0
+ if ret < 0 {
+ agoraPrint("removeMusic failed: ret:\(ret)")
+ }
+ }
+
+ private func agoraPrint(_ message: String) {
+ apiRepoter?.writeLog(content: message, level: .info)
+ }
+
+ private func agoraPrintError(_ message: String) {
+ apiRepoter?.writeLog(content: message, level: .error)
+ }
+
+}
+
+// 主要是角色切换,加入合唱,加入多频道,退出合唱,退出多频道
+extension KTVGiantChorusApiImpl {
+// private func switchSingerRole(oldRole: KTVSingRole, newRole: KTVSingRole, token: String, stateCallBack:@escaping ISwitchRoleStateListener) {
+// // agoraPrint("switchSingerRole oldRole: \(oldRole.rawValue), newRole: \(newRole.rawValue)")
+// if oldRole == .audience && newRole == .soloSinger {
+// // 1、KTVSingRoleAudience -》KTVSingRoleMainSinger
+// singerRole = newRole
+// becomeSoloSinger()
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .audience, newRole: .soloSinger)
+// }
+//
+// stateCallBack(.success, .none)
+// } else if oldRole == .audience && newRole == .leadSinger {
+// becomeSoloSinger()
+// joinChorus(role: newRole, token: token, joinExChannelCallBack: {[weak self] flag, status in
+// guard let self = self else {return}
+// //还原临时变量为观众
+// self.joinChorusNewRole = .audience
+//
+// if flag == true {
+// self.singerRole = newRole
+// self.getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .audience, newRole: .leadSinger)
+// }
+// stateCallBack(.success, .none)
+// } else {
+// self.leaveChorus(role: .leadSinger)
+// stateCallBack(.fail, .joinChannelFail)
+// }
+// })
+//
+// } else if oldRole == .soloSinger && newRole == .audience {
+// stopSing()
+// singerRole = newRole
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .soloSinger, newRole: .audience)
+// }
+//
+// stateCallBack(.success, .none)
+// } else if oldRole == .audience && newRole == .coSinger {
+// joinChorus(role: newRole, token: token, joinExChannelCallBack: {[weak self] flag, status in
+// guard let self = self else {return}
+// //还原临时变量为观众
+// self.joinChorusNewRole = .audience
+// if flag == true {
+// self.singerRole = newRole
+// //TODO(chenpan):如果观众变成伴唱,需要重置state,防止同步主唱state因为都是playing不会修改
+// //后面建议改成remote state(通过data stream获取)和local state(通过player didChangedToState获取)
+// self.playerState = self.mediaPlayer?.getPlayerState() ?? .idle
+// self.getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .audience, newRole: .coSinger)
+// }
+// stateCallBack(.success, .none)
+// } else {
+// self.leaveChorus(role: .coSinger)
+// stateCallBack(.fail, .joinChannelFail)
+// }
+// })
+// } else if oldRole == .coSinger && newRole == .audience {
+// leaveChorus(role: .coSinger)
+// singerRole = newRole
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .coSinger, newRole: .audience)
+// }
+//
+// stateCallBack(.success, .none)
+// } else if oldRole == .soloSinger && newRole == .leadSinger {
+// joinChorus(role: newRole, token: token, joinExChannelCallBack: {[weak self] flag, status in
+// guard let self = self else {return}
+// //还原临时变量为观众
+// self.joinChorusNewRole = .audience
+// if flag == true {
+// self.singerRole = newRole
+// self.getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .soloSinger, newRole: .leadSinger)
+// }
+// stateCallBack(.success, .none)
+// } else {
+// self.leaveChorus(role: .leadSinger)
+// stateCallBack(.fail, .joinChannelFail)
+// }
+// })
+// } else if oldRole == .leadSinger && newRole == .soloSinger {
+// leaveChorus(role: .leadSinger)
+// singerRole = newRole
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .leadSinger, newRole: .soloSinger)
+// }
+//
+// stateCallBack(.success, .none)
+// } else if oldRole == .leadSinger && newRole == .audience {
+// leaveChorus(role: .leadSinger)
+// stopSing()
+// singerRole = newRole
+// getEventHander { delegate in
+// delegate.onSingerRoleChanged(oldRole: .leadSinger, newRole: .audience)
+// }
+//
+// stateCallBack(.success, .none)
+// } else {
+// stateCallBack(.fail, .noPermission)
+// agoraPrint("Error!You can not switch role from \(oldRole.rawValue) to \(newRole.rawValue)!")
+// }
+//
+// }
+
+ func switchSingerRole(newRole: KTVSingRole, onSwitchRoleState: @escaping (KTVSwitchRoleState, KTVSwitchRoleFailReason) -> Void) {
+
+ agoraPrint("switchSingerRole oldRole: \(singerRole), newRole: \(newRole)")
+ let oldRole = singerRole
+
+ if singerRole == .audience && newRole == .leadSinger {
+ // 1、Audience -》LeadSinger
+ // 离开观众频道
+ apiConfig?.engine?.leaveChannelEx(AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0))
+ joinChorus(newRole: newRole)
+ self.singerRole = newRole
+ self.getEventHander { delegate in
+ delegate.onSingerRoleChanged(oldRole: .audience, newRole: .leadSinger)
+ }
+ onSwitchRoleState(.success, .none)
+ } else if singerRole == .audience && newRole == .coSinger {
+ // 2、Audience -》CoSinger
+ // 离开观众频道
+ apiConfig?.engine?.leaveChannelEx(AgoraRtcConnection( channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0))
+ joinChorus(newRole: newRole)
+ singerRole = newRole
+ self.getEventHander { delegate in
+ delegate.onSingerRoleChanged(oldRole: .audience, newRole: .coSinger)
+ }
+ onSwitchRoleState(.success, .none)
+ } else if singerRole == .coSinger && newRole == .audience {
+ // 3、CoSinger -》Audience
+ leaveChorus2(role: singerRole)
+ // 加入观众频道
+ apiConfig?.engine?.joinChannelEx(byToken: apiConfig?.audienceChannelToken,
+ connection: AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0),
+ delegate: self,
+ mediaOptions: AgoraRtcChannelMediaOptions(),
+ joinSuccess: {[weak self] _,_, _ in
+ })
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0))
+ self.singerRole = newRole
+ self.getEventHander { delegate in
+ delegate.onSingerRoleChanged(oldRole: oldRole, newRole: newRole)
+ }
+ onSwitchRoleState(.success, .none)
+ } else if singerRole == .leadSinger && newRole == .audience {
+ // 4、LeadSinger -》Audience
+ stopSing()
+ leaveChorus2(role: singerRole)
+ // 加入观众频道
+ apiConfig?.engine?.joinChannelEx(byToken: apiConfig?.audienceChannelToken,
+ connection: AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0),
+ delegate: self,
+ mediaOptions: AgoraRtcChannelMediaOptions(),
+ joinSuccess: {[weak self] _,_, _ in
+ })
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: AgoraRtcConnection(channelId: apiConfig?.channelName ?? "", localUid: apiConfig?.localUid ?? 0))
+ self.singerRole = newRole
+ self.getEventHander { delegate in
+ delegate.onSingerRoleChanged(oldRole: oldRole, newRole: newRole)
+ }
+ onSwitchRoleState(.success, .none)
+ } else {
+ onSwitchRoleState(.fail, .noPermission)
+ print("Error! You can not switch role from \(singerRole) to \(newRole)!")
+ }
+ }
+
+ private func becomeSoloSinger() {
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 80000}")
+ apiConfig?.engine?.setAudioScenario(.chorus)
+ agoraPrint("becomeSoloSinger")
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ mediaOption.autoSubscribeAudio = true
+ //mediaOption.autoSubscribeVideo = true
+ if apiConfig?.musicType == .mcc {
+ mediaOption.publishMediaPlayerId = Int(mediaPlayer?.getMediaPlayerId() ?? 0)
+ } else {
+ mediaOption.publishMediaPlayerId = Int(mediaPlayer?.getMediaPlayerId() ?? 0)
+ }
+ mediaOption.publishMediaPlayerAudioTrack = true
+ apiConfig?.engine?.updateChannel(with: mediaOption)
+ }
+
+ /**
+ * 加入合唱
+ */
+ private func joinChorus(role: KTVSingRole, token: String, joinExChannelCallBack: @escaping JoinExChannelCallBack) {
+ self.onJoinExChannelCallBack = joinExChannelCallBack
+ if role == .leadSinger {
+ agoraPrint("joinChorus: KTVSingRoleMainSinger")
+ joinChorus2ndChannel(newRole: role, token: token)
+ } else if role == .coSinger {
+
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ mediaOption.autoSubscribeAudio = true
+ // mediaOption.autoSubscribeVideo = true
+ mediaOption.publishMediaPlayerAudioTrack = false
+ apiConfig?.engine?.updateChannel(with: mediaOption)
+
+ if apiConfig?.musicType == .mcc {
+ (mediaPlayer as? AgoraMusicPlayerProtocol)?.openMedia(songCode: self.songCode , startPos: 0)
+ } else {
+ mediaPlayer?.open(self.songUrl, startPos: 0)
+ }
+
+ joinChorus2ndChannel(newRole: role, token: token)
+
+ } else if role == .audience {
+ agoraPrint("joinChorus fail!")
+ }
+ }
+
+ private func joinChorus2ndChannel(newRole: KTVSingRole, token: String) {
+ let role = newRole
+ if role == .soloSinger || role == .audience {
+ agoraPrint("joinChorus2ndChannel with wrong role")
+ return
+ }
+
+ agoraPrint("joinChorus2ndChannel role: \(role.rawValue)")
+ if newRole == .coSinger {
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ apiConfig?.engine?.setAudioScenario(.chorus)
+ }
+
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ // main singer do not subscribe 2nd channel
+ // co singer auto sub
+ mediaOption.autoSubscribeAudio = role != .leadSinger
+ // mediaOption.autoSubscribeVideo = false
+ mediaOption.publishMicrophoneTrack = newRole == .leadSinger
+ mediaOption.enableAudioRecordingOrPlayout = role != .leadSinger
+ mediaOption.clientRoleType = .broadcaster
+
+ let rtcConnection = AgoraRtcConnection()
+ rtcConnection.channelId = apiConfig?.chorusChannelName ?? ""
+ rtcConnection.localUid = UInt(apiConfig?.localUid ?? 0)
+ subChorusConnection = rtcConnection
+
+ joinChorusNewRole = role
+ let ret = apiConfig?.engine?.joinChannelEx(byToken: token, connection: rtcConnection, delegate: self, mediaOptions: mediaOption, joinSuccess: nil)
+ agoraPrint("joinChannelEx ret: \(ret ?? -999)")
+ if newRole == .coSinger {
+ let uid = UInt(songConfig?.mainSingerUid ?? 0)
+ let ret =
+ apiConfig?.engine?.muteRemoteAudioStreamEx(uid, mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ agoraPrint("muteRemoteAudioStream: \(uid), ret: \(ret ?? -1)")
+ }
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: rtcConnection)
+
+ }
+
+ private func leaveChorus2ndChannel(_ role: KTVSingRole) {
+ guard let config = songConfig else {return}
+ guard let subConn = subChorusConnection else {return}
+ if (role == .leadSinger) {
+ apiConfig?.engine?.leaveChannelEx(subConn)
+ } else if (role == .coSinger) {
+ apiConfig?.engine?.leaveChannelEx(subConn)
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(config.mainSingerUid), mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ }
+
+ /**
+ * 离开合唱
+ */
+
+ private func leaveChorus(role: KTVSingRole) {
+ agoraPrint("leaveChorus role: \(singerRole.rawValue)")
+ if role == .leadSinger {
+ mainSingerHasJoinChannelEx = false
+ leaveChorus2ndChannel(role)
+ } else if role == .coSinger {
+ mediaPlayer?.stop()
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ // mediaOption.autoSubscribeAudio = true
+ // mediaOption.autoSubscribeVideo = false
+ mediaOption.publishMediaPlayerAudioTrack = false
+ apiConfig?.engine?.updateChannel(with: mediaOption)
+ leaveChorus2ndChannel(role)
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ apiConfig?.engine?.setAudioScenario(.gameStreaming)
+ } else if role == .audience {
+ agoraPrint("joinChorus: KTVSingRoleAudience does not need to leaveChorus!")
+ }
+ }
+
+}
+
+extension KTVGiantChorusApiImpl {
+
+ private func getEventHander(callBack:((KTVApiEventHandlerDelegate)-> Void)) {
+ for obj in eventHandlers.allObjects {
+ if obj is KTVApiEventHandlerDelegate {
+ callBack(obj as! KTVApiEventHandlerDelegate)
+ }
+ }
+ }
+
+ private func _loadMusic(config: KTVSongConfiguration, mode: KTVLoadMusicMode, onMusicLoadStateListener: IMusicLoadStateListener){
+
+ songConfig = config
+ lastReceivedPosition = 0
+ localPosition = 0
+
+ if (config.mode == .loadNone) {
+ return
+ }
+
+ if mode == .loadLrcOnly {
+ loadLyric(with: songCode) { [weak self] url in
+ guard let self = self else { return }
+ agoraPrint("loadLrcOnly: songCode:\(self.songCode) ulr:\(String(describing: url))")
+// if self.songCode != songCode {
+// onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .cancled)
+// return
+// }
+ if let urlPath = url, !urlPath.isEmpty {
+ self.lyricUrlMap[String(self.songCode)] = urlPath
+ self.setLyric(with: urlPath) { lyricUrl in
+ onMusicLoadStateListener.onMusicLoadSuccess(songCode: self.songCode, lyricUrl: urlPath)
+ }
+ } else {
+ onMusicLoadStateListener.onMusicLoadFail(songCode: self.songCode, reason: .noLyricUrl)
+ }
+
+// if (config.autoPlay) {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+//
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
+ }
+ } else {
+ loadMusicListeners.setObject(onMusicLoadStateListener, forKey: "\(self.songCode)" as NSString)
+ onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, state: .preloading, msg: "", lyricUrl: "")
+ // TODO: 只有未缓存时才显示进度条
+ if mcc?.isPreloaded(songCode: songCode) != 0 {
+ onMusicLoadStateListener.onMusicLoadProgress(songCode: self.songCode, percent: 0, state: .preloading, msg: "", lyricUrl: "")
+ }
+
+ preloadMusic(with: songCode) { [weak self] status, songCode in
+ guard let self = self else { return }
+ if self.songCode != songCode {
+ onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .cancled)
+ return
+ }
+ if status == .OK {
+ if mode == .loadMusicAndLrc {
+ // 需要加载歌词
+ self.loadLyric(with: songCode) { url in
+ self.agoraPrint("loadMusicAndLrc: songCode:\(songCode) status:\(status.rawValue) ulr:\(String(describing: url))")
+ if self.songCode != songCode {
+ onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .cancled)
+ return
+ }
+ if let urlPath = url, !urlPath.isEmpty {
+ self.lyricUrlMap[String(songCode)] = urlPath
+ self.setLyric(with: urlPath) { lyricUrl in
+ onMusicLoadStateListener.onMusicLoadSuccess(songCode: songCode, lyricUrl: urlPath)
+ }
+ } else {
+ onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .noLyricUrl)
+ }
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+//
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
+ }
+ } else if mode == .loadMusicOnly {
+ agoraPrint("loadMusicOnly: songCode:\(songCode) load success")
+// if config.autoPlay {
+// // 主唱自动播放歌曲
+// if self.singerRole != .leadSinger {
+// self.switchSingerRole(newRole: .soloSinger) { _, _ in
+//
+// }
+// }
+// self.startSing(songCode: self.songCode, startPos: 0)
+// }
+ onMusicLoadStateListener.onMusicLoadSuccess(songCode: songCode, lyricUrl: "")
+ }
+ } else {
+ agoraPrint("load music failed songCode:\(songCode)")
+ onMusicLoadStateListener.onMusicLoadFail(songCode: songCode, reason: .musicPreloadFail)
+ }
+ }
+ }
+ }
+
+ private func loadLyric(with songCode: NSInteger, callBack:@escaping LyricCallback) {
+ agoraPrint("loadLyric songCode: \(songCode)")
+ let requestId: String = self.mcc?.getLyric(songCode: songCode, lyricType: 0) ?? ""
+ self.lyricCallbacks.updateValue(callBack, forKey: requestId)
+ }
+
+ private func preloadMusic(with songCode: Int, callback: @escaping LoadMusicCallback) {
+ agoraPrint("preloadMusic songCode: \(songCode)")
+ if self.mcc?.isPreloaded(songCode: songCode) == 0 {
+ musicCallbacks.removeValue(forKey: String(songCode))
+ callback(.OK, songCode)
+ return
+ }
+ let err = self.mcc?.preload(songCode: songCode)
+ if err == nil {
+ musicCallbacks.removeValue(forKey: String(songCode))
+ callback(.error, songCode)
+ return
+ }
+ musicCallbacks.updateValue(callback, forKey: String(songCode))
+ }
+
+ private func setLyric(with url: String, callBack: @escaping LyricCallback) {
+ agoraPrint("setLyric url: (url)")
+ self.lrcControl?.onDownloadLrcData(url: url)
+ callBack(url)
+ }
+
+ func startSing(songCode: Int, startPos: Int) {
+ let dict: [String: Any] = [
+ "songCode": songCode,
+ "startPos": startPos
+ ]
+ sendCustomMessage(with: "startSing", dict: dict)
+ let role = singerRole
+ agoraPrint("startSing role: \(role.rawValue)")
+ if self.songCode != songCode {
+ agoraPrint("startSing failed: canceled")
+ return
+ }
+ mediaPlayer?.setPlayerOption("enable_multi_audio_track", value: 1)
+ apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
+ let ret = (mediaPlayer as? AgoraMusicPlayerProtocol)?.openMedia(songCode: songCode, startPos: startPos)
+ mediaPlayer?.setLoopCount(-1)
+ agoraPrint("startSing->openMedia(\(songCode) fail: \(ret ?? -1)")
+ }
+
+ func startSing(url: String, startPos: Int) {
+ let dict: [String: Any] = [
+ "url": url,
+ "startPos": startPos
+ ]
+ sendCustomMessage(with: "startSing", dict: dict)
+ let role = singerRole
+ agoraPrint("startSing role: \(role.rawValue)")
+ if self.songUrl != songUrl {
+ agoraPrint("startSing failed: canceled")
+ return
+ }
+ apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
+ let ret = mediaPlayer?.open(url, startPos: startPos)
+ agoraPrint("startSing->openMedia(\(url) fail: \(ret ?? -1)")
+ }
+
+ /**
+ * 停止播放歌曲
+ */
+ @objc public func stopSing() {
+ agoraPrint("stopSing")
+ sendCustomMessage(with: "stopSing", dict: [:])
+ let mediaOption = AgoraRtcChannelMediaOptions()
+ // mediaOption.autoSubscribeAudio = true
+ // mediaOption.autoSubscribeVideo = true
+ mediaOption.publishMediaPlayerAudioTrack = false
+ apiConfig?.engine?.updateChannelEx(with: mediaOption, connection: singChannelConnection ?? AgoraRtcConnection())
+
+ if mediaPlayer?.getPlayerState() != .stopped {
+ mediaPlayer?.stop()
+ }
+
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ apiConfig?.engine?.setAudioScenario(.gameStreaming)
+ }
+
+ @objc public func setSingingScore(score: Int) {
+ self.singingScore = score
+ }
+
+ @objc func setAudienceStreamMessage(dict: [String: Any]) {
+ sendStreamMessageWithDict(dict) { _ in
+
+ }
+ }
+
+ @objc public func setAudioPlayoutDelay(audioPlayoutDelay: Int) {
+ self.audioPlayoutDelay = audioPlayoutDelay
+ }
+
+ @objc func enableProfessionalStreamerMode(_ enable: Bool) {
+ if self.isPublishAudio == false {return}
+ self.enableProfessional = enable
+ //专业非专业还需要根据是否佩戴耳机来判断是否开启3A
+ apiConfig?.engine?.setAudioProfile(enable ? .musicHighQualityStereo : .musicStandardStereo)
+ apiConfig?.engine?.setParameters("{\"che.audio.aec.enable\":\((enable && isWearingHeadPhones) ? false : true)}")
+ apiConfig?.engine?.setParameters("{\"che.audio.agc.enable\":\((enable && isWearingHeadPhones) ? false : true)}")
+ apiConfig?.engine?.setParameters("{\"che.audio.ans.enable\":\((enable && isWearingHeadPhones) ? false : true)}")
+ apiConfig?.engine?.setParameters("{\"che.audio.md.enable\": false}")
+ }
+
+ func joinChorus(newRole: KTVSingRole) {
+ agoraPrint("joinChorus: \(newRole)")
+ let singChannelMediaOptions = AgoraRtcChannelMediaOptions()
+ singChannelMediaOptions.autoSubscribeAudio = true
+ singChannelMediaOptions.publishMicrophoneTrack = true
+ singChannelMediaOptions.clientRoleType = .broadcaster
+// singChannelMediaOptions.parameters = "{\"che.audio.max_mixed_participants\": 8}"
+ if newRole == .leadSinger {
+ // 主唱不参加TopN
+ singChannelMediaOptions.isAudioFilterable = false
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\":\(apiConfig?.routeSelectionConfig.streamNum ?? 0)}")
+ } else {
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\":\((apiConfig?.routeSelectionConfig.streamNum ?? 0) - 1)}")
+ }
+
+ guard let token = apiConfig?.chorusChannelToken, let singConnection = singChannelConnection else {return}
+
+
+ // 加入演唱频道
+ let ret = apiConfig?.engine?.joinChannelEx(byToken: token, connection: singConnection, delegate: self, mediaOptions: singChannelMediaOptions)
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: singConnection)
+ if apiConfig?.routeSelectionConfig.type == .topN || apiConfig?.routeSelectionConfig.type == .byDelayAndTopN {
+ if newRole == .leadSinger {
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\":\(apiConfig?.routeSelectionConfig.streamNum)}")
+ } else {
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\":\((apiConfig?.routeSelectionConfig.streamNum ?? 0) - 1)}")
+ }
+ } else {
+ apiConfig?.engine?.setParameters("{\"che.audio.filter_streams\": 0}")
+ }
+
+ let res = apiConfig?.engine?.enableAudioVolumeIndicationEx(50, smooth: 10, reportVad: true, connection: singConnection)
+ switch newRole {
+ case .leadSinger:
+ // 更新音频配置
+ apiConfig?.engine?.setAudioScenario(.chorus)
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 80000}")
+
+ // mpk流加入频道
+ let options = AgoraRtcChannelMediaOptions()
+ options.autoSubscribeAudio = false
+ options.autoSubscribeVideo = false
+ options.publishMicrophoneTrack = false
+ options.publishMediaPlayerAudioTrack = true
+ options.publishMediaPlayerId = Int(mediaPlayer?.getMediaPlayerId() ?? 0)
+ options.clientRoleType = .broadcaster
+ // 防止主唱和合唱听见mpk流的声音
+ options.enableAudioRecordingOrPlayout = false
+
+ let rtcConnection = AgoraRtcConnection()
+ rtcConnection.channelId = apiConfig?.chorusChannelName ?? ""
+ rtcConnection.localUid = UInt(apiConfig?.musicStreamUid ?? 0)
+ mpkConnection = rtcConnection
+
+ // 加入演唱频道
+ let delegate = NSObject()
+ let ret = apiConfig?.engine?.joinChannelEx(byToken: apiConfig?.musicChannelToken, connection: mpkConnection ?? AgoraRtcConnection(), delegate: nil, mediaOptions: options)
+ apiConfig?.engine?.setParametersEx("{\"rtc.use_audio4\": true}", connection: mpkConnection ?? AgoraRtcConnection())
+
+
+ case .coSinger:
+ // 防止主唱和合唱听见mpk流的声音
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(apiConfig?.musicStreamUid ?? 0), mute: true, connection: singChannelConnection ?? AgoraRtcConnection())
+
+ // 更新音频配置
+ apiConfig?.engine?.setAudioScenario(.chorus)
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":false}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+
+ // 预加载歌曲成功
+ // 导唱
+ mediaPlayer?.setPlayerOption("enable_multi_audio_track", value: 1)
+ if apiConfig?.musicType == .mcc {
+ (mediaPlayer as? AgoraMusicPlayerProtocol)?.openMedia(songCode: self.songCode , startPos: 0) // TODO open failed
+ } else {
+ mediaPlayer?.open(songUrl, startPos: 0) // TODO open failed
+ }
+ default:
+ agoraPrintError("JoinChorus with Wrong role: \(singerRole)")
+ }
+
+
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(apiConfig?.musicStreamUid ?? 0), mute: true, connection: singChannelConnection ?? AgoraRtcConnection())
+ // 加入演唱频道后,创建data stream
+ renewInnerDataStreamId()
+ }
+
+ func leaveChorus2(role: KTVSingRole) {
+ agoraPrint("leaveChorus: \(role)")
+ switch role {
+ case .leadSinger:
+ apiConfig?.engine?.leaveChannelEx(mpkConnection ?? AgoraRtcConnection())
+ case .coSinger:
+ mediaPlayer?.stop()
+
+ // 更新音频配置
+ apiConfig?.engine?.setAudioScenario(.gameStreaming)
+ apiConfig?.engine?.setParameters("{\"rtc.video.enable_sync_render_ntp_broadcast\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.neteq.enable_stable_playout\":true}")
+ apiConfig?.engine?.setParameters("{\"che.audio.custom_bitrate\": 48000}")
+ default:
+ agoraPrint("JoinChorus with wrong role: \(singerRole)")
+ }
+ apiConfig?.engine?.leaveChannelEx(singChannelConnection ?? AgoraRtcConnection())
+ }
+
+}
+
+// rtc的代理回调
+extension KTVGiantChorusApiImpl: AgoraRtcEngineDelegate {
+
+ public func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) {
+ agoraPrint("didJoinChannel channel:\(channel) uid: \(uid)")
+ if joinChorusNewRole == .leadSinger {
+ mainSingerHasJoinChannelEx = true
+ onJoinExChannelCallBack?(true, nil)
+ }
+ if joinChorusNewRole == .coSinger {
+ self.onJoinExChannelCallBack?(true, nil)
+ }
+ if let subChorusConnection = subChorusConnection {
+ apiConfig?.engine?.enableAudioVolumeIndicationEx(50, smooth: 10, reportVad: true, connection: subChorusConnection)
+ }
+ }
+
+ public func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
+ if errorCode != .joinChannelRejected {return}
+ agoraPrintError("join ex channel failed")
+ engine.setAudioScenario(.gameStreaming)
+ if joinChorusNewRole == .leadSinger {
+ mainSingerHasJoinChannelEx = false
+ onJoinExChannelCallBack?(false, .joinChannelFail)
+ }
+
+ if joinChorusNewRole == .coSinger {
+ self.onJoinExChannelCallBack?(false, .joinChannelFail)
+ }
+ }
+
+ //合唱频道的声音回调
+ public func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) {
+ getEventHander { delegate in
+ delegate.onChorusChannelAudioVolumeIndication(speakers: speakers, totalVolume: totalVolume)
+ }
+ didKTVAPIReceiveAudioVolumeIndication(with: speakers, totalVolume: totalVolume)
+ }
+
+ public func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) {
+ didKTVAPIReceiveStreamMessageFrom(uid: NSInteger(uid), streamId: streamId, data: data)
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, audioMetadataReceived uid: UInt, metadata: Data) {
+ guard let time: LrcTime = try? LrcTime(serializedData: metadata) else {return}
+ if time.type == .lrcTime && self.singerRole == .audience {
+ self.setProgress(with: Int(time.ts))
+ }
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
+ guard let musicId = apiConfig?.musicStreamUid,let mainSingerId = songConfig?.mainSingerUid else {return}
+ if uid != musicId && subScribeSingerMap.count < 8 {
+ apiConfig?.engine?.muteRemoteAudioStreamEx(uid, mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ if uid != mainSingerId {
+ subScribeSingerMap[Int(uid)] = 0
+ }
+ } else if uid != musicId && subScribeSingerMap.count == 8 {
+ apiConfig?.engine?.muteRemoteAudioStreamEx(uid, mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ if uid != musicId && uid != mainSingerId {
+ singerList.append(Int(uid))
+ }
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) {
+ subScribeSingerMap.removeAll()
+ singerList.removeAll()
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
+ subScribeSingerMap.removeValue(forKey: Int(uid))
+ if let index = singerList.firstIndex(of: Int(uid)) {
+ singerList.remove(at: index)
+ }
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) {
+ guard let musicId = apiConfig?.musicStreamUid,let mainSingerId = songConfig?.mainSingerUid else {return}
+ if apiConfig?.routeSelectionConfig.type == .random || apiConfig?.routeSelectionConfig.type == .topN { return }
+ let uid = stats.uid
+ if uid == mainSingerId {
+ mainSingerDelay = stats.e2eDelay
+ }
+ if uid != mainSingerId && uid != musicId && subScribeSingerMap[Int(uid)] != nil {
+ subScribeSingerMap[Int(uid)] = stats.e2eDelay
+ }
+ }
+}
+
+//需要外部转发的方法 主要是dataStream相关的
+extension KTVGiantChorusApiImpl {
+
+ @objc func didAudioPublishStateChange(newState: AgoraStreamPublishState) {
+ self.isPublishAudio = newState == .published
+ enableProfessionalStreamerMode(self.enableProfessional)
+ agoraPrint("PublishStateChange:\(newState)")
+ }
+
+ @objc func didAudioRouteChanged( routing: AgoraAudioOutputRouting) {
+ agoraPrint("Route changed:\(routing)")
+ self.audioRouting = routing.rawValue
+ let headPhones: [AgoraAudioOutputRouting] = [.headset, .bluetoothDeviceHfp, .bluetoothDeviceA2dp, .headsetNoMic]
+ let wearHeadPhone: Bool = headPhones.contains(routing)
+ if wearHeadPhone == self.isWearingHeadPhones {
+ return
+ }
+ self.isWearingHeadPhones = wearHeadPhone
+ enableProfessionalStreamerMode(self.enableProfessional)
+ }
+
+ @objc public func didKTVAPIReceiveStreamMessageFrom(uid: NSInteger, streamId: NSInteger, data: Data) {
+ let role = singerRole
+ if isRelease {return}
+ guard let dict = dataToDictionary(data: data), let cmd = dict["cmd"] as? String else { return }
+ agoraPrint("recv dict:\(dict)")
+ switch cmd {
+ case "setLrcTime":
+ handleSetLrcTimeCommand(dict: dict, role: role)
+ case "PlayerState":
+ handlePlayerStateCommand(dict: dict, role: role)
+ case "setVoicePitch":
+ handleSetVoicePitchCommand(dict: dict, role: role)
+ default:
+ break
+ }
+ }
+
+ private func handleSetLrcTimeCommand(dict: [String: Any], role: KTVSingRole) {
+ guard let position = dict["time"] as? Int64,
+ let duration = dict["duration"] as? Int64,
+ let realPosition = dict["realTime"] as? Int64,
+ // let songCode = dict["songCode"] as? Int64,
+ let mainSingerState = dict["playerState"] as? Int,
+ let ntpTime = dict["ntp"] as? Int,
+ let songId = dict["songIdentifier"] as? String
+ else { return }
+ #if DUBUG
+ print("realTime:\(realPosition) position:\(position) lastNtpTime:\(lastNtpTime) ntpTime:\(ntpTime) ntpGap:\(ntpTime - self.lastNtpTime) ")
+ #endif
+ //如果接收到的歌曲和自己本地的歌曲不一致就不更新进度
+// guard songCode == self.songCode else {
+// agoraPrint("local songCode[\(songCode)] is not equal to recv songCode[\(self.songCode)] role: \(singerRole.rawValue)")
+// return
+// }
+
+ self.lastNtpTime = ntpTime
+ self.remotePlayerDuration = TimeInterval(duration)
+
+ let state = AgoraMediaPlayerState(rawValue: mainSingerState) ?? .stopped
+// self.lastMainSingerUpdateTime = Date().milListamp
+// self.remotePlayerPosition = TimeInterval(realPosition)
+ if self.playerState != state {
+ #if DUBUG
+ print("[setLrcTime] recv state: \(self.playerState.rawValue)->\(state.rawValue) role: \(singerRole.rawValue) role: \(singerRole.rawValue)")
+ #endif
+ if state == .playing, singerRole == .coSinger, playerState == .openCompleted {
+ //如果是伴唱等待主唱开始播放,seek 到指定位置开始播放保证歌词显示位置准确
+ self.localPlayerPosition = self.lastMainSingerUpdateTime - Double(position)
+ print("localPlayerPosition:playerKit:handleSetLrcTimeCommand \(localPlayerPosition)")
+ agoraPrint("seek toPosition: \(position)")
+ mediaPlayer?.seek(toPosition: Int(position))
+ }
+
+ syncPlayStateFromRemote(state: state, needDisplay: false)
+ }
+
+ if role == .coSinger {
+ self.lastMainSingerUpdateTime = Date().milListamp
+ self.remotePlayerPosition = TimeInterval(realPosition)
+ handleCoSingerRole(dict: dict)
+ } else if role == .audience {
+ if dict.keys.contains("ver") {
+ recvFromDataStream = false
+ } else {
+ recvFromDataStream = true
+ if self.songIdentifier == songId {
+ self.lastMainSingerUpdateTime = Date().milListamp
+ self.remotePlayerPosition = TimeInterval(realPosition)
+ } else {
+ self.lastMainSingerUpdateTime = 0
+ self.remotePlayerPosition = 0
+ }
+ handleAudienceRole(dict: dict)
+ }
+ }
+ }
+
+ private func handlePlayerStateCommand(dict: [String: Any], role: KTVSingRole) {
+ let mainSingerState: Int = dict["state"] as? Int ?? 0
+ let state = AgoraMediaPlayerState(rawValue: mainSingerState) ?? .idle
+
+// if state == .playing, singerRole == .coSinger, playerState == .openCompleted {
+// //如果是伴唱等待主唱开始播放,seek 到指定位置开始播放保证歌词显示位置准确
+// self.localPlayerPosition = getPlayerCurrentTime()
+// print("localPlayerPosition:playerKit:handlePlayerStateCommand \(localPlayerPosition)")
+// agoraPrint("seek toPosition: \(self.localPlayerPosition)")
+// mediaPlayer?.seek(toPosition: Int(self.localPlayerPosition))
+// }
+
+ agoraPrint("recv state with MainSinger: \(state.rawValue)")
+ syncPlayStateFromRemote(state: state, needDisplay: true)
+ }
+
+ private func handleSetVoicePitchCommand(dict: [String: Any], role: KTVSingRole) {
+ if role == .audience, let voicePitch = dict["pitch"] as? Double {
+ self.pitch = voicePitch
+ }
+ }
+
+ private func handleCoSingerRole(dict: [String: Any]) {
+
+ if mediaPlayer?.getPlayerState() == .playing {
+ let localNtpTime = getNtpTimeInMs()
+ let localPosition = localNtpTime - Int(localPlayerSystemTime) + localPosition
+ let expectPosition = Int(dict["time"] as? Int64 ?? 0) + localNtpTime - Int(dict["ntp"] as? Int64 ?? 0) + self.audioPlayoutDelay
+ let threshold = expectPosition - Int(localPosition)
+ let ntpTime = dict["ntp"] as? Int ?? 0
+ let time = dict["time"] as? Int64 ?? 0
+ #if DUBUG
+ agoraPrint("checkNtp, diff:\(threshold), localNtp:\(getNtpTimeInMs()), localPosition:\(localPosition), audioPlayoutDelay:\(audioPlayoutDelay), remoteDiff:\(String(describing: ntpTime - Int(time)))")
+ #endif
+ if abs(threshold) > 50 {
+ agoraPrint("need seek, time:\(threshold)")
+ mediaPlayer?.seek(toPosition: expectPosition)
+ }
+ }
+
+ }
+
+ private func handleAudienceRole(dict: [String: Any]) {
+ // do something for audience role
+ guard let position = dict["time"] as? Int64,
+ let duration = dict["duration"] as? Int64,
+ let realPosition = dict["realTime"] as? Int64,
+ let songCode = dict["songCode"] as? Int64,
+ let mainSingerState = dict["playerState"] as? Int
+ else { return }
+ }
+
+ @objc public func didKTVAPIReceiveAudioVolumeIndication(with speakers: [AgoraRtcAudioVolumeInfo], totalVolume: NSInteger) {
+ if playerState != .playing {return}
+ if singerRole == .audience {return}
+
+ guard var pitch: Double = speakers.first?.voicePitch else {return}
+ pitch = isNowMicMuted ? 0 : pitch
+ //如果mpk不是playing状态 pitch = 0
+ if mediaPlayer?.getPlayerState() != .playing {pitch = 0}
+ self.pitch = pitch
+ //将主唱的pitch同步到观众
+// if isMainSinger() {
+// let dict: [String: Any] = [ "cmd": "setVoicePitch",
+// "pitch": pitch,
+// ]
+// sendStreamMessageWithDict(dict, success: nil)
+// }
+ }
+
+ @objc public func didKTVAPILocalAudioStats(stats: AgoraRtcLocalAudioStats) {
+ if useCustomAudioSource == true {return}
+ audioPlayoutDelay = Int(stats.audioPlayoutDelay)
+ }
+
+ @objc func didAudioMetadataReceived( uid: UInt, metadata: Data) {
+ guard let time: LrcTime = try? LrcTime(serializedData: metadata) else {return}
+ if time.type == .lrcTime && self.singerRole == .audience {
+ self.setProgress(with: Int(time.ts))
+ }
+ }
+
+}
+
+//private method
+extension KTVGiantChorusApiImpl {
+
+ private func initTimer() {
+
+ guard timer == nil else { return }
+
+ timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: {[weak self] timer in
+ guard let self = self else {
+ timer.invalidate()
+ return
+ }
+
+ var current = self.getPlayerCurrentTime()
+ if self.singerRole == .audience && (Date().milListamp - (self.lastMainSingerUpdateTime )) > 1000 {
+ return
+ }
+
+ if self.singerRole != .audience && (Date().milListamp - (self.lastReceivedPosition )) > 1000 {
+ return
+ }
+
+ if self.oldPitch == self.pitch && (self.oldPitch != 0 && self.pitch != 0) {
+ self.pitch = -1
+ }
+
+ if self.singerRole != .audience {
+ current = Date().milListamp - self.lastReceivedPosition + Double(self.localPosition)
+ }
+ if self.singerRole == .audience && !recvFromDataStream {
+
+ } else {
+ if self.singerRole != .audience {
+ current = Date().milListamp - self.lastReceivedPosition + Double(self.localPosition)
+ if self.singerRole == .leadSinger || self.singerRole == .soloSinger {
+ var time: LrcTime = LrcTime()
+ time.forward = true
+ time.ts = Int64(current) + Int64(self.startHighTime)
+ time.songID = songIdentifier
+ time.type = .lrcTime
+ //大合唱的uid是musicuid
+ time.uid = Int32(Int(apiConfig?.musicStreamUid ?? 0))
+ sendMetaMsg(with: time)
+ }
+ }
+ self.setProgress(with: Int(current) + Int(self.startHighTime))
+ }
+ self.oldPitch = self.pitch
+ })
+ }
+
+ private func setPlayerState(with state: AgoraMediaPlayerState) {
+ playerState = state
+ updateRemotePlayBackVolumeIfNeed()
+ updateTimer(with: state)
+ }
+
+ private func updateRemotePlayBackVolumeIfNeed() {
+ let role = singerRole
+ if role == .audience {
+ apiConfig?.engine?.adjustPlaybackSignalVolume(100)
+ return
+ }
+
+ let vol = self.playerState == .playing ? remoteVolume : 100
+ apiConfig?.engine?.adjustPlaybackSignalVolume(Int(vol))
+ }
+
+ private func updateTimer(with state: AgoraMediaPlayerState) {
+ DispatchQueue.main.async {
+ if state == .paused || state == .stopped {
+ self.pauseTimer()
+ } else if state == .playing {
+ self.startTimer()
+ }
+ }
+ }
+
+ //timer method
+ private func startTimer() {
+ guard let timer = self.timer else {return}
+ if isPause == false {
+ RunLoop.current.add(timer, forMode: .common)
+ self.timer?.fire()
+ } else {
+ resumeTimer()
+ }
+ }
+
+ private func resumeTimer() {
+ if isPause == false {return}
+ isPause = false
+ timer?.fireDate = Date()
+ }
+
+ private func pauseTimer() {
+ if isPause == true {return}
+ isPause = true
+ timer?.fireDate = Date.distantFuture
+ }
+
+ private func freeTimer() {
+ guard let _ = self.timer else {return}
+ self.timer?.invalidate()
+ self.timer = nil
+ }
+
+ private func getPlayerCurrentTime() -> TimeInterval {
+ let role = singerRole
+ if role == .soloSinger || role == .leadSinger{
+ let time = Date().milListamp - localPlayerPosition
+ return time
+ } else if role == .coSinger {
+ if playerState == .playing || playerState == .paused {
+ let time = Date().milListamp - localPlayerPosition
+ return time
+ }
+ }
+
+ var position = Date().milListamp - self.lastMainSingerUpdateTime + remotePlayerPosition
+ if playerState != .playing {
+ position = remotePlayerPosition
+ }
+ return position
+ }
+
+ private func syncPlayStateFromRemote(state: AgoraMediaPlayerState, needDisplay: Bool) {
+ let role = singerRole
+ if role == .coSinger {
+ if state == .stopped {
+ // stopSing()
+ } else if state == .paused {
+ pausePlay()
+ } else if state == .playing {
+ resumeSing()
+ } else if (state == .playBackAllLoopsCompleted && needDisplay == true) {
+ getEventHander { delegate in
+ delegate.onMusicPlayerStateChanged(state: state, reason: .none, isLocal: true)
+ }
+ }
+ } else {
+ self.playerState = state
+ getEventHander { delegate in
+ delegate.onMusicPlayerStateChanged(state: self.playerState, reason: .none, isLocal: false)
+ }
+ }
+ }
+
+ private func pausePlay() {
+ mediaPlayer?.pause()
+ }
+
+ private func dataToDictionary(data: Data) -> [String: Any]? {
+ do {
+ let json = try JSONSerialization.jsonObject(with: data, options: [])
+ return json as? [String: Any]
+ } catch {
+ print("Error decoding data: (error.localizedDescription)")
+ return nil
+ }
+ }
+
+ private func compactDictionaryToData(_ dict: [String: Any]) -> Data? {
+ do {
+ let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
+ return jsonData
+ } catch {
+ print("Error encoding data: (error.localizedDescription)")
+ return nil
+ }
+ }
+
+ private func getNtpTimeInMs() -> Int {
+ var localNtpTime: Int = Int(apiConfig?.engine?.getNtpWallTimeInMs() ?? 0)
+
+ if localNtpTime != 0 {
+ localNtpTime = localNtpTime + 2208988800 * 1000
+ }
+
+ return localNtpTime
+ }
+
+ private func syncPlayState(state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
+ let dict: [String: Any] = ["cmd": "PlayerState", "userId": apiConfig?.localUid as Any, "state": state.rawValue, "reason": "\(reason.rawValue)"]
+ sendStreamMessageWithDict(dict, success: nil)
+ }
+
+// private func sendCustomMessage(with event: String, label: String) {
+// apiConfig?.engine?.sendCustomReportMessage(messageId, category: version, event: event, label: label, value: 0)
+// apiRepoter?.reportFuncEvent(name: event, value: <#T##[String : Any]#>, ext: <#T##[String : Any]#>)
+// }
+
+ private func sendCustomMessage(with event: String, dict: [String: Any]) {
+ apiRepoter?.reportFuncEvent(name: event, value: dict, ext: [:])
+ }
+
+ private func sendStreamMessageWithDict(_ dict: [String: Any], success: ((_ success: Bool) -> Void)?) {
+ let messageData = compactDictionaryToData(dict as [String: Any])
+ let code = apiConfig?.engine?.sendStreamMessageEx(dataStreamId, data: messageData ?? Data(), connection: singChannelConnection ?? AgoraRtcConnection())
+ if code == 0 && success != nil { success!(true) }
+ if code != 0 {
+ print("sendStreamMessage fail: \(String(describing: code))")
+ }
+ }
+
+ private func syncPlayState(_ state: AgoraMediaPlayerState) {
+ let dict: [String: Any] = [ "cmd": "PlayerState", "userId": apiConfig?.localUid as Any, "state": "\(state.rawValue)" ]
+ sendStreamMessageWithDict(dict, success: nil)
+ }
+
+ private func setProgress(with pos: Int) {
+ lrcControl?.onUpdatePitch(pitch: Float(self.pitch))
+ lrcControl?.onUpdateProgress(progress: pos > 200 ? pos - 200 : pos)
+ }
+
+ private func sendMetaMsg(with time: LrcTime) {
+ let data: Data? = try? time.serializedData()
+ let code = apiConfig?.engine?.sendAudioMetadataEx(mpkConnection ?? AgoraRtcConnection(), metadata: data ?? Data())
+ if code != 0 {
+ // agoraPrint("sendStreamMessage fail: \(String(describing: code))")
+ }
+ }
+}
+
+//主要是MPK的回调
+extension KTVGiantChorusApiImpl: AgoraRtcMediaPlayerDelegate {
+
+ func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol, didChangedTo position_ms: Int, atTimestamp timestamp_ms: TimeInterval) {
+ self.lastReceivedPosition = Date().milListamp
+ self.localPosition = Int(position_ms)
+ self.localPlayerSystemTime = timestamp_ms
+ self.localPlayerPosition = Date().milListamp - Double(position_ms)
+ if isMainSinger() && getPlayerCurrentTime() > TimeInterval(self.audioPlayoutDelay) {
+ let dict: [String: Any] = [ "cmd": "setLrcTime",
+ "duration": self.playerDuration,
+ "time": position_ms - audioPlayoutDelay,
+ "realTime":position_ms,
+ "ntp": timestamp_ms,
+ "playerState": self.playerState.rawValue,
+ "songIdentifier": songIdentifier,
+ "forward": true,
+ "ver":2,
+ ]
+ #if DEBUG
+ print("position_ms:\(position_ms), ntp:\(getNtpTimeInMs()), delta:\(self.getNtpTimeInMs() - position_ms), autoPlayoutDelay:\(self.audioPlayoutDelay), state:\(self.playerState.rawValue)")
+ #endif
+ sendStreamMessageWithDict(dict, success: nil)
+ }
+ }
+
+ func AgoraRtcMediaPlayer(_ playerKit: any AgoraRtcMediaPlayerProtocol, didChangedTo state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
+ agoraPrint("agoraRtcMediaPlayer didChangedToState: \(state.rawValue) \(self.songCode)")
+ if isRelease {return}
+ self.playerState = state
+ if state == .openCompleted {
+ self.localPlayerPosition = Date().milListamp
+ self.playerDuration = TimeInterval(mediaPlayer?.getDuration() ?? 0)
+ playerKit.selectMultiAudioTrack(1, publishTrackIndex: 1)
+ if isMainSinger() { //主唱播放,通过同步消息“setLrcTime”通知伴唱play
+ playerKit.play()
+ }
+ self.startProcessDelay()
+ } else if state == .stopped {
+ apiConfig?.engine?.adjustPlaybackSignalVolume(100)
+ self.localPlayerPosition = Date().milListamp
+ self.playerDuration = 0
+ }
+ else if state == .paused {
+ apiConfig?.engine?.adjustPlaybackSignalVolume(100)
+ } else if state == .playing {
+ apiConfig?.engine?.adjustPlaybackSignalVolume(Int(remoteVolume))
+ self.localPlayerPosition = Date().milListamp - Double(mediaPlayer?.getPosition() ?? 0)
+ } else if state == .stopped {
+ self.stopProcessDelay()
+ }
+
+ if isMainSinger() {
+ syncPlayState(state: state, reason: reason)
+ }
+ agoraPrint("recv state with player callback : \(state.rawValue)")
+ if state == .playBackAllLoopsCompleted && singerRole == .coSinger {//可能存在伴唱不返回allloopbackComplete状态 这个状态通过主唱的playerState来同步
+ return
+ }
+ getEventHander { delegate in
+ delegate.onMusicPlayerStateChanged(state: state, reason: .none, isLocal: true)
+ }
+ }
+
+ private func isMainSinger() -> Bool {
+ return singerRole == .soloSinger || singerRole == .leadSinger
+ }
+}
+
+//主要是MCC的回调
+extension KTVGiantChorusApiImpl: AgoraMusicContentCenterEventDelegate {
+
+ func onSongSimpleInfoResult(_ requestId: String, songCode: Int, simpleInfo: String?, reason: AgoraMusicContentCenterStateReason) {
+ if let jsonData = simpleInfo?.data(using: .utf8) {
+ do {
+ let jsonMsg = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any]
+ let format = jsonMsg["format"] as! [String: Any]
+ let highPart = format["highPart"] as! [[String: Any]]
+ let highStartTime = highPart[0]["highStartTime"] as! Int
+ let highEndTime = highPart[0]["highEndTime"] as! Int
+ let time = highStartTime
+ startHighTime = time
+ self.lrcControl?.onHighPartTime(highStartTime: highStartTime, highEndTime: highEndTime)
+ } catch {
+ agoraPrintError("Error while parsing JSON: \(error.localizedDescription)")
+ }
+ }
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ }
+
+ func onMusicChartsResult(_ requestId: String, result: [AgoraMusicChartInfo], reason: AgoraMusicContentCenterStateReason) {
+ guard let callback = musicChartDict[requestId] else {return}
+ callback(requestId, reason, result)
+ musicChartDict.removeValue(forKey: requestId)
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ }
+
+ func onMusicCollectionResult(_ requestId: String, result: AgoraMusicCollection, reason: AgoraMusicContentCenterStateReason) {
+ guard let callback = musicSearchDict[requestId] else {return}
+ callback(requestId, reason, result)
+ musicSearchDict.removeValue(forKey: requestId)
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ }
+
+ func onLyricResult(_ requestId: String, songCode: Int, lyricUrl: String?, reason: AgoraMusicContentCenterStateReason) {
+ guard let lrcUrl = lyricUrl else {return}
+ let callback = self.lyricCallbacks[requestId]
+ guard let lyricCallback = callback else { return }
+ self.lyricCallbacks.removeValue(forKey: requestId)
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ if lrcUrl.isEmpty {
+ lyricCallback(nil)
+ return
+ }
+ lyricCallback(lrcUrl)
+ }
+
+ func onPreLoadEvent(_ requestId: String, songCode: Int, percent: Int, lyricUrl: String?, state: AgoraMusicContentCenterPreloadState, reason: AgoraMusicContentCenterStateReason) {
+ if let listener = self.loadMusicListeners.object(forKey: "\(songCode)" as NSString) as? IMusicLoadStateListener {
+ listener.onMusicLoadProgress(songCode: songCode, percent: percent, state: state, msg: String(reason.rawValue), lyricUrl: lyricUrl)
+ }
+ if (state == .preloading) { return }
+ let SongCode = "\(songCode)"
+ guard let block = self.musicCallbacks[SongCode] else { return }
+ self.musicCallbacks.removeValue(forKey: SongCode)
+ if (reason == .errorGateway) {
+ getEventHander { delegate in
+ delegate.onTokenPrivilegeWillExpire()
+ }
+ }
+ block(state, songCode)
+ }
+
+}
+
+extension KTVGiantChorusApiImpl {
+
+ private func sendSyncPitch(_ pitch: Double) {
+ var msg: [String:Any] = [:]
+ msg["cmd"] = "setVoicePitch"
+ msg["pitch"] = pitch
+ sendStreamMessageWithDict(msg) { _ in
+
+ }
+ }
+
+ private func startSyncPitch() {
+ print("startSyncPitch")
+ mStopSyncPitch = false
+ let queue = DispatchQueue(label: "com.example.syncpitch")
+ mSyncPitchTimer = DispatchSource.makeTimerSource(queue: queue)
+ mSyncPitchTimer?.schedule(deadline: .now(), repeating: .milliseconds(50))
+ mSyncPitchTimer?.setEventHandler { [weak self] in
+ guard let self = self else { return }
+ if !self.mStopSyncPitch &&
+ playerState == .playing &&
+ (singerRole == .leadSinger || singerRole == .soloSinger) {
+ self.sendSyncPitch(pitch)
+ }
+ }
+ mSyncPitchTimer?.resume()
+ }
+
+ private func stopSyncPitch() {
+ print("stopSyncPitch")
+ mStopSyncPitch = true
+ pitch = 0.0
+
+ mSyncPitchTimer?.cancel()
+ mSyncPitchTimer = nil
+ }
+
+ private func sendSyncScore() {
+ print("sendSyncScore")
+ var dictionary: [String: Any] = [:]
+ dictionary["service"] = "audio_smart_mixer"
+ dictionary["version"] = "V1"
+ var payload: [String: Any] = [:]
+ payload["cname"] = apiConfig?.chorusChannelName
+ payload["uid"] = String(apiConfig?.localUid ?? 0)
+ payload["uLv"] = -1
+ payload["specialLabel"] = 0
+ payload["audioRoute"] = audioRouting
+ payload["vocalScore"] = singingScore
+ dictionary["payload"] = payload
+ sendStreamMessageWithDict(dictionary) { _ in
+
+ }
+ }
+
+ private func startSyncScore() {
+ print("startSyncScore")
+ mStopSyncScore = false
+ let queue = DispatchQueue(label: "com.example.syncscore")
+ mSyncScoreTimer = DispatchSource.makeTimerSource(queue: queue)
+ mSyncScoreTimer?.schedule(deadline: .now(), repeating: .milliseconds(3000))
+ mSyncScoreTimer?.setEventHandler { [weak self] in
+ guard let self = self else { return }
+ if !self.mStopSyncScore &&
+ playerState == .playing &&
+ (singerRole == .leadSinger || singerRole == .coSinger) {
+ self.sendSyncScore()
+ }
+ }
+ mSyncScoreTimer?.resume()
+ }
+
+ private func stopSyncScore() {
+ print("stopSyncScore")
+ mStopSyncScore = true
+ singingScore = 0
+
+ mSyncScoreTimer?.cancel()
+ mSyncScoreTimer = nil
+ }
+
+ // -1: unknown,0:非K歌状态,1:K歌播放状态,2:K歌暂停状态)
+ private func getCloudConvergenceStatus() -> Int {
+ var status = -1
+ switch playerState {
+ case .playing:
+ status = 1
+ case .paused:
+ status = 2
+ default:
+ break
+ }
+ return status
+ }
+
+ private func sendSyncCloudConvergenceStatus() {
+ print("sendSyncCloudConvergenceStatus")
+ var dictionary: [String: Any] = [:]
+ dictionary["service"] = "audio_smart_mixer_status"
+ dictionary["version"] = "V1"
+ var payload: [String: Any] = [:]
+ payload["Ts"] = getNtpTimeInMs()
+ payload["cname"] = apiConfig?.chorusChannelName
+ payload["status"] = getCloudConvergenceStatus()
+ payload["bgmUID"] = mpkConnection?.localUid
+ payload["leadsingerUID"] = String(songConfig?.mainSingerUid ?? 0)
+ dictionary["payload"] = payload
+ sendStreamMessageWithDict(dictionary) { _ in
+
+ }
+ }
+
+ private func startSyncCloudConvergenceStatus() {
+ print("startSyncCloudConvergenceStatus")
+ mStopSyncCloudConvergenceStatus = false
+ let queue = DispatchQueue(label: "com.example.synccloudconvergencestatus")
+ mSyncCloudConvergenceStatusTimer = DispatchSource.makeTimerSource(queue: queue)
+ mSyncCloudConvergenceStatusTimer?.schedule(deadline: .now(), repeating: .milliseconds(200))
+ mSyncCloudConvergenceStatusTimer?.setEventHandler { [weak self] in
+ guard let self = self else { return }
+ if !self.mStopSyncCloudConvergenceStatus &&
+ singerRole == .leadSinger {
+ self.sendSyncCloudConvergenceStatus()
+ }
+ }
+ mSyncCloudConvergenceStatusTimer?.resume()
+ }
+
+ private func stopSyncCloudConvergenceStatus() {
+ print("stopSyncCloudConvergenceStatus")
+ mStopSyncCloudConvergenceStatus = true
+
+ mSyncCloudConvergenceStatusTimer?.cancel()
+ mSyncCloudConvergenceStatusTimer = nil
+ }
+
+}
+
+extension KTVGiantChorusApiImpl {
+
+ private func processDelayTask() {
+ if !mStopProcessDelay && singerRole != .audience {
+ let n = singerRole == .leadSinger ? apiConfig?.routeSelectionConfig.streamNum : (apiConfig?.routeSelectionConfig.streamNum ?? 1) - 1
+ let sortedEntries = subScribeSingerMap.sorted(by: { $0.value < $1.value })
+ let other = Array(sortedEntries.dropFirst(3))
+ var drop = [Int]()
+
+ if n ?? 3 > 3 {
+ for (uid, _) in other.dropLast(n! - 3) {
+ drop.append(uid)
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(uid), mute: true, connection: singChannelConnection ?? AgoraRtcConnection())
+ subScribeSingerMap.removeValue(forKey: uid)
+ }
+ }
+
+ agoraPrint("选路重新订阅, drop:\(drop)")
+
+ let filteredList = singerList.filter { !subScribeSingerMap.keys.contains($0) }
+ let filteredList2 = filteredList.filter { !drop.contains($0) }
+ let shuffledList = filteredList2.shuffled()
+
+ if subScribeSingerMap.count < 8 {
+ let randomSingers = Array(shuffledList.prefix(8 - subScribeSingerMap.count))
+ agoraPrintError("选路重新订阅, newSingers:\(randomSingers)")
+
+ for singer in randomSingers {
+ subScribeSingerMap[singer] = 0
+ apiConfig?.engine?.muteRemoteAudioStreamEx(UInt(singer), mute: false, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ }
+
+ agoraPrint("选路重新订阅, newSubScribeSingerMap:\(subScribeSingerMap)")
+ }
+ }
+
+ private func processSubscribeTask() {
+ if !mStopProcessDelay && singerRole != .audience {
+ let n = singerRole == .leadSinger ? apiConfig?.routeSelectionConfig.streamNum : (apiConfig?.routeSelectionConfig.streamNum ?? 0) - 1
+ let sortedEntries = subScribeSingerMap.sorted(by: { $0.value < $1.value })
+ let mustToHave = Array(sortedEntries.prefix(3))
+
+ for (uid, _) in mustToHave {
+ apiConfig?.engine?.adjustUserPlaybackSignalVolumeEx(UInt(uid), volume: 100, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+
+ let other = Array(sortedEntries.dropFirst(3))
+
+ if n ?? 3 > 3 {
+ for (uid, delay) in Array(other.prefix(n! - 3)) {
+ if delay > 300 {
+ apiConfig?.engine?.adjustUserPlaybackSignalVolumeEx(UInt(uid), volume: 0, connection: singChannelConnection ?? AgoraRtcConnection())
+ } else {
+ apiConfig?.engine?.adjustUserPlaybackSignalVolumeEx(UInt(uid), volume: 100, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ }
+
+ for (uid, _) in Array(other.dropFirst(n! - 3)) {
+ apiConfig?.engine?.adjustUserPlaybackSignalVolumeEx(UInt(uid), volume: 0, connection: singChannelConnection ?? AgoraRtcConnection())
+ }
+ }
+
+ agoraPrint("选路排序+调整播放音量, mustToHave:\(mustToHave), other:\(other)")
+ }
+ }
+
+ private func startProcessDelay() {
+ guard apiConfig?.routeSelectionConfig.type != .topN && apiConfig?.routeSelectionConfig.type != .random else { return }
+
+ mStopProcessDelay = false
+
+ // 创建并配置 processDelayTimer
+ processDelayFuture = DispatchSource.makeTimerSource()
+ processDelayFuture?.schedule(deadline: .now() + .seconds(10), repeating: .seconds(20))
+ processDelayFuture?.setEventHandler { [weak self] in
+ // 执行 mProcessDelayTask
+ self?.processDelayTask()
+ }
+ processDelayFuture?.resume()
+
+ // 创建并配置 processSubscribeTimer
+ processSubscribeFuture = DispatchSource.makeTimerSource()
+ processSubscribeFuture?.schedule(deadline: .now() + .seconds(15), repeating: .seconds(20))
+ processSubscribeFuture?.setEventHandler { [weak self] in
+ // 执行 mProcessSubscribeTask
+ self?.processSubscribeTask()
+ }
+ processSubscribeFuture?.resume()
+ }
+
+ private func stopProcessDelay() {
+ mStopProcessDelay = true
+
+ processDelayFuture?.cancel()
+ processDelayFuture = nil
+ processSubscribeFuture?.cancel()
+ processSubscribeFuture = nil
+ }
+}
+
+
+extension Date {
+ /// 获取当前 秒级 时间戳 - 10位
+ ///
+ var timeStamp : TimeInterval {
+ let timeInterval: TimeInterval = self.timeIntervalSince1970
+ return timeInterval
+ }
+ /// 获取当前 毫秒级 时间戳 - 13位
+ var milListamp : TimeInterval {
+ let timeInterval: TimeInterval = self.timeIntervalSince1970
+ let millisecond = CLongLong(round(timeInterval*1000))
+ return TimeInterval(millisecond)
+ }
+}
+
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/LrcTime.pb.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/LrcTime.pb.swift
new file mode 100644
index 0000000..b790fb2
--- /dev/null
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/LrcTime.pb.swift
@@ -0,0 +1,140 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: LrcTime.proto
+//
+// For information on using the generated types, please see the documentation:
+// https://github.com/apple/swift-protobuf/
+
+import Foundation
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+ struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+ typealias Version = _2
+}
+
+enum MsgType: SwiftProtobuf.Enum {
+ typealias RawValue = Int
+ case unknownType // = 0
+ case lrcTime // = 1001
+ case UNRECOGNIZED(Int)
+
+ init() {
+ self = .unknownType
+ }
+
+ init?(rawValue: Int) {
+ switch rawValue {
+ case 0: self = .unknownType
+ case 1001: self = .lrcTime
+ default: self = .UNRECOGNIZED(rawValue)
+ }
+ }
+
+ var rawValue: Int {
+ switch self {
+ case .unknownType: return 0
+ case .lrcTime: return 1001
+ case .UNRECOGNIZED(let i): return i
+ }
+ }
+
+ // The compiler won't synthesize support with the UNRECOGNIZED case.
+ static let allCases: [MsgType] = [
+ .unknownType,
+ .lrcTime,
+ ]
+
+}
+
+struct LrcTime: Sendable {
+ // SwiftProtobuf.Message conformance is added in an extension below. See the
+ // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+ // methods supported on all messages.
+
+ var type: MsgType = .unknownType
+
+ var forward: Bool = false
+
+ var ts: Int64 = 0
+
+ var songID: String = String()
+
+ var uid: Int32 = 0
+
+ var unknownFields = SwiftProtobuf.UnknownStorage()
+
+ init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension MsgType: SwiftProtobuf._ProtoNameProviding {
+ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+ 0: .same(proto: "UNKNOWN_TYPE"),
+ 1001: .same(proto: "LRC_TIME"),
+ ]
+}
+
+extension LrcTime: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+ static let protoMessageName: String = "LrcTime"
+ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+ 1: .same(proto: "type"),
+ 2: .same(proto: "forward"),
+ 3: .same(proto: "ts"),
+ 4: .same(proto: "songId"),
+ 5: .same(proto: "uid"),
+ ]
+
+ mutating func decodeMessage(decoder: inout D) throws {
+ while let fieldNumber = try decoder.nextFieldNumber() {
+ // The use of inline closures is to circumvent an issue where the compiler
+ // allocates stack space for every case branch when no optimizations are
+ // enabled. https://github.com/apple/swift-protobuf/issues/1034
+ switch fieldNumber {
+ case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }()
+ case 2: try { try decoder.decodeSingularBoolField(value: &self.forward) }()
+ case 3: try { try decoder.decodeSingularInt64Field(value: &self.ts) }()
+ case 4: try { try decoder.decodeSingularStringField(value: &self.songID) }()
+ case 5: try { try decoder.decodeSingularInt32Field(value: &self.uid) }()
+ default: break
+ }
+ }
+ }
+
+ func traverse(visitor: inout V) throws {
+ if self.type != .unknownType {
+ try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1)
+ }
+ if self.forward != false {
+ try visitor.visitSingularBoolField(value: self.forward, fieldNumber: 2)
+ }
+ if self.ts != 0 {
+ try visitor.visitSingularInt64Field(value: self.ts, fieldNumber: 3)
+ }
+ if !self.songID.isEmpty {
+ try visitor.visitSingularStringField(value: self.songID, fieldNumber: 4)
+ }
+ if self.uid != 0 {
+ try visitor.visitSingularInt32Field(value: self.uid, fieldNumber: 5)
+ }
+ try unknownFields.traverse(visitor: &visitor)
+ }
+
+ static func ==(lhs: LrcTime, rhs: LrcTime) -> Bool {
+ if lhs.type != rhs.type {return false}
+ if lhs.forward != rhs.forward {return false}
+ if lhs.ts != rhs.ts {return false}
+ if lhs.songID != rhs.songID {return false}
+ if lhs.uid != rhs.uid {return false}
+ if lhs.unknownFields != rhs.unknownFields {return false}
+ return true
+ }
+}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/LrcTime.proto b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/LrcTime.proto
new file mode 100644
index 0000000..084175b
--- /dev/null
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVAPI/LrcTime.proto
@@ -0,0 +1,14 @@
+syntax = "proto3";
+
+enum MsgType {
+ UNKNOWN_TYPE = 0;
+ LRC_TIME = 1001;
+}
+
+message LrcTime {
+ MsgType type = 1;
+ bool forward = 2;
+ int64 ts = 3;
+ string songId = 4;
+ int32 uid = 5;
+}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVLyricView.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVLyricView.swift
index 3bb1bdd..e62e43c 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVLyricView.swift
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVLyricView.swift
@@ -8,7 +8,7 @@
import UIKit
import AgoraLyricsScore
class KTVLyricView: UIView {
- var downloadManager = AgoraDownLoadManager()
+ var downloadManager = LyricsFileDownloader()
var lrcView: KaraokeView!
override init(frame: CGRect) {
super.init(frame: frame)
@@ -25,7 +25,7 @@ class KTVLyricView: UIView {
lrcView.scoringView.viewHeight = 60
lrcView.scoringView.topSpaces = 5
lrcView.backgroundColor = .lightGray
- lrcView.lyricsView.textNormalColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.5)
+ lrcView.lyricsView.inactiveLineTextColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.5)
// lrcView.lyricsView.textHighlightedColor = UIColor(hex: "#EEFF25")
lrcView.lyricsView.lyricLineSpacing = 6
lrcView.lyricsView.draggable = false
@@ -46,34 +46,7 @@ extension KTVLyricView: KTVLrcViewDelegate, KaraokeDelegate {
func onDownloadLrcData(url: String) {
//开始歌词下载
- startDownloadLrc(with: url) {[weak self] url in
- guard let self = self, let url = url else {return}
- self.resetLrcData(with: url)
- }
- }
-
- func startDownloadLrc(with url: String, callBack: @escaping LyricCallback) {
- var path: String? = nil
- downloadManager.downloadLrcFile(urlString: url) { lrcurl in
- defer {
- callBack(path)
- }
- guard let lrcurl = lrcurl else {
- print("downloadLrcFile fail, lrcurl is nil")
- return
- }
-
- let curSong = URL(string: url)?.lastPathComponent.components(separatedBy: ".").first
- let loadSong = URL(string: lrcurl)?.lastPathComponent.components(separatedBy: ".").first
- guard curSong == loadSong else {
- print("downloadLrcFile fail, missmatch, cur:\(curSong ?? "") load:\(loadSong ?? "")")
- return
- }
- path = lrcurl
- } failure: {
- callBack(nil)
- print("歌词解析失败")
- }
+ let _ = downloadManager.download(urlString: url)
}
func resetLrcData(with url: String) {
@@ -90,12 +63,15 @@ extension KTVLyricView: KTVLrcViewDelegate, KaraokeDelegate {
}
}
-extension KTVLyricView: AgoraLrcDownloadDelegate {
- public func downloadLrcFinished(url: String) {
- print("download lrc finished \(url)")
+extension KTVLyricView: LyricsFileDownloaderDelegate {
+ func onLyricsFileDownloadProgress(requestId: Int, progress: Float) {
+
}
- public func downloadLrcError(url: String, error: Error?) {
- print("download lrc fail \(url): \(String(describing: error))")
+ func onLyricsFileDownloadCompleted(requestId: Int, fileData: Data?, error: DownloadError?) {
+ guard let data = fileData, let model = KaraokeView.parseLyricData(data: data) else {
+ return
+ }
+ lrcView?.setLyricData(data: model)
}
}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVViewController.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVViewController.swift
index 9da8895..7807233 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVViewController.swift
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KTVViewController.swift
@@ -21,17 +21,18 @@ class KTVViewController: UIViewController {
var type: LoadMusicType = .mcc
var rtcKit: AgoraRtcEngineKit!
var rtcDataStreamId: Int = 0
- var ktvApi: KTVApiImpl!
+ var ktvApi: KTVApiDelegate!
var rtcToken: String?
var rtmToken: String?
var rtcPlayerToken: String?
var userId: Int = 0
+ var isCantata: Bool = false
let mainSingerId = 1000
let coSingerId = 2000
let audienceId = 3000
- let mccSongCode = 6654550232746660
+ let mccSongCode = 7162848697922600
var lyricView: KTVLyricView!
@@ -56,7 +57,7 @@ class KTVViewController: UIViewController {
self.view.backgroundColor = .white
self.title = "KTV online"
- if role == .leadSinger {
+ if role == .leadSinger || role == .leadSinger {
userId = mainSingerId
} else if role == .coSinger {
userId = coSingerId
@@ -73,8 +74,21 @@ class KTVViewController: UIViewController {
layoutUI()
joinRTCChannel()
- loadKTVApi()
-
+
+ if isCantata && role == .leadSinger{
+ getCloudMixerToken(with: "232425") {[weak self] inputToken, outputToken in
+ guard let self = self else {return}
+ ApiManager.shared.fetchStartCloud(mainChannel: self.channelName, cloudRtcUid: 232425, inputToken:inputToken, outputToken:outputToken) {[weak self] flag in
+ if flag == false {//云端合流失败
+ SVProgressHUD.show(withStatus: "云端合流失败")
+ } else {
+ self?.loadKTVApi()
+ }
+ }
+ }
+ } else {
+ loadKTVApi()
+ }
}
override func viewWillDisappear(_ animated: Bool) {
@@ -162,20 +176,46 @@ class KTVViewController: UIViewController {
}
private func loadKTVApi() {
- getMccData(with: "\(userId)") {[weak self] rtcToken, rtmToken, rtcPlayerToken in
- guard let self = self else {return}
- self.rtcToken = rtcToken
- self.rtmToken = rtmToken
- self.rtcPlayerToken = rtcPlayerToken
-
- let apiConfig = KTVApiConfig(appId: KeyCenter.AppId, rtmToken: self.type == .mcc ? (self.rtmToken ?? "") : "", engine: self.rtcKit, channelName: self.channelName, localUid: self.userId, chorusChannelName: "\(self.channelName)_ex", chorusChannelToken: self.rtcPlayerToken ?? "", type: .normal, maxCacheSize: 10, musicType: self.type == .mcc ? .mcc : .local, isDebugMode: false)
- self.ktvApi = KTVApiImpl(config: apiConfig)
- self.ktvApi.renewInnerDataStreamId()
- self.ktvApi.setLrcView(view: self.lyricView)
- self.ktvApi.addEventHandler(ktvApiEventHandler: self)
-
- self.rtcKit.joinChannel(byToken: KeyCenter.Token, channelId: self.channelName, uid: UInt(self.userId), mediaOptions: self.mediaOptions())
- self.loadMusic()
+ if isCantata {
+ getCantataMccData(with: "\(userId)") {[weak self] rtcToken, rtmToken, audienceToken, rtcPlayerToken in
+ guard let self = self else {return}
+ self.rtcToken = rtcToken
+ self.rtmToken = rtmToken
+ self.rtcPlayerToken = rtcPlayerToken
+
+ let giantConfig = GiantChorusConfiguration(appId: KeyCenter.AppId, rtmToken: rtmToken ?? "", engine: rtcKit, localUid: self.userId, audienceChannelName: "\(channelName)_ad", audienceChannelToken: audienceToken ?? "", chorusChannelName: "\(channelName)", chorusChannelToken: rtcToken ?? "", musicStreamUid: 2023, musicChannelToken: rtcPlayerToken ?? "", maxCacheSize: 10, musicType: self.type == .mcc ? .mcc : .local, routeSelectionConfig: GiantChorusRouteSelectionConfig(type: .byDelay, streamNum: 6), mccDomain: nil)
+
+ self.ktvApi = KTVGiantChorusApiImpl()
+ self.ktvApi.createKTVGiantChorusApi?(config: giantConfig)
+ self.ktvApi.renewInnerDataStreamId()
+ self.ktvApi.setLrcView(view: self.lyricView)
+
+ self.ktvApi.addEventHandler(ktvApiEventHandler: self)
+
+ let connection = AgoraRtcConnection(channelId: "\(channelName)_ad", localUid: self.userId)
+ let _ = self.rtcKit.joinChannelEx(byToken: audienceToken, connection: connection, delegate: self, mediaOptions: self.mediaOptions())
+ self.rtcKit.setParametersEx("{\"rtc.use_audio4\": true}", connection: connection)
+
+ self.loadMusic()
+ }
+ } else {
+ getMccData(with: "\(userId)") {[weak self] rtcToken, rtmToken, rtcPlayerToken in
+ guard let self = self else {return}
+ self.rtcToken = rtcToken
+ self.rtmToken = rtmToken
+ self.rtcPlayerToken = rtcPlayerToken
+
+ let apiConfig = KTVApiConfig(appId: KeyCenter.AppId, rtmToken: self.type == .mcc ? (self.rtmToken ?? "") : "", engine: self.rtcKit, channelName: self.channelName, localUid: self.userId, chorusChannelName: "\(self.channelName)_ex", chorusChannelToken: self.rtcPlayerToken ?? "", type: .normal, musicType: self.type == .mcc ? .mcc : .local, maxCacheSize: 10, mccDomain: nil)
+
+ self.ktvApi = KTVApiImpl()
+ self.ktvApi.createKtvApi?(config: apiConfig)
+ self.ktvApi.renewInnerDataStreamId()
+ self.ktvApi.setLrcView(view: self.lyricView)
+ self.ktvApi.addEventHandler(ktvApiEventHandler: self)
+
+ self.rtcKit.joinChannel(byToken: KeyCenter.Token, channelId: self.channelName, uid: UInt(self.userId), mediaOptions: self.mediaOptions())
+ self.loadMusic()
+ }
}
}
@@ -197,16 +237,22 @@ class KTVViewController: UIViewController {
private func switchRole() {
ktvApi.switchSingerRole(newRole: role) { state, failReason in
-
+ if self.role == .soloSinger || self.role == .leadSinger {
+ if self.type == .mcc {
+ self.ktvApi.startSing(songCode: self.mccSongCode, startPos: 0)
+ } else {
+ let mUrl = Bundle.main.path(forResource: "不如跳舞", ofType: "mp4")!
+ self.ktvApi.startSing(url: mUrl, startPos: 0)
+ }
+ }
}
}
private func loadMusic() {
if type == .local {
- let mUrl = Bundle.main.path(forResource: "成都", ofType: "mp3")!
- let lUrl = Bundle.main.path(forResource: "成都", ofType: "xml")!
+ let mUrl = Bundle.main.path(forResource: "不如跳舞", ofType: "mp4")!
+ let lUrl = Bundle.main.path(forResource: "不如跳舞", ofType: "xml")!
let songConfig = KTVSongConfiguration()
- songConfig.autoPlay = (role == .leadSinger || role == .soloSinger) ? true : false
songConfig.mode = role == .audience ? .loadNone : .loadMusicOnly
songConfig.mainSingerUid = mainSingerId
songConfig.songIdentifier = "chengdu"
@@ -215,8 +261,13 @@ class KTVViewController: UIViewController {
switchRole()
} else {
let songConfig = KTVSongConfiguration()
- songConfig.autoPlay = (role == .leadSinger || role == .soloSinger) ? true : false
- songConfig.mode = role == .audience ? .loadLrcOnly : .loadMusicAndLrc
+ if role == .audience {
+ songConfig.mode = .loadLrcOnly
+ } else if role == .coSinger {
+ songConfig.mode = .loadMusicOnly
+ } else {
+ songConfig.mode = .loadMusicAndLrc
+ }
songConfig.mainSingerUid = mainSingerId
songConfig.songIdentifier = "\(mccSongCode)"
ktvApi.loadMusic(songCode: mccSongCode, config: songConfig, onMusicLoadStateListener: self)
@@ -229,7 +280,10 @@ class KTVViewController: UIViewController {
}
private func leaveChannel() {
- ktvApi.cleanCache()
+ ApiManager.shared.fetchStopCloud()
+ if ktvApi != nil {
+ ktvApi.cleanCache()
+ }
rtcKit.leaveChannel()
}
@@ -266,6 +320,82 @@ class KTVViewController: UIViewController {
}
}
+ private func getCantataMccData(with userId: String, completion:@escaping ((String?, String?, String?, String?)->Void)) {
+ var tokenMap1:[Int: String] = [:], tokenMap2:[Int: String] = [:], tokenMap3:[Int: String] = [:]
+
+ let dispatchGroup = DispatchGroup()
+ dispatchGroup.enter()
+ NetworkManager.shared.generateTokens(channelName: channelName ,
+ uid: "\(userId)",
+ tokenGeneratorType: .token006,
+ tokenTypes: [.rtc, .rtm]) { tokenMap in
+ tokenMap1 = tokenMap
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.enter()
+ NetworkManager.shared.generateTokens(channelName: "\(channelName )_ad",
+ uid: "\(userId)",
+ tokenGeneratorType: .token006,
+ tokenTypes: [.rtc]) { tokenMap in
+ tokenMap2 = tokenMap
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.enter()
+ NetworkManager.shared.generateTokens(channelName: "\(channelName )",
+ uid: "2023",
+ tokenGeneratorType: .token006,
+ tokenTypes: [.rtc]) { tokenMap in
+ tokenMap3 = tokenMap
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.notify(queue: .main){
+ guard let rtcToken = tokenMap1[NetworkManager.AgoraTokenType.rtc.rawValue],
+ let rtmToken = tokenMap1[NetworkManager.AgoraTokenType.rtm.rawValue],
+ let audienceToken = tokenMap2[NetworkManager.AgoraTokenType.rtc.rawValue],
+ let rtcPlayerToken = tokenMap3[NetworkManager.AgoraTokenType.rtc.rawValue]
+ else {
+ completion(nil, nil, nil, nil)
+ return
+ }
+ completion(rtcToken, rtmToken, audienceToken, rtcPlayerToken)
+ }
+ }
+
+ private func getCloudMixerToken(with userId: String, completion:@escaping ((String, String)->Void)) {
+ var tokenMap1:[Int: String] = [:], tokenMap2:[Int: String] = [:]
+
+ let dispatchGroup = DispatchGroup()
+ dispatchGroup.enter()
+ NetworkManager.shared.generateTokens(channelName: channelName,
+ uid: "0",
+ tokenGeneratorType: .token007,
+ tokenTypes: [.rtc]) { tokenMap in
+ tokenMap1 = tokenMap
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.enter()
+ NetworkManager.shared.generateTokens(channelName: "\(channelName)_ad",
+ uid: userId,
+ tokenGeneratorType: .token007,
+ tokenTypes: [.rtc]) { tokenMap in
+ tokenMap2 = tokenMap
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.notify(queue: .main){
+ if let inputToken = tokenMap1[NetworkManager.AgoraTokenType.rtc.rawValue],
+ let outputToken = tokenMap2[NetworkManager.AgoraTokenType.rtc.rawValue] {
+ completion(inputToken, outputToken)
+ } else {
+ print("获取合流Token失败")
+ }
+ }
+ }
+
}
extension KTVViewController {
@@ -312,6 +442,12 @@ extension KTVViewController: AgoraRtcEngineDelegate {
}
+ func rtcEngine(_ engine: AgoraRtcEngineKit, audioMetadataReceived uid: UInt, metadata: Data) {
+ if isCantata {
+ self.ktvApi.didAudioMetadataReceived(uid: uid, metadata: metadata)
+ }
+ }
+
@objc private func leaveChorus() {
if role == .coSinger {
//合唱者才能离开合唱
@@ -352,7 +488,7 @@ extension KTVViewController: AgoraRtcEngineDelegate {
}
extension KTVViewController: IMusicLoadStateListener {
- func onMusicLoadProgress(songCode: Int, percent: Int, status: AgoraMusicContentCenterPreloadStatus, msg: String?, lyricUrl: String?) {
+ func onMusicLoadProgress(songCode: Int, percent: Int, state: AgoraMusicContentCenterPreloadState, msg: String?, lyricUrl: String?) {
//歌曲加载进度
print("歌曲加载进度:\(percent)%")
}
@@ -374,7 +510,7 @@ extension KTVViewController: IMusicLoadStateListener {
}
extension KTVViewController: KTVApiEventHandlerDelegate {
- func onMusicPlayerStateChanged(state: AgoraMediaPlayerState, error: AgoraMediaPlayerError, isLocal: Bool) {
+ func onMusicPlayerStateChanged(state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason, isLocal: Bool) {
}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KeyCenter.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KeyCenter.swift
new file mode 100644
index 0000000..f1f9140
--- /dev/null
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/KeyCenter.swift
@@ -0,0 +1,51 @@
+//
+// KeyCenter.swift
+// OpenLive
+//
+// Created by GongYuhua on 6/25/16.
+// Copyright © 2016 Agora. All rights reserved.
+//
+import Foundation
+
+@objcMembers
+class KeyCenter: NSObject {
+
+ /**
+ Agora APP ID.
+ Agora assigns App IDs to app developers to identify projects and organizations.
+ If you have multiple completely separate apps in your organization, for example built by different teams,
+ you should use different App IDs.
+ If applications need to communicate with each other, they should use the same App ID.
+ In order to get the APP ID, you can open the agora console (https://console.shengwang.cn/) to create a project,
+ then the APP ID can be found in the project detail page.
+ 声网APP ID
+ Agora 给应用程序开发人员分配 App ID,以识别项目和组织。如果组织中有多个完全分开的应用程序,例如由不同的团队构建,
+ 则应使用不同的 App ID。如果应用程序需要相互通信,则应使用同一个App ID。
+ 进入声网控制台(https://console.shengwang.cn/),创建一个项目,进入项目配置页,即可看到APP ID。
+ */
+
+ static let AppId: String = ""
+
+ /**
+ Certificate.
+ Agora provides App certificate to generate Token. You can deploy and generate a token on your server,
+ or use the console to generate a temporary token.
+ In order to get the APP ID, you can open the agora console (https://console.shengwang.cn/) to create a project with the App Certificate enabled,
+ then the APP Certificate can be found in the project detail page.
+ PS: If the project does not have certificates enabled, leave this field blank.
+ 声网APP证书
+ Agora 提供 App certificate 用以生成 Token。您可以在您的服务器部署并生成,或者使用控制台生成临时的 Token。
+ 进入声网控制台(https://console.shengwang.cn/),创建一个带证书鉴权的项目,进入项目配置页,即可看到APP证书。
+ 注意:如果项目没有开启证书鉴权,这个字段留空。
+ */
+
+ static let Certificate: String? = nil
+
+ // cantata cloud server key
+ static let RestfulApiKey: String? = ""
+ // cantata cloud server secret
+ static let RestfulApiSecret: String? = ""
+
+ static var baseServerUrl: String? = "https://service.shengwang.cn/"
+ static var Token: String? = ""
+}
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/NetworkManager.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/NetworkManager.swift
index 9b6a03a..8a863df 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/NetworkManager.swift
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/NetworkManager.swift
@@ -55,7 +55,7 @@ class NetworkManager:NSObject {
@objc static let shared = NetworkManager()
private let baseUrl = "https://agoraktv.xyz/1.1/functions/"
- private let baseServerUrl: String = "https://toolbox.bj2.agoralab.co/v1/"
+ private let baseServerUrl: String = "https://service.shengwang.cn/toolbox/v2/"
private func basicAuth(key: String, password: String) -> String {
let loginString = String(format: "%@:%@", key, password)
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/ViewController.swift b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/ViewController.swift
index 8abe846..9cfb4d8 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/ViewController.swift
+++ b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/ViewController.swift
@@ -15,6 +15,8 @@ class ViewController: UIViewController {
var rtmToken: String?
var rtcPlayerToken: String?
var userId: Int = 0
+ var selBtn: UIButton?
+ var isCantata: Bool = false
@IBOutlet weak var tf: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
@@ -24,21 +26,30 @@ class ViewController: UIViewController {
@IBAction func leadSet(_ sender: UIButton) {
role = .leadSinger
+ if self.selBtn != nil {
+ self.selBtn?.setTitleColor(.white, for: .normal)
+ }
+ sender.setTitleColor(.red, for: .normal)
+ self.selBtn = sender
}
-
- @IBAction func coSet(_ sender: Any) {
- role = .coSinger
- }
-
-
- @IBAction func auSet(_ sender: Any) {
+
+ @IBAction func auSet(_ sender: UIButton) {
role = .audience
+ if self.selBtn != nil {
+ self.selBtn?.setTitleColor(.white, for: .normal)
+ }
+ sender.setTitleColor(.red, for: .normal)
+ self.selBtn = sender
}
@IBAction func valueChange(_ sender: UISegmentedControl) {
type = sender.selectedSegmentIndex == 0 ? .mcc : .local
}
+ @IBAction func ktvTypeChange(_ sender: UISegmentedControl) {
+ isCantata = sender.selectedSegmentIndex == 0 ? false : true
+ }
+
@IBAction func startSing(_ sender: UIButton) {
if tf.text?.count == 0 {
return
@@ -48,6 +59,7 @@ class ViewController: UIViewController {
let vc = KTVViewController()
vc.role = role
vc.type = type
+ vc.isCantata = isCantata
vc.channelName = channelName
self.navigationController?.pushViewController(vc, animated: true)
}
diff --git "a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\344\270\215\345\246\202\350\267\263\350\210\236.mp4" "b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\344\270\215\345\246\202\350\267\263\350\210\236.mp4"
new file mode 100644
index 0000000..94d0846
Binary files /dev/null and "b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\344\270\215\345\246\202\350\267\263\350\210\236.mp4" differ
diff --git "a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\344\270\215\345\246\202\350\267\263\350\210\236.xml" "b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\344\270\215\345\246\202\350\267\263\350\210\236.xml"
new file mode 100644
index 0000000..5e8e027
--- /dev/null
+++ "b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\344\270\215\345\246\202\350\267\263\350\210\236.xml"
@@ -0,0 +1,1440 @@
+
+
+
+ 不如跳舞
+ 陈慧琳
+ 1
+
+
+
+
+
+ 你
+
+
+ 正
+
+
+ 在
+
+
+ 说
+
+
+ 着
+
+
+ 什
+
+
+ 么
+
+
+ 我
+
+
+ 很
+
+
+ 模
+
+
+ 糊
+
+
+
+
+ 只
+
+
+ 有
+
+
+ 好
+
+
+ 音
+
+
+ 乐
+
+
+ 让
+
+
+ 我
+
+
+ 听
+
+
+ 得
+
+
+ 清
+
+
+ 楚
+
+
+
+
+ 言
+
+
+ 语
+
+
+ 从
+
+
+ 这
+
+
+ 里
+
+
+ 开
+
+
+ 始
+
+
+ 失
+
+
+ 去
+
+
+ 作
+
+
+ 用
+
+
+
+
+ 只
+
+
+ 有
+
+
+ 节
+
+
+ 奏
+
+
+ 感
+
+
+ 能
+
+
+ 够
+
+
+ 互
+
+
+ 相
+
+
+ 接
+
+
+ 触
+
+
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+
+
+ 需
+
+
+ 要
+
+
+ 的
+
+
+ 是
+
+
+ 速
+
+
+ 度
+
+
+
+
+ 你
+
+
+ 看
+
+
+ 看
+
+
+ 你
+
+
+ 看
+
+
+ 看
+
+
+
+
+ 是
+
+
+ 什
+
+
+ 么
+
+
+ 在
+
+
+ 作
+
+
+ 主
+
+
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+
+
+ 是
+
+
+ 一
+
+
+ 个
+
+
+ 大
+
+
+ 银
+
+
+ 幕
+
+
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 踏
+
+
+ 着
+
+
+ 一
+
+
+ 样
+
+
+ 的
+
+
+ 脚
+
+
+ 步
+
+
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+ 聊
+
+
+ 天
+
+
+ 倒
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 让
+
+
+ 自
+
+
+ 己
+
+
+ 觉
+
+
+ 得
+
+
+ 舒
+
+
+ 服
+
+
+
+
+ 是
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 的
+
+
+ 天
+
+
+ 赋
+
+
+
+
+ 继
+
+
+ 续
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 谈
+
+
+ 恋
+
+
+ 爱
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 用
+
+
+ 这
+
+
+ 个
+
+
+ 方
+
+
+ 式
+
+
+ 相
+
+
+ 处
+
+
+
+
+ 没
+
+
+ 有
+
+
+ 人
+
+
+ 觉
+
+
+ 得
+
+
+ 孤
+
+
+ 独
+
+
+
+
+ 也
+
+
+ 没
+
+
+ 有
+
+
+ 包
+
+
+ 袱
+
+
+
+
+ 你
+
+
+ 正
+
+
+ 在
+
+
+ 说
+
+
+ 着
+
+
+ 什
+
+
+ 么
+
+
+ 我
+
+
+ 很
+
+
+ 模
+
+
+ 糊
+
+
+
+
+ 只
+
+
+ 有
+
+
+ 好
+
+
+ 音
+
+
+ 乐
+
+
+ 让
+
+
+ 我
+
+
+ 听
+
+
+ 得
+
+
+ 清
+
+
+ 楚
+
+
+
+
+ 言
+
+
+ 语
+
+
+ 从
+
+
+ 这
+
+
+ 里
+
+
+ 开
+
+
+ 始
+
+
+ 失
+
+
+ 去
+
+
+ 作
+
+
+ 用
+
+
+
+
+ 只
+
+
+ 有
+
+
+ 节
+
+
+ 奏
+
+
+ 感
+
+
+ 能
+
+
+ 够
+
+
+ 互
+
+
+ 相
+
+
+ 接
+
+
+ 触
+
+
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+
+
+ 需
+
+
+ 要
+
+
+ 的
+
+
+ 是
+
+
+ 速
+
+
+ 度
+
+
+
+
+ 你
+
+
+ 看
+
+
+ 看
+
+
+ 你
+
+
+ 看
+
+
+ 看
+
+
+
+
+ 是
+
+
+ 什
+
+
+ 么
+
+
+ 在
+
+
+ 作
+
+
+ 主
+
+
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+ 全
+
+
+ 世
+
+
+ 界
+
+
+
+
+ 是
+
+
+ 一
+
+
+ 个
+
+
+ 大
+
+
+ 银
+
+
+ 幕
+
+
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 踏
+
+
+ 着
+
+
+ 一
+
+
+ 样
+
+
+ 的
+
+
+ 脚
+
+
+ 步
+
+
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 聊
+
+
+ 天
+
+
+ 倒
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 让
+
+
+ 自
+
+
+ 己
+
+
+ 觉
+
+
+ 得
+
+
+ 舒
+
+
+ 服
+
+
+
+
+ 是
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 的
+
+
+ 天
+
+
+ 赋
+
+
+
+
+ 继
+
+
+ 续
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 谈
+
+
+ 恋
+
+
+ 爱
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 用
+
+
+ 这
+
+
+ 个
+
+
+ 方
+
+
+ 式
+
+
+ 相
+
+
+ 处
+
+
+
+
+ 没
+
+
+ 有
+
+
+ 人
+
+
+ 觉
+
+
+ 得
+
+
+ 孤
+
+
+ 独
+
+
+
+
+ 也
+
+
+ 没
+
+
+ 有
+
+
+ 包
+
+
+ 袱
+
+
+
+
+ 当
+
+
+ 所
+
+
+ 有
+
+
+ 甜
+
+
+ 言
+
+
+ 蜜
+
+
+ 语
+
+
+ 都
+
+
+ 那
+
+
+ 么
+
+
+ 虚
+
+
+ 无
+
+
+
+
+ 让
+
+
+ 我
+
+
+ 们
+
+
+ 一
+
+
+ 起
+
+
+ 做
+
+
+ 个
+
+
+ 节
+
+
+ 奏
+
+
+ 的
+
+
+ 信
+
+
+ 徒
+
+
+
+
+ 让
+
+
+ 速
+
+
+ 度
+
+
+ 变
+
+
+ 成
+
+
+ 一
+
+
+ 场
+
+
+ 前
+
+
+ 所
+
+
+ 未
+
+
+ 有
+
+
+ 的
+
+
+ 梦
+
+
+
+
+ 你
+
+
+ 看
+
+
+ 你
+
+
+ 看
+
+
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 聊
+
+
+ 天
+
+
+ 倒
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 让
+
+
+ 自
+
+
+ 己
+
+
+ 觉
+
+
+ 得
+
+
+ 舒
+
+
+ 服
+
+
+
+
+ 是
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 的
+
+
+ 天
+
+
+ 赋
+
+
+
+
+ 继
+
+
+ 续
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 谈
+
+
+ 恋
+
+
+ 爱
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 用
+
+
+ 这
+
+
+ 个
+
+
+ 方
+
+
+ 式
+
+
+ 相
+
+
+ 处
+
+
+
+
+ 没
+
+
+ 有
+
+
+ 人
+
+
+ 觉
+
+
+ 得
+
+
+ 孤
+
+
+ 独
+
+
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 聊
+
+
+ 天
+
+
+ 倒
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 让
+
+
+ 自
+
+
+ 己
+
+
+ 觉
+
+
+ 得
+
+
+ 舒
+
+
+ 服
+
+
+
+
+ 是
+
+
+ 每
+
+
+ 个
+
+
+ 人
+
+
+ 的
+
+
+ 天
+
+
+ 赋
+
+
+
+
+ 继
+
+
+ 续
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 谈
+
+
+ 恋
+
+
+ 爱
+
+
+ 不
+
+
+ 如
+
+
+ 跳
+
+
+ 舞
+
+
+
+
+ 用
+
+
+ 这
+
+
+ 个
+
+
+ 方
+
+
+ 式
+
+
+ 相
+
+
+ 处
+
+
+
+
+ 没
+
+
+ 有
+
+
+ 人
+
+
+ 觉
+
+
+ 得
+
+
+ 孤
+
+
+ 独
+
+
+
+
+ 也
+
+
+ 没
+
+
+ 有
+
+
+ 包
+
+
+ 袱
+
+
+
+
+
\ No newline at end of file
diff --git "a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\346\210\220\351\203\275.mp3" "b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\346\210\220\351\203\275.mp3"
deleted file mode 100644
index da4336d..0000000
Binary files "a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\346\210\220\351\203\275.mp3" and /dev/null differ
diff --git "a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\346\210\220\351\203\275.xml" "b/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\346\210\220\351\203\275.xml"
deleted file mode 100644
index 1328c0f..0000000
--- "a/KTVAPI/iOS/Example/KTVApiDemo/KTVApiDemo/\346\210\220\351\203\275.xml"
+++ /dev/null
@@ -1,1225 +0,0 @@
-
-
-
- 成都
- 赵雷
- 1
-
-
-
-
-
- 让
-
-
- 我
-
-
- 掉
-
-
- 下
-
-
- 眼
-
-
- 泪
-
-
- 的
-
-
-
-
- 不
-
-
- 止
-
-
- 昨
-
-
- 夜
-
-
- 的
-
-
- 酒
-
-
-
-
- 让
-
-
- 我
-
-
- 依
-
-
- 依
-
-
- 不
-
-
- 舍
-
-
- 的
-
-
-
-
- 不
-
-
- 止
-
-
- 你
-
-
- 的
-
-
- 温
-
-
- 柔
-
-
-
-
- 余
-
-
- 路
-
-
- 还
-
-
- 要
-
-
- 走
-
-
- 多
-
-
- 久
-
-
-
-
- 你
-
-
- 攥
-
-
- 着
-
-
- 我
-
-
- 的
-
-
- 手
-
-
-
-
- 让
-
-
- 我
-
-
- 感
-
-
- 到
-
-
- 为
-
-
- 难
-
-
- 的
-
-
-
-
- 是
-
-
- 挣
-
-
- 扎
-
-
- 的
-
-
- 自
-
-
- 由
-
-
-
-
- 分
-
-
- 别
-
-
- 总
-
-
- 是
-
-
- 在
-
-
- 九
-
-
- 月
-
-
-
-
- 回
-
-
- 忆
-
-
- 是
-
-
- 思
-
-
- 念
-
-
- 的
-
-
- 愁
-
-
-
-
- 深
-
-
- 秋
-
-
- 嫩
-
-
- 绿
-
-
- 的
-
-
- 垂
-
-
- 柳
-
-
-
-
- 亲
-
-
- 吻
-
-
- 着
-
-
- 我
-
-
- 额
-
-
- 头
-
-
-
-
- 在
-
-
- 那
-
-
- 座
-
-
- 阴
-
-
- 雨
-
-
- 的
-
-
- 小
-
-
- 城
-
-
- 里
-
-
-
-
- 我
-
-
- 从
-
-
- 未
-
-
- 忘
-
-
- 记
-
-
- 你
-
-
-
-
- 成
-
-
- 都
-
-
- 带
-
-
- 不
-
-
- 走
-
-
- 的
-
-
- 只
-
-
- 有
-
-
- 你
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
- 你
-
-
- 会
-
-
- 挽
-
-
- 着
-
-
- 我
-
-
- 的
-
-
- 衣
-
-
- 袖
-
-
-
-
- 我
-
-
- 会
-
-
- 把
-
-
- 手
-
-
- 揣
-
-
- 进
-
-
- 裤
-
-
- 兜
-
-
-
-
- 走
-
-
- 到
-
-
- 玉
-
-
- 林
-
-
- 路
-
-
- 的
-
-
- 尽
-
-
- 头
-
-
-
-
- 坐
-
-
- 在
-
-
- 小
-
-
- 酒
-
-
- 馆
-
-
- 的
-
-
- 门
-
-
- 口
-
-
-
-
- 分
-
-
- 别
-
-
- 总
-
-
- 是
-
-
- 在
-
-
- 九
-
-
- 月
-
-
-
-
- 回
-
-
- 忆
-
-
- 是
-
-
- 思
-
-
- 念
-
-
- 的
-
-
- 愁
-
-
-
-
- 深
-
-
- 秋
-
-
- 嫩
-
-
- 绿
-
-
- 的
-
-
- 垂
-
-
- 柳
-
-
-
-
- 亲
-
-
- 吻
-
-
- 着
-
-
- 我
-
-
- 额
-
-
- 头
-
-
-
-
- 在
-
-
- 那
-
-
- 座
-
-
- 阴
-
-
- 雨
-
-
- 的
-
-
- 小
-
-
- 城
-
-
- 里
-
-
-
-
- 我
-
-
- 从
-
-
- 未
-
-
- 忘
-
-
- 记
-
-
- 你
-
-
-
-
- 成
-
-
- 都
-
-
- 带
-
-
- 不
-
-
- 走
-
-
- 的
-
-
- 只
-
-
- 有
-
-
- 你
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
- 你
-
-
- 会
-
-
- 挽
-
-
- 着
-
-
- 我
-
-
- 的
-
-
- 衣
-
-
- 袖
-
-
-
-
- 我
-
-
- 会
-
-
- 把
-
-
- 手
-
-
- 揣
-
-
- 进
-
-
- 裤
-
-
- 兜
-
-
-
-
- 走
-
-
- 到
-
-
- 玉
-
-
- 林
-
-
- 路
-
-
- 的
-
-
- 尽
-
-
- 头
-
-
-
-
- 坐
-
-
- 在
-
-
- 小
-
-
- 酒
-
-
- 馆
-
-
- 的
-
-
- 门
-
-
- 口
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
- 你
-
-
- 会
-
-
- 挽
-
-
- 着
-
-
- 我
-
-
- 的
-
-
- 衣
-
-
- 袖
-
-
-
-
- 我
-
-
- 会
-
-
- 把
-
-
- 手
-
-
- 揣
-
-
- 进
-
-
- 裤
-
-
- 兜
-
-
-
-
- 走
-
-
- 到
-
-
- 玉
-
-
- 林
-
-
- 路
-
-
- 的
-
-
- 尽
-
-
- 头
-
-
-
-
- 走
-
-
- 过
-
-
- 小
-
-
- 酒
-
-
- 馆
-
-
- 的
-
-
- 门
-
-
- 口
-
-
-
-
- 和
-
-
- 我
-
-
- 在
-
-
- 成
-
-
- 都
-
-
- 的
-
-
- 街
-
-
- 头
-
-
- 走
-
-
- 一
-
-
- 走
-
-
-
-
- 直
-
-
- 到
-
-
- 所
-
-
- 有
-
-
- 的
-
-
- 灯
-
-
- 都
-
-
- 熄
-
-
- 灭
-
-
- 了
-
-
- 也
-
-
- 不
-
-
- 停
-
-
- 留
-
-
-
-
-
\ No newline at end of file
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/PodFile b/KTVAPI/iOS/Example/KTVApiDemo/PodFile
index ec09ea2..dda56e0 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/PodFile
+++ b/KTVAPI/iOS/Example/KTVApiDemo/PodFile
@@ -3,8 +3,9 @@ use_frameworks!
platform :ios, '13.0'
target 'KTVApiDemo' do
- pod 'AgoraRtcEngine_Special_iOS', '4.1.1.23'
- pod 'AgoraLyricsScore', '1.1.1-beta-3'
+ pod 'AgoraRtcEngine_Special_iOS', '4.3.2.2'
+ pod 'AgoraLyricsScore', '1.1.6'
pod 'Zip'
pod 'SVProgressHUD'
+ pod 'SwiftProtobuf'
end
diff --git a/KTVAPI/iOS/Example/KTVApiDemo/Podfile.lock b/KTVAPI/iOS/Example/KTVApiDemo/Podfile.lock
index 3870da4..9fa3f7b 100644
--- a/KTVAPI/iOS/Example/KTVApiDemo/Podfile.lock
+++ b/KTVAPI/iOS/Example/KTVApiDemo/Podfile.lock
@@ -1,30 +1,39 @@
PODS:
- - AgoraLyricsScore (1.1.1-beta-3)
- - AgoraRtcEngine_Special_iOS (4.1.1.23)
+ - AgoraComponetLog (0.0.1)
+ - AgoraLyricsScore (1.1.6):
+ - AgoraComponetLog
+ - Zip
+ - AgoraRtcEngine_Special_iOS (4.3.2.2)
- SVProgressHUD (2.3.1):
- SVProgressHUD/Core (= 2.3.1)
- SVProgressHUD/Core (2.3.1)
+ - SwiftProtobuf (1.25.2)
- Zip (2.1.2)
DEPENDENCIES:
- - AgoraLyricsScore (= 1.1.1-beta-3)
- - AgoraRtcEngine_Special_iOS (= 4.1.1.23)
+ - AgoraLyricsScore (= 1.1.6)
+ - AgoraRtcEngine_Special_iOS (= 4.3.2.2)
- SVProgressHUD
+ - SwiftProtobuf
- Zip
SPEC REPOS:
trunk:
+ - AgoraComponetLog
- AgoraLyricsScore
- AgoraRtcEngine_Special_iOS
- SVProgressHUD
+ - SwiftProtobuf
- Zip
SPEC CHECKSUMS:
- AgoraLyricsScore: 7e25860ccecd3c6ed73f2b503f1d73d8849079e1
- AgoraRtcEngine_Special_iOS: 6a3f7814058f819b374cec3037cbbf7c4de519a5
+ AgoraComponetLog: c52aec8db3ea38c5693fd4f1beee05ba7715e68b
+ AgoraLyricsScore: 7576387b199cdc3b2752a113688abd0a807a2ca3
+ AgoraRtcEngine_Special_iOS: 4f1a1d2f2e7b564735fd74ced733c76e33b5dc04
SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22
+ SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1
Zip: b3fef584b147b6e582b2256a9815c897d60ddc67
-PODFILE CHECKSUM: d1c78c95a57910e01b805128afe6e86b075e3b0e
+PODFILE CHECKSUM: db850d4294833f17b507bcd17de203fd23084ce3
-COCOAPODS: 1.12.1
+COCOAPODS: 1.15.2