diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/K \346\255\214\346\210\277\345\234\272\346\231\257\345\214\226 Kotlin API for Android.md" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/K \346\255\214\346\210\277\345\234\272\346\231\257\345\214\226 Kotlin API for Android.md"
new file mode 100644
index 00000000000..b8ab3bac3fa
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/K \346\255\214\346\210\277\345\234\272\346\231\257\345\214\226 Kotlin API for Android.md"
@@ -0,0 +1,276 @@
+本文提供在线 K 歌房场景定制化 Kotlin API。你可以在 GitHub 上查看源码文件 [KTVApi.kt](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-Android/Android/scenes/ktv/src/main/java/io/agora/scene/ktv/live/KTVApi.kt) 和 [KTVApiImpl.kt](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-Android/Android/scenes/ktv/src/main/java/io/agora/scene/ktv/live/KTVApiImpl.kt)。
+
+
本文适用于场景化 API v2.1.1。
+
+## 方法
+
+### initWithRtcEngine
+
+```kotlin
+fun initWithRtcEngine(
+ engine: RtcEngine,
+ channelName: String,
+ musicCenter: IAgoraMusicContentCenter,
+ player: IAgoraMusicPlayer,
+ streamId: Int,
+ ktvApiEventHandler: KTVApiEventHandler
+)
+```
+
+初始化 KTV API。
+
+调用该方法可以初始化 KTV API 模块内部变量和缓存数据,并注册相应的回调监听。
+
+#### 注意事项
+
+调用其他 KTV API 之前,你需要先调用本方法初始化。
+
+#### 参数
+
+- `engine`: [RtcEngine](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/rtc_interface_class.html#class_irtcengine) 实例。
+- `channelName`: 待加入的频道名。
+- `musicCenter`: 版权音乐内容中心实例。详见 [IAgoraMusicContentCenter](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/rtc_interface_class.html#class_imusiccontentcenter)。
+- `player`: 音乐播放器实例。详见 [IAgoraMusicPlayer](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/rtc_interface_class.html#class_imusicplayer)。
+- `streamId`: 数据流(Data Stream)ID。
+- `ktvApiEventHandler`: K 歌场景化 API 的事件句柄。详见[回调](#onplayerstatechanged)。
+
+
+### release
+
+```kotlin
+fun release()
+```
+
+释放 KTV API 资源。
+
+调用该方法可以清空 KTV API 模块内部变量和缓存数据,取消 `ktvApiEventHandler` 的事件监听,取消网络请求等。
+
+#### 用法示例
+
+```kotlin
+// K 歌房 Activity 销毁时调用 ktvApiProtocol 释放,随后释放创建的实例
+@Override
+protected void onDestroy() {
+ super.onDestroy();
+ ktvApiProtocol.release()
+ // 释放 mRtcEngine、iAgoraMusicContentCenter、mPlayer、streamId
+}
+```
+
+### loadSong
+
+```kotlin
+fun loadSong(
+ songCode: Long,
+ config: KTVSongConfiguration,
+ onLoaded: (songCode: Long, lyricUrl: String, role: KTVSingRole, state: KTVLoadSongState) -> Unit
+)
+```
+
+加载歌曲。
+
+传入歌曲编号和 K 歌配置,调用 `loadSong` 加载歌曲。加载结果会异步地通过 `onLoaded` 回调通知你。
+
+#### 参数
+
+- `songCode`: 歌曲编号。
+- `config`: K 歌配置。详见 [KTVSongConfiguration](#ktvsongconfiguration)。
+- `onLoaded`: 歌词加载状态事件,包含如下参数:
+ - `songCode`: 歌曲编号。
+ - `lyricUrl`: 歌词文件的 URL。
+ - `role`: 当前用户角色,详见 [KTVSingRole](#ktvsingrole)。
+ - `state`: 歌曲加载状态,详见 [KTVLoadSongState](#ktvloadsongstate)。
+
+
+### playSong
+
+```kotlin
+fun playSong(songCode: Long)
+```
+
+播放歌曲。
+
+建议在调用 `loadSong` 函数并收到 `onLoaded` 回调的 `KTVLoadSongState.KTVLoadSongStateOK` 状态后再调用 `playSong`。
+
+#### 参数
+
+- `songCode`: 歌曲编号。
+
+### stopSong
+
+```kotlin
+fun stopSong()
+```
+
+结束播放歌曲。
+
+### resumePlay
+
+```kotlin
+fun resumePlay()
+```
+
+恢复播放歌曲。
+
+
+### pausePlay
+
+```kotlin
+fun pausePlay()
+```
+
+暂停播放歌曲。
+
+### seek
+
+```kotlin
+fun seek(time: Long)
+```
+
+跳转到指定时间播放歌曲。
+
+#### 参数
+
+- `time`: 跳转的时间点。单位为毫秒。
+
+### selectTrackMode
+
+```kotlin
+fun selectTrackMode(mode: KTVPlayerTrackMode)
+```
+
+选择播放的音轨。
+
+歌曲的音轨包含原唱和伴奏。调用该方法可以选择播放的音轨。
+
+#### 参数
+
+- `mode`: 音轨的类型。详见 [KTVPlayerTrackMode](#ktvplayertrackmode)。
+
+### setLycView
+
+```kotlin
+fun setLycView(view: LrcControlView)
+```
+
+设置歌词控制视图。
+
+歌词控制视图用于显示歌词和控制歌词滚动等操作。调用该方法后,可以将歌词控制视图和 KTV 模块进行绑定,从而实现歌词的同步滚动。
+
+#### 参数
+
+- `view`: 歌词控制视图,`LrcControlView` 对象。
+
+
+## 回调
+
+### onPlayerStateChanged
+
+```kotlin
+interface KTVApiEventHandler {
+ fun onPlayerStateChanged(controller: KTVApi, state: Constants.MediaPlayerState, error: Constants.MediaPlayerError, isLocal: Boolean)
+}
+```
+播放器状态改变回调。
+
+#### 参数
+
+- `controller`: KTVApi 实例。
+- `state`: 播放器的当前状态。详见 [MediaPlayerState](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/enum_mediaplayerstate.html?platform=Android)。
+- `error`: 播放器的错误码。详见 [MediaPlayerError](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/enum_mediaplayererror.html?platform=Android)。
+- `isLocal`: 是否为本地事件:
+ - `true`: 代表是本地播放器的状态改变。可用于主唱和伴唱监听本地播放器状态。
+ - `false`: 是远端播放器的状态改变。可用于伴唱和听众知晓主唱的播放器状态,从而方便后续进行多端播放同步。
+
+举例来说,在合唱场景下,主唱、伴唱、听众收到的 `onPlayerStateChanged` 回调有如下区别:
+
+- 主唱:收到一个 `isLocal` 为 `true` 的回调,报告主唱播放器的状态改变。
+- 伴唱:收到一个 `isLocal` 为 `true` 的回调,报告伴唱播放器的状态改变;同时,还收到一个 `isLocal` 为 `false` 的回调,报告主唱播放器的状态改变。
+- 听众:收到一个 `isLocal` 为 `false` 的回调报告主唱端播放器的状态改变。
+
+
+## Enum class
+
+### KTVSongType
+
+```kotlin
+enum class KTVSongType {
+ KTVSongTypeSolo,
+ KTVSongTypeChorus
+}
+```
+K 歌场景类型:
+- `KTVSongTypeSolo`: 独唱场景
+- `KTVSongTypeChorus`: 合唱场景
+
+### KTVSingRole
+
+```kotlin
+enum class KTVSingRole {
+ KTVSingRoleMainSinger,
+ KTVSingRoleCoSinger,
+ KTVSingRoleAudience
+}
+```
+K 歌用户角色类型:
+- `KTVSingRoleMainSinger`: 主唱
+- `KTVSingRoleCoSinger`: 伴唱
+- `KTVSingRoleAudience`: 观众
+
+### KTVPlayerTrackMode
+
+```kotlin
+enum class KTVPlayerTrackMode {
+ KTVPlayerTrackOrigin,
+ KTVPlayerTrackAcc
+}
+```
+
+K 歌播放音轨类型:
+- `KTVPlayerTrackOrigin`: 原唱
+- `KTVPlayerTrackAcc`: 伴奏
+
+
+### KTVLoadSongState
+
+```kotlin
+enum class KTVLoadSongState {
+ KTVLoadSongStateOK,
+ KTVLoadSongStateInProgress,
+ KTVLoadSongStateNoLyricUrl,
+ KTVLoadSongStatePreloadFail,
+ KTVLoadSongStateIdle
+}
+```
+
+歌曲加载的状态:
+- `KTVLoadSongStateOK`: 加载成功
+- `KTVLoadSongStateInProgress`: 正在加载中
+- `KTVLoadSongStateNoLyricUrl`: 歌曲无法加载,缺少歌词地址
+- `KTVLoadSongStatePreloadFail`: 加载失败
+- `KTVLoadSongStateIdle`: 空闲状态,未加载歌曲
+
+
+## Data class
+
+### KTVSongConfiguration
+
+```kotlin
+data class KTVSongConfiguration(
+ val type: KTVSongType,
+ val role: KTVSingRole,
+ val songCode: Long,
+ val mainSingerUid: Int,
+ val coSingerUid: Int
+)
+```
+
+K 歌配置:
+
+- `type`: K 歌场景类型,详见 [KTVSongType](#ktvsongtype)
+- `role`: K 歌用户角色类型,详见 [KTVSingRole](#ktvsingrole)
+- `songCode`: 歌曲的编号
+- `mainSingerUid`: 主唱的 UID
+- `coSingerUid`: 伴唱的 UID
+
+UID 指用户 ID,用于标识频道内的用户,频道内的每个 UID 都必须是唯一。UID 是 32 位无符号整数,建议取值范围为 [1,232 -1]。
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\345\220\210\345\224\261.wsd" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\345\220\210\345\224\261.wsd"
new file mode 100644
index 00000000000..ad9312341d0
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\345\220\210\345\224\261.wsd"
@@ -0,0 +1,41 @@
+@startuml
+title 合唱 API 时序图
+autonumber
+skinparam monochrome true
+participant "主唱/伴唱 App" as a
+participant "声网 SDK" as b
+participant "听众 App" as c
+== 设置私有参数 ==
+a -> b: setParameters
+== 加入频道 ==
+a -> b: joinChannel
+c -> b: joinChannel
+== 加载歌曲歌词 ==
+a -> b: loadSong
+c -> b: loadSong
+b -->> a: KTVLoadSongStateOK
+b -->> c: KTVLoadSongStateOK
+== 开始播放歌曲 ==
+a -> b: playSong(KTVSingRoleMainSinger/KTVSingRoleCoSinger)
+b -->> a: onPlayerStateChanged(PLAYER_STATE_PLAYING)
+c -> b: playSong(KTVSingRoleAudience)
+b -->> c: onPlayerStateChanged(PLAYER_STATE_PLAYING)
+note left
+听众不播放歌曲,只是进入播放状态
+end note
+== 停止播放 ==
+a -> b: stopSong
+b -->> a: onPlayerStateChanged(PLAYER_STATE_STOPPED)
+c -> b: stopSong
+b -->> c: onPlayerStateChanged(PLAYER_STATE_STOPPED)
+== 关闭麦克风 ==
+a -> b: adjustRecordingSignalVolume
+== 更新媒体选项 ==
+a -> b: updateChannelMediaOptions
+c -> b: updateChannelMediaOptions
+note left
+主唱、伴唱、上麦听众发布麦克风,角色为主播
+
+普通听众不发布麦克风,角色为观众
+end note
+@enduml
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\347\202\271\346\255\214.wsd" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\347\202\271\346\255\214.wsd"
new file mode 100644
index 00000000000..4c35e523ee8
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\347\202\271\346\255\214.wsd"
@@ -0,0 +1,29 @@
+@startuml
+title 点歌 API 时序图
+autonumber
+skinparam monochrome true
+participant "App" as a
+participant "声网 SDK" as b
+== 初始化 KTV API 模块==
+a -> b: initWithRtcEngine
+== 获取歌曲列表(方式一:用关键词)==
+a -> b: searchMusic
+b -->> a: onMusicCollectionResult
+== 获取歌曲列表(方式二:用音乐榜单)==
+a -> b: getMusicCollectionByMusicChartId
+b -->> a: onMusicChartsResult
+== 加载歌曲 ==
+a -> b: loadSong
+b -->> a: KTVLoadSongStateOK
+== 开始播放歌曲 ==
+a -> b: playSong
+b -->> a: onPlayerStateChanged(PLAYER_STATE_PLAYING)
+== 控制歌曲播放 ==
+a ->b: seek/pause/resume/selectAudioTrack
+b -->> a: onPlayerStateChanged(PLAYER_STATE_XX)
+== 停止播放 ==
+a -> b: stopSong
+b -->> a: onPlayerStateChanged(PLAYER_STATE_STOPPED)
+== 释放 KTV API 模块资源 ==
+a ->b: release
+@enduml
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\347\213\254\345\224\261.wsd" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\347\213\254\345\224\261.wsd"
new file mode 100644
index 00000000000..6593bbf9cf4
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\347\213\254\345\224\261.wsd"
@@ -0,0 +1,39 @@
+@startuml
+title 独唱 API 时序图
+autonumber
+skinparam monochrome true
+participant "主唱 App" as a
+participant "声网 SDK" as b
+participant "听众 App" as c
+== 加入频道 ==
+a -> b: joinChannel
+c -> b: joinChannel
+== 加载歌曲歌词 ==
+a -> b: loadSong
+c -> b: loadSong
+b -->> a: KTVLoadSongStateOK
+b -->> c: KTVLoadSongStateOK
+== 开始播放歌曲 ==
+a -> b: playSong(KTVSingRoleMainSinger)
+b -->> a: onPlayerStateChanged(PLAYER_STATE_PLAYING)
+c -> b: playSong(KTVSingRoleAudience)
+b -->> c: onPlayerStateChanged(PLAYER_STATE_PLAYING)
+note left
+听众不播放歌曲,只是进入播放状态
+end note
+== 停止播放 ==
+a -> b: stopSong
+b -->> a: onPlayerStateChanged(PLAYER_STATE_STOPPED)
+c -> b: stopSong
+b -->> c: onPlayerStateChanged(PLAYER_STATE_STOPPED)
+== 关闭麦克风 ==
+a -> b: adjustRecordingSignalVolume
+== 更新媒体选项 ==
+a -> b: updateChannelMediaOptions
+c -> b: updateChannelMediaOptions
+note left
+主唱和上麦听众发布麦克风,角色为主播
+
+普通听众不发布麦克风,角色为观众
+end note
+@enduml
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\351\233\206\346\210\220.md" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\351\233\206\346\210\220.md"
new file mode 100644
index 00000000000..fdcb762a123
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/Android/\345\256\236\347\216\260\346\226\207\346\241\243/\351\233\206\346\210\220.md"
@@ -0,0 +1,584 @@
+## 概述
+
+为降低开发者的集成难度,声网为 K 歌房场景提供了场景化 API。场景化 API 封装了声网音视频 SDK 的 API,并提供了 K 歌业务常见的功能,例如,对主唱和伴唱进行 NTP 时间同步。你只需要调用一个场景化 API 即可实现通过多个音视频 SDK 的 API 完成的复杂代码逻辑,从而更轻松实现 K 歌场景。声网在 GitHub 上提供 KTV 场景化 API 的源码文件 [KTVApi.kt](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-Android/Android/scenes/ktv/src/main/java/io/agora/scene/ktv/live/KTVApi.kt) 和 [KTVApiImpl.kt](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-Android/Android/scenes/ktv/src/main/java/io/agora/scene/ktv/live/KTVApiImpl.kt)。
+
+本文介绍如何使用 KTV 场景化 API 实现点歌、独唱、合唱等基础业务功能。
+
+## 前提条件
+
+实现点歌、独唱、合唱前,请确保你已完成如下步骤:
+
+1. 参考项目配置 集成所需 SDK。
+2. 在工程文件中引入 [KTVApi.kt](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-Android/Android/scenes/ktv/src/main/java/io/agora/scene/ktv/live/KTVApi.kt) 和 [KTVApiImpl.kt](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-Android/Android/scenes/ktv/src/main/java/io/agora/scene/ktv/live/KTVApiImpl.kt) 文件。
+
+## 点歌
+
+本节介绍如何实现点歌功能。点歌指用户通过浏览榜单或搜索关键词选定想唱的正版音乐,然后下载播放音乐。用户需要在唱歌前进行点歌。
+
+### 方案介绍
+
+下图展示点歌的 API 调用时序图:
+
+![](https://web-cdn.agora.io/docs-files/1683360019651)
+
+### 1. 初始化 KTV API 模块
+
+实例化 `rtcEngine`、`musicCenter`、`musicPlayer`、`streamId` 实例,并将它们通过 `initWithRtcEngine` 方法传入 KTV API 模块。调用 KTV API 模块的 API 前,请确保已调用 `initWithRtcEngine` 初始化 KTV API 实例。
+
+
+1. 调用 [`create`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_initialize) 初始化 `RtcEngine`。
+
+ ```Kotlin
+ // 初始化 RtcEngine
+ val config = RtcEngineConfig()
+ config.mContext = null
+ config.mAppId = ""
+ config.mEventHandler = object : IRtcEngineEventHandler() {}
+ config.mChannelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING
+ config.mAudioScenario = AUDIO_SCENARIO_CHORUS
+ val mRtcEngine = RtcEngine.create(config) as RtcEngineEx
+ ```
+
+2. 调用 [`initialize`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_drm.html#api_imusiccontentcenter_initialize) 初始化 `IAgoraMusicContentCenter`。示例代码中需要传入 RTM Token。你可以参考[获取 RTM Token](/cn/Agora%20Platform/get_appid_token?platform=All%20Platforms#获取-rtm-token) 了解什么是 RTM Token,如何获取测试用途的临时 RTM Token,如何从服务器生成 RTM Token。
+
+ ```Kotlin
+ // 初始化 IAgoraMusicContentCenter
+ val contentCenterConfiguration = MusicContentCenterConfiguration()
+ contentCenterConfiguration.appId = ""
+ contentCenterConfiguration.mccUid =
+ contentCenterConfiguration.token = ""
+ val iAgoraMusicContentCenter = IAgoraMusicContentCenter.create(mRtcEngine)
+ iAgoraMusicContentCenter.initialize(contentCenterConfiguration)
+ ```
+
+3. 调用 [`createMusicPlayer`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_drm.html?platform=Android#api_imusiccontentcenter_createmusicplayer) 创建音乐播放器。
+
+ ```Kotlin
+ // 创建音乐播放器
+ val mPlayer = iAgoraMusicContentCenter.createMusicPlayer()
+ ```
+
+4. 调用 [`createDataStream`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_network.html#api_irtcengine_createdatastream2) 创建数据流。
+
+ ```Kotlin
+ // 创建数据流
+ val cfg = DataStreamConfig()
+ cfg.syncWithAudio = false
+ cfg.ordered = false
+ val streamId = mRtcEngine.createDataStream(cfg)
+ ```
+
+ 考虑到数据流的消息通道有频率限制,为了确保 KTV 模块和其他模块不会相互影响,声网建议你在不同模块中为数据流创建的 streamId
都不同。例如,如果你在其他模块中也使用 sendStreamMessage
发送数据流,请确保两个模块中数据流的 streamId
不同。此外,每个用户在每个频道中最多只能创建 5 个数据流,请不要超出上限。
+
+5. 调用 `initWithRtcEngine` 初始化 KTV API 实例。
+
+ ```Kotlin
+ // 初始化 KTV API 实例
+ val ktvApiProtocol = KTVApiImpl()
+ ktvApiProtocol.initWithRtcEngine(mRtcEngine, , iAgoraMusicContentCenter, mPlayer, streamId, object: KTVApiEventHandler {})
+ ```
+
+### 2. 获取歌曲列表
+
+通过关键词搜索或音乐榜单获取歌曲列表。
+
+```Kotlin
+// 用关键词搜索歌曲
+fun searchSong(condition: String, page: Int) {
+ // 搜索过滤条件
+ val jsonOption = "{\"pitchType\":1,\"needLyric\":true}"
+ val requestId = iAgoraMusicContentCenter.searchMusic(condition, page, 50, jsonOption)
+}
+
+// IMusicContentCenterEventHandler
+// 报告搜索结果
+override fun onMusicCollectionResult(
+ requestId: String?,
+ status: Int,
+ page: Int,
+ pageSize: Int,
+ total: Int,
+ list: Array?
+) {}
+```
+
+```Kotlin
+// 用音乐榜单获取歌曲
+fun searchSongWithRankingChartId(type: Int, page: Int) {
+ // 搜索过滤条件
+ val jsonOption = "{\"pitchType\":1,\"needLyric\":true}"
+ val requestId = iAgoraMusicContentCenter.getMusicCollectionByMusicChartId(condition, page, 50, jsonOption)
+}
+
+// IMusicContentCenterEventHandler
+// 报告搜索结果
+override fun onMusicChartsResult(
+ requestId: String?,
+ status: Int,
+ list: Array?
+) {}
+```
+
+### 3. 加载歌曲
+
+调用 `loadSong` 加载歌曲。该方法中你需要传入歌曲编号和 K 歌配置,例如当前的 K 歌场景(独唱或合唱)、用户角色、主唱伴唱的 UID。K 歌配置会决定 K 歌时歌曲的播放情况、各端用户的收发流情况等。歌曲加载结果会异步地通过 `onLoaded` 回调通知你。
+
+```Kotlin
+// isChorus: 歌曲是否合唱
+// isOwnSong: 是否为自己点的歌,点歌者默认为主唱
+// isChorusMem: 是否为伴唱点的歌
+val type = if (isChorus) KTVSongType.KTVSongTypeChorus else KTVSongType.KTVSongTypeSolo
+val role = if (isOwnSong) KTVSingRole.KTVSingRoleMainSinger else if (isChorusMem) KTVSingRole.KTVSingRoleCoSinger else KTVSingRole.KTVSingRoleAudience
+// 主唱 UID
+val mainSingerUid =
+// 伴唱 UID。如果是独唱场景,不存在伴唱角色,那么传入 0 即可。
+val coSingerUid =
+
+// 加载歌曲
+ktvApiProtocol.loadSong(
+ songCode,
+ KTVSongConfiguration(type, role, songCode, mainSingerUid, coSingerUid)
+) { song, lyricUrl, singRole, singState ->
+ if (singState === KTVLoadSongState.KTVLoadSongStateOK) {
+ // 歌曲加载成功的操作
+ ...
+ } else if (singState == KTVLoadSongState.KTVLoadSongStatePreloadFail) {
+ // 歌曲加载失败的操作
+ ...
+ } else if (singState == KTVLoadSongState.KTVLoadSongStateNoLyricUrl) {
+ // 歌曲没有歌词的操作
+ ...
+ }
+}
+```
+
+### 4. 播放歌曲
+
+收到 `onLoaded(KTVLoadSongStateOK)` 回调状态后,调用 `playSong` 开始播放歌曲。
+
+```Kotlin
+ktvApiProtocol.loadSong(
+ songCode,
+ KTVSongConfiguration(type, role, songCode, mainSingerUid, coSingerUid)
+) { song, lyricUrl, singRole, singState ->
+ if (singState === KTVLoadSongState.KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ ktvApiProtocol.playSong()
+ } else {
+ // 否则进行其他操作
+ ...
+ }
+}
+```
+
+### 5. 监听并控制歌曲播放
+
+歌曲播放时,音乐播放器会通过 `onPlayerStateChanged` 回调向业务层通知歌曲播放状态改变。收到 `onPlayerStateChanged(PLAYER_STATE_PLAYING)` 回调后,你可以使用 `seek`、`pause`、`resume`、`selectAudioTrack` 等方法控制播放器。
+
+KTV API 模块内部会自动处理播放器同步,因此你也可以通过 onPlayerStateChanged
回调获取远端播放器的状态。
+
+```Kotlin
+// 跳转到指定时间播放歌曲
+ktvApiProtocol.seek(time)
+```
+
+### 6. 停止歌曲播放
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放。
+
+```Kotlin
+ktvApiProtocol.stopSong()
+```
+
+### 7. 释放资源
+
+当退出 K 歌场景时,你需要调用 `release` 释放 KTV API 模块内的资源和取消注册事件回调。
+
+```Kotlin
+ktvApiProtocol.release()
+```
+
+## 独唱
+
+本节介绍如何实现独唱功能。主唱点歌后,可以开始独唱,K 歌房内的听众都可以听到这位主唱唱歌。房间内想与主唱连麦语聊的听众可以上麦。
+
+### 方案介绍
+
+独唱场景下存在两种角色:
+
+- 主唱:加入频道,加载并播放歌曲。KTV API 模块内部控制音乐播放器播放音乐,发布音乐到远端,将音乐播放进度同步到远端,让歌词组件进入歌词滚动状态等逻辑。
+- 听众:加入频道,加载歌曲。KTV API 模块内部控制听众订阅主唱的人声和音乐的音频合流,同步主唱的音乐播放进度,让歌词组件进入歌词滚动状态等逻辑。如果普通观众需要上麦聊天,可以更新媒体选项。
+
+![](https://web-cdn.agora.io/docs-files/1678784206362)
+
+下图展示独唱的 API 调用时序图:
+
+![](https://web-cdn.agora.io/docs-files/1678788551670)
+
+### 主唱实现
+
+#### 1. 加入频道
+
+调用 [`joinChannel`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让主唱加入频道。
+
+```Kotlin
+// 加入频道
+mRtcEngine.joinChannel(
+ ,
+ ,
+ ,
+ // 媒体选项详见第 5 步操作
+ channelMediaOption
+)
+```
+
+#### 2. 播放歌曲
+
+调用 `loadSong` 加载歌曲。歌曲加载结果会异步地通过 `onLoaded` 回调通知你。收到 `onLoaded(KTVLoadSongStateOK)` 回调状态后,调用 `playSong` 开始播放歌曲。
+
+```Kotlin
+ktvApiProtocol.loadSong(
+ "ABCDEFG", KTVSongConfiguration(KTVSongType.KTVSongTypeSolo, KTVSingRole.KTVSingRoleMainSinger, "ABCDEFG", 12345, 0)
+) { songCode, lyricUrl, singRole, singState ->
+ if (singState === KTVLoadSongState.KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ ktvApiProtocol.playSong(songCode)
+ }
+}
+```
+
+#### 3. 停止播放
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放。
+
+```Kotlin
+ktvApiProtocol.stopSong()
+```
+
+#### 4. 关闭麦克风
+
+主唱停止唱歌或希望暂时关闭麦克风时,可以调用 [`adjustRecordingSignalVolume`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_audio_process.html#api_irtcengine_adjustrecordingsignalvolume),将音频采集信号音量设置为 0。
+
+```Kotlin
+mRtcEngine.adjustRecordingSignalVolume(0)
+```
+
+#### 5. 根据角色更新媒体选项
+
+通过 [`updateChannelMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在主播加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+```Kotlin
+val channelMediaOption = ChannelMediaOptions()
+// 发布本地麦克风流
+channelMediaOption.publishMicrophoneTrack = true
+// 启用音频采集和播放
+channelMediaOption.enableAudioRecordingOrPlayout = true
+// 设置角色为主播
+channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER
+// 更新媒体选项
+mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+```
+
+### 听众实现
+
+#### 1. 加入频道
+
+调用 [`joinChannel`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让听众加入频道。
+
+```Kotlin
+// 加入频道
+mRtcEngine.joinChannel(
+ ,
+ ,
+ ,
+ // 媒体选项详见第 4 步操作
+ channelMediaOption
+)
+```
+
+#### 2. 加载歌曲
+
+调用 `loadSong` 加载歌曲。加载结果会异步地通过 `onLoaded` 回调通知你。收到 `onLoaded(KTVLoadSongStateOK)` 回调状态后,调用 `playSong`。
+
+听众加入频道后,默认订阅主唱发布的音频合流,即主唱人声和音乐混合的音频流。听众只需调用 `playSong` 进入歌曲播放状态即可。
+
+```Kotlin
+ktvApiProtocol.loadSong(
+ "ABCDEFG", KTVSongConfiguration(KTVSongType.KTVSongTypeSolo, KTVSingRole.KTVSingRoleAudience, "ABCDEFG", 12345, 0) // 听众可以不填演唱者 UID
+) { songCode, lyricUrl, singRole, singState ->
+ if (singState === KTVLoadSongState.KTVLoadSongStateOK) {
+ // 歌曲加载成功后,进入歌曲播放状态
+ ktvApiProtocol.playSong(songCode)
+ }
+}
+```
+
+#### 3. 停止播放
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放。
+
+```Kotlin
+ktvApiProtocol.stopSong()
+```
+
+#### 4. 根据角色更新媒体选项
+
+通过 [`updateChannelMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在听众加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+听众的用户角色为 AgoraClientRoleAudience,因此无法在频道内发布音频流。如果听众想上麦与主唱语聊,需要将用户角色修改为 AgoraClientRoleBroadcaster。修改角色后,SDK 默认发布该连麦听众的音频流,主唱和其他听众都能听到连麦听众的声音。
+
+```Kotlin
+// 对需要上麦聊天的听众更新媒体选项
+val channelMediaOption = ChannelMediaOptions()
+// 发布本地麦克风流
+channelMediaOption.publishMicrophoneTrack = true
+// 启用音频采集和播放
+channelMediaOption.enableAudioRecordingOrPlayout = true
+// 设置角色为主播
+channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER
+// 更新媒体选项
+mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+
+
+// 对未上麦的听众更新媒体选项
+val channelMediaOption = ChannelMediaOptions()
+// 不发布本地麦克风流
+channelMediaOption.publishMicrophoneTrack = false
+// 启用音频采集和播放
+channelMediaOption.enableAudioRecordingOrPlayout = true
+// 设置角色为用户
+channelMediaOption.clientRoleType = CLIENT_ROLE_AUDIENCE
+// 更新媒体选项
+mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+```
+
+## 合唱
+
+本节介绍如何实现合唱功能。主唱点歌开唱后,伴唱可以和主唱一起唱歌,K 歌房内的听众都可以听到合唱。房间内想与主唱或伴唱连麦语聊的听众可以上麦。
+
+### 方案介绍
+
+合唱场景下存在三种角色:
+
+- 主唱:加入频道,加载并播放歌曲,发布麦克风采集的音频流。KTV API 模块内部控制音乐播放器播放音乐,发布音乐到远端,将音乐播放进度同步到远端,让歌词组件进入歌词滚动状态等逻辑。
+- 伴唱:加入频道,加载并播放歌曲,发布麦克风采集的音频流。KTV API 模块内部控制音乐播放器播放音乐,同步主唱的音乐播放进度,让歌词组件进入歌词滚动状态等逻辑。
+- 听众:加入频道,加载歌曲。KTV API 模块内部控制听众订阅主唱的人声和音乐的音频合流,订阅伴唱的人声,同步主唱的音乐播放进度,让歌词组件进入歌词滚动状态等逻辑。如果普通观众需要上麦聊天,可以更新媒体选项。
+
+![](https://web-cdn.agora.io/docs-files/1678784685024)
+
+下图展示合唱的 API 调用时序图:
+
+![](https://web-cdn.agora.io/docs-files/1678866284780)
+
+### 主唱实现
+
+#### 1. 设置合唱私有参数
+
+实时合唱场景对低延时和音质的要求很高。开启合唱前,你需要在初始化 RtcEngine 引擎的方法里设置如下私有参数。
+
+```Kotlin
+mRtcEngine.setParameters("{\"rtc.ntp_delay_drop_threshold\":1000}");
+mRtcEngine.setParameters("{\"che.audio.agc.enable\": true}");
+mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp\": true}");
+mRtcEngine.setParameters("{\"rtc.net.maxS2LDelay\": 800}");
+```
+
+#### 2. 加入频道
+
+调用 [`joinChannel`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让主唱加入频道。
+
+```Kotlin
+// 加入频道
+mRtcEngine.joinChannel(
+ ,
+ ,
+ ,
+ // 媒体选项详见第 5 步操作
+ channelMediaOption
+)
+```
+
+#### 3. 开始合唱
+
+调用 `loadSong` 加载歌曲。歌曲加载结果会异步地通过 `onLoaded` 回调通知你。收到 `onLoaded(KTVLoadSongStateOK)` 回调状态后,调用 `playSong` 开始播放歌曲,开始合唱。
+
+```Kotlin
+ktvApiProtocol.loadSong(
+ "ABCDEFG", KTVSongConfiguration(KTVSongType.KTVSongTypeChorus, KTVSingRole.KTVSingRoleMainSinger, "ABCDEFG", 12345, 0) // 主唱可以不填伴唱 UID,传 0 即可。
+) { songCode, lyricUrl, singRole, singState ->
+ if (singState === KTVLoadSongState.KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ ktvApiProtocol.playSong(songCode)
+ }
+}
+```
+
+#### 4. 停止合唱
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放,停止合唱。
+
+```Kotlin
+ktvApiProtocol.stopSong()
+```
+
+#### 5. 根据角色更新媒体选项
+
+通过 [`updateChannelMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在主播加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+```Kotlin
+val channelMediaOption = ChannelMediaOptions()
+// 发布本地麦克风流
+channelMediaOption.publishMicrophoneTrack = true
+// 启用音频采集和播放
+channelMediaOption.enableAudioRecordingOrPlayout = true
+// 设置角色为主播
+channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER
+// 更新媒体选项
+mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+```
+
+### 伴唱实现
+
+#### 1. 设置合唱私有参数
+
+实时合唱场景对低延时和音质的要求很高。开启合唱前,你需要在初始化 RtcEngine 引擎的方法里设置如下私有参数。
+
+```Kotlin
+mRtcEngine.setParameters("{\"rtc.ntp_delay_drop_threshold\":1000}");
+mRtcEngine.setParameters("{\"che.audio.agc.enable\": true}");
+mRtcEngine.setParameters("{\"rtc.video.enable_sync_render_ntp\": true}");
+mRtcEngine.setParameters("{\"rtc.net.maxS2LDelay\": 800}");
+```
+
+#### 2. 加入频道
+
+调用 [`joinChannel`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让伴唱加入频道。
+
+```Kotlin
+// 加入频道
+mRtcEngine.joinChannel(
+ ,
+ ,
+ ,
+ // 媒体选项详见第 5 步操作
+ channelMediaOption
+)
+```
+
+#### 3. 开始合唱
+
+调用 `loadSong` 加载歌曲。歌曲加载结果会异步地通过 `onLoaded` 回调通知你。收到 `onLoaded(KTVLoadSongStateOK)` 回调状态后,调用 `playSong` 开始播放歌曲,开始合唱。
+
+```Kotlin
+ktvApiProtocol.loadSong(
+ "ABCDEFG", KTVSongConfiguration(KTVSongType.KTVSongTypeChorus, KTVSingRole.KTVSingRoleCoSinger, "ABCDEFG", 12345, 54321) // 伴唱一定要填主唱 UID
+) { songCode, lyricUrl, singRole, singState ->
+ if (singState === KTVLoadSongState.KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ ktvApiProtocol.playSong(songCode)
+ }
+}
+```
+
+#### 4. 停止合唱
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放,停止合唱。
+
+```Kotlin
+ktvApiProtocol.stopSong()
+```
+
+#### 5. 根据角色更新媒体选项
+
+通过 [`updateChannelMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在伴唱加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+```Kotlin
+val channelMediaOption = ChannelMediaOptions()
+// 发布本地麦克风流
+channelMediaOption.publishMicrophoneTrack = true
+// 启用音频采集和播放
+channelMediaOption.enableAudioRecordingOrPlayout = true
+// 设置角色为主播
+channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER
+// 更新媒体选项
+mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+```
+
+### 听众实现
+
+#### 1. 加入频道
+
+调用 [`joinChannel`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让听众加入频道。
+
+```Kotlin
+// 加入频道
+mRtcEngine.joinChannel(
+ ,
+ ,
+ ,
+ // 媒体选项详见第 4 步操作
+ channelMediaOption
+)
+```
+
+#### 2. 加载歌曲
+
+调用 `loadSong` 加载歌曲。加载结果会异步地通过 `onLoaded` 回调通知你。收到 `onLoaded(KTVLoadSongStateOK)` 回调状态后,调用 `playSong`。
+
+听众加入频道后,默认订阅主唱人声和音乐混合的音频流,默认订阅伴唱人声。听众只需调用 `playSong` 进入歌曲播放状态即可。
+
+```Kotlin
+ktvApiProtocol.loadSong(
+ "ABCDEFG", KTVSongConfiguration(KTVSongType.KTVSongTypeChorus, KTVSingRole.KTVSingRoleAudience, "ABCDEFG", 0, 0) // 听众可以不填演唱者 UID
+) { songCode, lyricUrl, singRole, singState ->
+ if (singState === KTVLoadSongState.KTVLoadSongStateOK) {
+ // 歌曲加载成功后,进入歌曲播放状态
+ ktvApiProtocol.playSong(songCode)
+ }
+}
+```
+
+#### 3. 停止播放
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放。
+
+```Kotlin
+ktvApiProtocol.stopSong()
+```
+
+#### 4. 根据角色更新媒体选项
+
+通过 [`updateChannelMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/java_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在听众加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+听众的用户角色为 `CLIENT_ROLE_AUDIENCE`,因此无法在频道内发布音频流。如果听众想上麦与主唱/伴唱语聊,需要将用户角色修改为 `CLIENT_ROLE_BROADCASTER`。修改角色后,SDK 默认发布该连麦听众的音频流,主唱、伴唱、其他听众都能听到连麦听众的声音。
+
+```Kotlin
+// 对需要上麦聊天的听众更新媒体选项
+val channelMediaOption = ChannelMediaOptions()
+// 发布本地麦克风流
+channelMediaOption.publishMicrophoneTrack = true
+// 启用音频采集和播放
+channelMediaOption.enableAudioRecordingOrPlayout = true
+// 设置角色为主播
+channelMediaOption.clientRoleType = CLIENT_ROLE_BROADCASTER
+// 更新媒体选项
+mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+
+
+// 对未上麦的听众更新媒体选项
+val channelMediaOption = ChannelMediaOptions()
+// 不发布本地麦克风流
+channelMediaOption.publishMicrophoneTrack = false
+// 启用音频采集和播放
+channelMediaOption.enableAudioRecordingOrPlayout = true
+// 设置角色为用户
+channelMediaOption.clientRoleType = CLIENT_ROLE_AUDIENCE
+// 更新媒体选项
+mRtcEngine.updateChannelMediaOptions(channelMediaOption)
+```
+
+## API 参考
+
+本文集成步骤中使用如下 API:
+- [RTC API](/cn/online-ktv/API%20Reference/java_ng/API/rtc_api_overview_ng.html)
+- [场景化 API](/cn/online-ktv/ktv_api_kotlin?platform=Android)
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/K \346\255\214\346\210\277\345\234\272\346\231\257\345\214\226 Objective-C API for iOS.md" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/K \346\255\214\346\210\277\345\234\272\346\231\257\345\214\226 Objective-C API for iOS.md"
new file mode 100644
index 00000000000..fff69a99eb2
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/K \346\255\214\346\210\277\345\234\272\346\231\257\345\214\226 Objective-C API for iOS.md"
@@ -0,0 +1,262 @@
+本文提供在线 K 歌房场景定制化 Objective-C API。你可以在 GitHub 上查看源码文件 [KTVApi.h](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-iOS/iOS/AgoraEntScenarios/Scenes/KTV/ViewController/KTV/KTVApi.h) 和 [KTVApi.m](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-iOS/iOS/AgoraEntScenarios/Scenes/KTV/ViewController/KTV/KTVApi.m)。
+
+本文适用于场景化 API v2.1.1。
+
+## 方法
+
+### initWithRtcEngine:channel:musicCenter:player:dataStreamId:delegate:
+
+```objective-c
+- (id)initWithRtcEngine:(AgoraRtcEngineKit *)engine
+ channel:(NSString*)channelName
+ musicCenter:(AgoraMusicContentCenter*)musicCenter
+ player:(nonnull id)rtcMediaPlayer
+ dataStreamId:(NSInteger)streamId
+ delegate:(id)delegate;
+```
+
+初始化 KTVApi 模块。
+
+调用该方法可以初始化 KTV API 模块内部变量和缓存数据,并注册相应的回调监听。
+
+#### 注意事项
+
+调用其他 KTV API 之前,你需要先调用本方法完成初始化。
+
+#### 参数
+
+- `engine`: [AgoraRtcEngineKit](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/rtc_interface_class.html#class_irtcengine)。
+- `channel`: 待加入的频道名。
+- `musicCenter`: 版权音乐内容中心实例。详见 [AgoraMusicContentCenter](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/rtc_interface_class.html#class_imusiccontentcenter)。
+- `player`: 音乐播放器实例。详见 [AgoraMusicPlayerProtocol](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/rtc_interface_class.html#class_imusicplayer)。
+- `dataStreamId`: 数据流(Data Stream)ID。
+- `delegate`: [KTVApiDelegate](#controllersongdidchangedtostatelocal)。
+
+#### 返回值
+
+KTVApi 实例。
+
+
+### loadSong:withConfig:withCallback:
+
+```objective-c
+- (void)loadSong:(NSInteger)songCode
+withConfig:(nonnull KTVSongConfiguration *)config
+withCallback:(void (^ _Nullable)(NSInteger songCode, NSString* lyricUrl, KTVSingRole role, KTVLoadSongState state))block
+```
+
+加载歌曲。
+
+传入歌曲编号和 K 歌配置,调用 `loadSong` 加载歌曲。加载结果会异步地通过 `block` 回调通知你。
+
+#### 参数
+
+- `songCode`: 歌曲编号。
+- `config`: K 歌配置。详见 [KTVSongConfiguration](#ktvsongconfiguration)。
+- `block`: 歌词加载状态事件,包含如下参数:
+ - `songCode`: 歌曲编号。
+ - `lyricUrl`: 歌词文件的 URL。
+ - `role`: 当前用户角色,详见 [KTVSingRole](#ktvsingrole)。
+ - `state`: 歌曲加载状态,详见 [KTVLoadSongState](#ktvloadsongstate)。
+
+
+### playSong:
+
+```objective-c
+- (void)playSong:(NSInteger)songCode
+```
+
+播放歌曲。
+
+建议在调用 `loadSong` 函数并收到 `block` 回调的 `KTVLoadSongStateOK` 状态后再调用 `playSong`。
+
+#### 参数
+
+- `songCode`: 歌曲编号。
+
+### stopSong
+
+```objective-c
+- (void)stopSong;
+```
+
+结束播放歌曲。
+
+### resumePlay
+
+```objective-c
+- (void)resumePlay;
+```
+
+恢复播放歌曲。
+
+
+### pausePlay
+
+```objective-c
+- (void)pausePlay;
+```
+
+暂停播放歌曲。
+
+### seek:
+
+```objective-c
+- (void)seek:(time: NSInteger);
+```
+
+跳转到指定时间播放歌曲。
+
+#### 参数
+
+- `time`: 跳转的时间点。单位为毫秒。取值不得超过歌曲总时长。
+
+### selectTrackMode:
+
+```objective-c
+- (void)selectTrackMode:(KTVPlayerTrackMode)mode;
+```
+
+选择播放的音轨。
+
+歌曲的音轨包含原唱和伴奏。调用该方法可以选择播放的音轨。
+
+#### 参数
+
+- `mode`: 音轨的类型。详见 [KTVPlayerTrackMode](#ktvplayertrackmode)。
+
+### setKaraokeView:
+
+```objective-c
+@property(nonatomic, weak) KaraokeView* karaokeView;
+
+- (void)setKaraokeView:(KaraokeView *)karaokeView{
+ _karaokeView = karaokeView;
+ _karaokeView.delegate = self;
+}
+```
+
+设置歌词控制视图。
+
+歌词控制视图用于显示歌词和控制歌词滚动等操作。调用该方法后,可以将歌词控制视图和 KTV 模块进行绑定,从而实现歌词的同步滚动。
+
+#### 参数
+
+- `karaokeView`: 歌词控制视图,`KaraokeView` 对象。
+
+## 回调
+
+### controller:song:didChangedToState:local:
+
+```objective-c
+@protocol KTVApiDelegate
+ - (void)controller:(KTVApi*)controller song:(NSInteger)songCode didChangedToState:(AgoraMediaPlayerState)state local:(BOOL)local;
+@end
+```
+播放器状态改变回调。
+
+#### 参数
+
+- `controller`: KTVApi 实例。
+- `songCode`: 歌曲编号。
+- `state`: 播放器的当前状态。详见 [AgoraMediaPlayerState](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/enum_mediaplayerstate.html?platform=iOS)。
+- `local`: 是否为本地事件:
+ - `YES`: 代表是本地播放器的状态改变。可用于主唱和伴唱监听本地播放器状态。
+ - `NO`: 是远端播放器的状态改变。可用于伴唱和听众知晓主唱的播放器状态,从而方便后续进行多端播放同步。
+
+举例来说,在合唱场景下,主唱、伴唱、听众收到的 `didChangedToState` 回调有如下区别:
+
+- 主唱:收到一个 `local` 为 `YES` 的回调,报告主唱播放器的状态改变。
+- 伴唱:收到一个 `local` 为 `YES` 的回调,报告伴唱播放器的状态改变;同时,还收到一个 `local` 为 `NO` 的回调,报告主唱播放器的状态改变。
+- 听众:收到一个 `local` 为 `NO` 的回调报告主唱端播放器的状态改变。
+
+
+## Enum
+
+### KTVSongType
+
+```objective-c
+typedef enum : NSUInteger {
+ KTVSongTypeUnknown = 0,
+ KTVSongTypeSolo,
+ KTVSongTypeChorus
+} KTVSongType;
+```
+K 歌场景类型:
+- `KTVSongTypeUnknown`: 未知
+- `KTVSongTypeSolo`: 独唱场景
+- `KTVSongTypeChorus`: 合唱场景
+
+### KTVSingRole
+
+```objective-c
+typedef enum : NSUInteger {
+ KTVSingRoleUnknown = 0,
+ KTVSingRoleMainSinger,
+ KTVSingRoleCoSinger,
+ KTVSingRoleAudience
+} KTVSingRole;
+```
+K 歌用户角色类型:
+- `KTVSingRoleUnknown`: 未知
+- `KTVSingRoleMainSinger`: 主唱
+- `KTVSingRoleCoSinger`: 伴唱
+- `KTVSingRoleAudience`: 观众
+
+### KTVPlayerTrackMode
+
+```objective-c
+typedef enum : NSUInteger {
+ KTVPlayerTrackOrigin = 0,
+ KTVPlayerTrackAcc = 1
+} KTVPlayerTrackMode;
+```
+
+K 歌播放音轨类型:
+- `KTVPlayerTrackOrigin`: 原唱
+- `KTVPlayerTrackAcc`: 伴奏
+
+
+### KTVLoadSongState
+
+```objective-c
+typedef enum : NSUInteger {
+ KTVLoadSongStateIdle = 0,
+ KTVLoadSongStateOK,
+ KTVLoadSongStateInProgress,
+ KTVLoadSongStateNoLyricUrl,
+ KTVLoadSongStatePreloadFail,
+} KTVLoadSongState;
+```
+
+歌曲加载的状态:
+- `KTVLoadSongStateIdle`: 空闲状态,未加载歌曲
+- `KTVLoadSongStateOK`: 加载成功
+- `KTVLoadSongStateInProgress`: 正在加载中
+- `KTVLoadSongStateNoLyricUrl`: 歌曲无法加载,缺少歌词地址
+- `KTVLoadSongStatePreloadFail`: 加载失败
+
+## Interface
+
+### KTVSongConfiguration
+
+```objective-c
+@interface KTVSongConfiguration : NSObject
+@property(nonatomic, assign)KTVSongType type;
+@property(nonatomic, assign)KTVSingRole role;
+@property(nonatomic, assign)NSInteger songCode;
+@property(nonatomic, assign)NSInteger mainSingerUid;
+@property(nonatomic, assign)NSInteger coSingerUid;
++(KTVSongConfiguration*)configWithSongCode:(NSInteger)songCode;
+@end
+```
+
+K 歌配置:
+
+- `type`: K 歌场景类型,详见 [KTVSongType](#ktvsongtype)
+- `role`: K 歌用户角色类型,详见 [KTVSingRole](#ktvsingrole)
+- `songCode`: 歌曲的编号
+- `mainSingerUid`: 主唱的 UID
+- `coSingerUid`: 伴唱的 UID
+
+UID 指用户 ID,用于标识频道内的用户,频道内的每个 UID 都必须是唯一。UID 是 32 位无符号整数,建议取值范围为 [1,232 -1]。
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\345\220\210\345\224\261.wsd" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\345\220\210\345\224\261.wsd"
new file mode 100644
index 00000000000..9bd1c232159
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\345\220\210\345\224\261.wsd"
@@ -0,0 +1,41 @@
+@startuml
+title 合唱 API 时序图
+autonumber
+skinparam monochrome true
+participant "主唱/伴唱 App" as a
+participant "声网 SDK" as b
+participant "听众 App" as c
+== 设置私有参数 ==
+a -> b: setParameters
+== 加入频道 ==
+a -> b: joinChannelByToken
+c -> b: joinChannelByToken
+== 加载歌曲歌词 ==
+a -> b: loadSong
+c -> b: loadSong
+b -->> a: KTVLoadSongStateOK
+b -->> c: KTVLoadSongStateOK
+== 开始播放歌曲 ==
+a -> b: playSong(KTVSingRoleMainSinger/KTVSingRoleCoSinger)
+b -->> a: didChangedToState(AgoraMediaPlayerStatePlaying)
+c -> b: playSong(KTVSingRoleAudience)
+b -->> c: didChangedToState(AgoraMediaPlayerStatePlaying)
+note left
+听众不播放歌曲,只是进入播放状态
+end note
+== 停止播放 ==
+a -> b: stopSong
+b -->> a: didChangedToState(AgoraMediaPlayerStateStopped)
+c -> b: stopSong
+b -->> c: didChangedToState(AgoraMediaPlayerStateStopped)
+== 关闭麦克风 ==
+a -> b: adjustRecordingSignalVolume
+== 更新媒体选项 ==
+a -> b: updateChannelMediaOptions
+c -> b: updateChannelMediaOptions
+note left
+主唱、伴唱、上麦听众发布麦克风,角色为主播
+
+普通听众不发布麦克风,角色为观众
+end note
+@enduml
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\347\202\271\346\255\214.wsd" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\347\202\271\346\255\214.wsd"
new file mode 100644
index 00000000000..5b1b2f63bbe
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\347\202\271\346\255\214.wsd"
@@ -0,0 +1,27 @@
+@startuml
+title 点歌 API 时序图
+autonumber
+skinparam monochrome true
+participant "App" as a
+participant "声网 SDK" as b
+== 初始化 KTV API 模块==
+a -> b: initWithRtcEngine
+== 获取歌曲列表(方式一:用关键词)==
+a -> b: searchMusicWithKeyWord
+b -->> a: onMusicCollectionResult
+== 获取歌曲列表(方式二:用音乐榜单)==
+a -> b: getMusicCollectionWithMusicChartId
+b -->> a: onMusicChartsResult
+== 加载歌曲 ==
+a -> b: loadSong
+== 开始播放歌曲 ==
+b -->> a: KTVLoadSongStateOK
+a -> b: playSong
+b -->> a: didChangedToState(AgoraMediaPlayerStatePlaying)
+== 控制歌曲播放 ==
+a ->b: seek/pause/resume/selectTrackMode
+b -->> a: didChangedToState(AgoraMediaPlayerStateXX)
+== 停止播放 ==
+a -> b: stopSong
+b -->> a: didChangedToState(AgoraMediaPlayerStateStopped)
+@enduml
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\347\213\254\345\224\261.wsd" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\347\213\254\345\224\261.wsd"
new file mode 100644
index 00000000000..257aab9c5c5
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\347\213\254\345\224\261.wsd"
@@ -0,0 +1,39 @@
+@startuml
+title 独唱 API 时序图
+autonumber
+skinparam monochrome true
+participant "主唱 App" as a
+participant "声网 SDK" as b
+participant "听众 App" as c
+== 加入频道 ==
+a -> b: joinChannelByToken
+c -> b: joinChannelByToken
+== 加载歌曲歌词 ==
+a -> b: loadSong
+c -> b: loadSong
+b -->> a: KTVLoadSongStateOK
+b -->> c: KTVLoadSongStateOK
+== 开始播放歌曲 ==
+a -> b: playSong(KTVSingRoleMainSinger)
+b -->> a: didChangedToState(AgoraMediaPlayerStatePlaying)
+c -> b: playSong(KTVSingRoleAudience)
+b -->> c: didChangedToState(AgoraMediaPlayerStatePlaying)
+note left
+听众不播放歌曲,只是进入播放状态
+end note
+== 停止播放 ==
+a -> b: stopSong
+b -->> a: didChangedToState(AgoraMediaPlayerStateStopped)
+c -> b: stopSong
+b -->> c: didChangedToState(AgoraMediaPlayerStateStopped)
+== 关闭麦克风 ==
+a -> b: adjustRecordingSignalVolume
+== 更新媒体选项 ==
+a -> b: updateChannelMediaOptions
+c -> b: updateChannelMediaOptions
+note left
+主唱和上麦听众发布麦克风,角色为主播
+
+普通听众不发布麦克风,角色为观众
+end note
+@enduml
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\351\233\206\346\210\220.md" "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\351\233\206\346\210\220.md"
new file mode 100644
index 00000000000..05dddde15b5
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210/iOS/\345\256\236\347\216\260\346\226\207\346\241\243/\351\233\206\346\210\220.md"
@@ -0,0 +1,630 @@
+## 概述
+
+为降低开发者的集成难度,声网为 K 歌房场景提供了场景化 API。场景化 API 封装了声网音视频 SDK 的 API,并提供了 K 歌业务常见的功能,例如,对主唱和伴唱进行 NTP 时间同步。你只需要调用一个场景化 API 即可实现通过多个音视频 SDK 的 API 完成的复杂代码逻辑,从而更轻松实现 K 歌场景。声网在 GitHub 上提供 KTV 场景化 API 的源码文件 [KTVApi.h](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-iOS/iOS/AgoraEntScenarios/Scenes/KTV/ViewController/KTV/KTVApi.h) 和 [KTVApi.m](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-iOS/iOS/AgoraEntScenarios/Scenes/KTV/ViewController/KTV/KTVApi.m)。
+
+本文介绍如何使用 KTV 场景化 API 实现点歌、独唱、合唱等基础业务功能。
+
+## 前提条件
+
+实现点歌、独唱、合唱前,请确保你已完成如下步骤:
+
+1. 参考项目配置 集成所需 SDK。
+2. 在工程文件中引入 [KTVApi.h](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-iOS/iOS/AgoraEntScenarios/Scenes/KTV/ViewController/KTV/KTVApi.h) 和 [KTVApi.m](https://github.com/AgoraIO-Usecase/agora-ent-scenarios/blob/v2.1.1-ktv-iOS/iOS/AgoraEntScenarios/Scenes/KTV/ViewController/KTV/KTVApi.m) 文件。
+
+## 点歌
+
+本节介绍如何实现点歌功能。点歌指用户通过浏览榜单或搜索关键词选定想唱的正版音乐,然后下载播放音乐。用户需要在唱歌前进行点歌。
+
+### 方案介绍
+
+下图展示点歌的 API 调用时序图:
+
+![](https://web-cdn.agora.io/docs-files/1683360046603)
+
+### 1. 初始化 KTV API 模块
+
+实例化 `rtcEngine`、`musicCenter`、`musicPlayer`、`streamId` 实例,并将它们通过 `initWithRtcEngine` 方法传入 KTV API 模块。调用 KTV API 模块的 API 前,请确保已调用 `initWithRtcEngine` 初始化 KTV API 实例。
+
+1. 调用 `sharedEngineWithAppId` 初始化 `AgoraRtcEngineKit`。
+
+ ```objective-c
+ // 初始化 AgoraRtcEngineKit
+ // self.RTCkit 是定义的 AgoraRtcEngineKit 全局变量
+ self.RTCkit = [AgoraRtcEngineKit sharedEngineWithAppId: delegate:self];
+ [self.RTCkit setAudioScenario:AgoraAudioScenarioGameStreaming];
+ [self.RTCkit setAudioProfile:AgoraAudioProfileMusicHighQuality];
+ [self.RTCkit setChannelProfile:AgoraChannelProfileLiveBroadcasting];
+ ```
+
+2. 调用 [`sharedContentCenterWithConfig`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_drm.html#api_imusiccontentcenter_initialize) 初始化 `AgoraMusicContentCenter`。示例代码中需要传入 RTM Token。你可以参考[获取 RTM Token](/cn/Agora%20Platform/get_appid_token?platform=All%20Platforms#获取-rtm-token) 了解什么是 RTM Token,如何获取测试用途的临时 RTM Token,如何从服务器生成 RTM Token。
+
+ ```objective-c
+ // 初始化 AgoraMusicContentCenter
+ AgoraMusicContentCenterConfig *contentCenterConfiguration = [[AgoraMusicContentCenterConfig alloc] init];
+ contentCenterConfiguration.rtcEngine = self.RTCkit;
+ contentCenterConfiguration.appId = "";
+ contentCenterConfiguration.mccUid = ;
+ contentCenterConfiguration.token = "";
+ // self.AgoraMcc 是定义的 AgoraMusicContentCenter 的全局变量
+ self.AgoraMcc = [AgoraMusicContentCenter sharedContentCenterWithConfig:contentCenterConfiguration];
+ // 注册音乐内容中心回调
+ [self.AgoraMcc registerEventDelegate:];
+ [self.AgoraMcc enableMainQueueDispatch:YES];
+ ```
+
+3. 调用 [`createMusicPlayerWithDelegate`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_drm.html#api_imusiccontentcenter_createmusicplayer) 创建音乐播放器。
+
+ ```objective-c
+ // 创建音乐播放器
+ self.rtcMediaPlayer = [self.AgoraMcc createMusicPlayerWithDelegate:];
+ ```
+
+4. 调用 [`createDataStream`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_stream_management.html#api_irtcengine_createdatastream2) 创建数据流
+
+ ```objective-c
+ // 创建数据流
+ AgoraDataStreamConfig *config = [AgoraDataStreamConfig new];
+ config.ordered = false;
+ config.syncWithAudio = false;
+ // ktvStreamId 是定义的可保存 Stream ID 的全局变量
+ [self.RTCkit createDataStream:&ktvStreamId
+ config:config];
+ ```
+
+ 考虑到数据流的消息通道有频率限制,为了确保 KTV 模块和其他模块不会相互影响,声网建议你在不同模块中为数据流创建的 streamId
都不同。例如,如果你在其他模块中也使用 sendStreamMessage
发送数据流,请确保两个模块中数据流的 streamId
不同。此外,每个用户在每个频道中最多只能创建 5 个数据流,请不要超出上限。
+
+5. 调用 `initWithRtcEngine` 初始化 KTV API 实例
+
+ ```objective-c
+ // 初始化 KTV API 实例
+ val ktvApiProtocol = KTVApiImpl()
+ self.ktvApi = [[KTVApi alloc] initWithRtcEngine:self.RTCkit channel: musicCenter:self.AgoraMcc player:self.rtcMediaPlayer dataStreamId:ktvApiStreamId delegate:self];
+ ```
+
+### 2. 获取歌曲列表
+
+通过关键词搜索或音乐榜单获取歌曲列表。
+
+```objective-c
+// 用关键词搜索歌曲
+- (void)loadSearchDataWithKeyWord:(NSString *)keyWord {
+ NSDictionary *dict = @{
+ @"pitchType":@(1),
+ @"needLyric": @(YES), // 用来过滤没有歌词的歌曲
+ };
+ NSString *extra = [NSString convertToJsonData:dict];
+ [self.agoraMcc searchMusicWithKeyWord:keyWord ? keyWord : @""
+ page:self.page
+ pageSize:50
+ jsonOption:extra];
+}
+```
+
+```objective-c
+// 回调中获取通过关键词搜索的歌曲
+- (void)onMusicCollectionResult:(nonnull NSString *)requestId
+ status:(AgoraMusicContentCenterStatusCode)status
+ result:(nonnull AgoraMusicCollection *)result {
+}
+```
+
+```objective-c
+// 用音乐榜单获取歌曲
+- (void)loadDataWithIndex:(NSInteger)pageType {
+ NSArray* chartIds = @[@(3), @(4), @(2), @(6)];
+ NSInteger chartId = [[chartIds objectAtIndex:MIN(pageType - 1, chartIds.count - 1)] intValue];
+ NSDictionary *dict = @{
+ @"pitchType":@(1),
+ @"needLyric": @(YES), // 用来过滤没有歌词的歌曲
+ };
+ NSString *extra = [NSString convertToJsonData:dict];
+ [self.agoraMcc getMusicCollectionWithMusicChartId:chartId
+ page:self.page
+ pageSize:20
+ jsonOption:extra];
+}
+```
+
+```objective-c
+// 回调中获取通过音乐榜单得到的歌曲
+- (void)onMusicChartsResult:(NSString *)requestId
+ status:(AgoraMusicContentCenterStatusCode)status
+ result:(NSArray *)result {
+}
+```
+
+### 3. 加载歌曲
+
+调用 `loadSong` 加载歌曲。该方法中你需要传入歌曲编号和 K 歌配置,例如当前的 K 歌场景(独唱或合唱)、用户角色、主唱伴唱的 UID。K 歌配置会决定 K 歌时歌曲的播放情况、各端用户的收发流情况等。歌曲加载结果会异步地通过 `block` 回调通知你。
+
+
+```objective-c
+// KTVSongType: 歌唱类型(合唱或独唱)
+// KTVSingRole: K 歌用户角色
+// songCode: 歌曲的编号
+// mainSingerUid: 主唱 UID
+// coSingerUid: 伴唱 UID。如果是独唱场景,不存在伴唱角色,那么传入 0 即可。
+KTVSongConfiguration *config = [[KTVSongConfiguration alloc] init];
+config.type = KTVSongTypeChorus | KTVSongTypeSolo;
+config.role = KTVSingRoleMainSinger | KTVSingRoleCoSinger | KTVSingRoleAudience;
+config.songCode = ;
+config.mainSingerUid = ;
+config.coSingerUid = ;
+
+// 加载歌曲
+[self.ktvApi loadSong:songCode withConfig:config withCallback:^(NSInteger songCode, NSString * _Nonnull lyricUrl, KTVSingRole role, KTVLoadSongState state) {
+ KTVLogInfo(@"loadSong result: %ld", state);
+ if(state == KTVLoadSongStateOK) {
+ // 歌曲加载成功的操作
+ ...
+ } else if(state == KTVLoadSongStateNoLyricUrl) {
+ // 歌曲加载失败的操作
+ ...
+ }
+}];
+```
+
+
+### 4. 播放歌曲
+
+收到 `KTVLoadSongStateOK` 回调状态后,调用 `playSong` 开始播放歌曲。
+
+```objective-c
+[self.ktvApi loadSong:songCode withConfig:config withCallback:^(NSInteger songCode, NSString * _Nonnull lyricUrl, KTVSingRole role, KTVLoadSongState state) {
+ KTVLogInfo(@"loadSong result: %ld", state);
+ if(state == KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ [self.ktvApi playSong:songCode];
+ } else if(state == KTVLoadSongStateNoLyricUrl) {
+ // 否则重试三次
+ if(weakSelf.retryCount < 2) {
+ KTVLogInfo(@"songName: %@, songNo: %@, retryCount: %lu",model.songName, model.songNo,(unsigned long)weakSelf.retryCount);
+ [weakSelf loadSongWithModel:model config:config];
+ weakSelf.retryCount++;
+ }
+ }
+}];
+```
+
+### 5. 监听并控制歌曲播放
+
+歌曲播放时,音乐播放器会通过 `didChangedToState` 回调向业务层通知歌曲播放状态改变。收到 `didChangedToState(AgoraMediaPlayerStatePlaying)` 回调后,你可以使用 `seek`、`pause`、`resume`、`selectAudioTrack` 等方法控制播放器。
+
+KTV API 模块内部会自动处理播放器同步,因此你也可以通过 didChangedToState
回调获取远端播放器的状态。
+
+```objective-c
+// 跳转到指定时间播放歌曲
+[self.ktvApi seek: time];
+```
+
+### 6. 停止歌曲播放
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放。
+
+```objective-c
+[self.ktvApi stopSong];
+```
+
+
+## 独唱
+
+本节介绍如何实现独唱功能。主唱点歌后,可以开始独唱,K 歌房内的听众都可以听到这位主唱唱歌。房间内想与主唱连麦语聊的听众可以上麦。
+
+### 方案介绍
+
+独唱场景下存在两种角色:
+
+- 主唱:加入频道,加载并播放歌曲,发布麦克风采集的音频流。KTV API 模块内部控制音乐播放器播放音乐,发布音乐到远端,将音乐播放进度同步到远端,让歌词组件进入歌词滚动状态等逻辑。
+- 听众:加入频道,加载歌曲。KTV API 模块内部控制听众订阅主唱的人声和音乐的音频合流,同步主唱的音乐播放进度,让歌词组件开始进入播放状态等逻辑。如果普通观众需要上麦聊天,可以更新媒体选项。
+
+![](https://web-cdn.agora.io/docs-files/1678784206362)
+
+下图展示独唱的 API 调用时序图:
+
+![](https://web-cdn.agora.io/docs-files/1678788677280)
+
+### 主唱实现
+
+#### 1. 加入频道
+
+调用 [`joinChannelByToken`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让主唱加入频道。
+
+```objective-c
+// 加入频道
+[self.RTCKit joinChannelByToken:
+ channelId:
+ uid:
+ // 媒体选项详见第 5 步操作
+ mediaOptions:mediaOption
+ joinSuccess:nil];
+```
+
+
+#### 2. 播放歌曲
+
+调用 `loadSong` 加载歌曲。歌曲加载结果会异步地通过 `block` 回调通知你。收到 `KTVLoadSongStateOK` 回调状态后,调用 `playSong` 开始播放歌曲。
+
+```objective-c
+KTVSongConfiguration *config = [[KTVSongConfiguration alloc] init];
+config.type = KTVSongTypeSolo;
+config.role = KTVSingRoleMainSinger;
+config.songCode = ;
+config.mainSingerUid = ;
+// 独唱场景不存在伴唱角色,那么传入 0 即可。
+config.coSingerUid = 0;
+[self.ktvApi loadSong:songCode withConfig:config withCallback:^(NSInteger songCode, NSString * _Nonnull lyricUrl, KTVSingRole role, KTVLoadSongState state) {
+ KTVLogInfo(@"loadSong result: %ld", state);
+ if(state == KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ [self.ktvApi playSong:songCode];
+ } else if(state == KTVLoadSongStateNoLyricUrl) {
+ // 歌曲加载失败,则重试三次
+ ...
+ }
+}];
+```
+
+#### 3. 停止播放
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放。
+
+```objective-c
+[self.ktvApi stopSong];
+```
+
+#### 4. 关闭麦克风
+
+主唱停止唱歌或希望暂时关闭麦克风时,可以调用 [`adjustRecordingSignalVolume`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_audio_process.html#api_irtcengine_adjustrecordingsignalvolume),将音频采集信号音量设置为 0。
+
+```objective-c
+[self.RTCKit adjustRecordingSignalVolume: 0];
+```
+
+#### 5. 根据角色更新媒体选项
+
+通过 [`updateChannelWithMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在主播加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+```objective-c
+AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+// 发布本地麦克风流
+options.publishMicrophoneTrack = YES;
+// 启用音频采集和播放
+options.enableAudioRecordingOrPlayout = YES;
+// 设置角色为主播
+options.clientRoleType = AgoraClientRoleBroadcaster;
+// 更新媒体选项
+[self.RTCKit updateChannelWithMediaOptions:options];
+```
+
+### 听众实现
+
+#### 1. 加入频道
+
+调用 [`joinChannelByToken`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让听众加入频道。
+
+```objective-c
+// 加入频道
+[self.RTCKit joinChannelByToken:
+ channelId:
+ uid:
+ // 媒体选项详见第 4 步操作
+ mediaOptions:mediaOption
+ joinSuccess:nil];
+```
+
+#### 2. 加载歌曲
+
+调用 `loadSong` 加载歌曲。加载结果会异步地通过 `block` 回调通知你。收到 `KTVLoadSongStateOK` 回调状态后,调用 `playSong`。
+
+听众加入频道后,默认订阅主唱发布的音频合流,即主唱人声和音乐混合的音频流。听众只需调用 `playSong` 进入歌曲播放状态即可。
+
+```objective-c
+KTVSongConfiguration *config = [[KTVSongConfiguration alloc] init];
+config.type = KTVSongTypeSolo;
+config.role = KTVSingRoleAudience;
+config.songCode = ;
+// 听众可以不传演唱者的 UID
+config.mainSingerUid = 0;
+config.coSingerUid = 0;
+[self.ktvApi loadSong:songCode withConfig:config withCallback:^(NSInteger songCode, NSString * _Nonnull lyricUrl, KTVSingRole role, KTVLoadSongState state) {
+ KTVLogInfo(@"loadSong result: %ld", state);
+ if(state == KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ [self.ktvApi playSong:songCode];
+ } else if(state == KTVLoadSongStateNoLyricUrl) {
+ // 歌曲加载失败,则重试三次
+ ...
+ }
+}];
+```
+
+#### 3. 停止播放
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放。
+
+```objective-c
+[self.ktvApi stopSong];
+```
+
+#### 4. 根据角色更新媒体选项
+
+通过 [`updateChannelWithMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在听众加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+听众的用户角色为 `AgoraClientRoleAudience`,因此无法在频道内发布音频流。如果听众想上麦与主唱语聊,需要将用户角色修改为 `AgoraClientRoleBroadcaster`。修改角色后,SDK 默认发布该连麦听众的音频流,主唱和其他听众都能听到连麦听众的声音。
+
+```objective-c
+// 对需要上麦聊天的听众更新媒体选项
+AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+// 发布本地麦克风流
+options.publishMicrophoneTrack = true
+// 启用音频采集和播放
+options.enableAudioRecordingOrPlayout = true
+// 设置角色为主播
+options.clientRoleType = AgoraClientRoleBroadcaster
+// 更新媒体选项
+[self.RTCKit updateChannelWithMediaOptions:options];
+
+
+// 对未上麦的听众更新媒体选项
+AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+// 不发布本地麦克风流
+options.publishMicrophoneTrack = false
+// 启用音频采集和播放
+options.enableAudioRecordingOrPlayout = true
+// 设置角色为用户
+options.clientRoleType = AgoraClientRoleAudience
+// 更新媒体选项
+[self.RTCKit updateChannelWithMediaOptions:options];
+```
+
+## 合唱
+
+本节介绍如何实现合唱功能。主唱点歌开唱后,伴唱可以和主唱一起唱歌,K 歌房内的听众都可以听到合唱。房间内想与主唱或伴唱连麦语聊的听众可以上麦。
+
+### 方案介绍
+
+合唱场景下存在三种角色:
+
+- 主唱:加入频道,加载并播放歌曲,发布麦克风采集的音频流。KTV API 模块内部控制音乐播放器播放音乐,发布音乐到远端,将音乐播放进度同步到远端,让歌词组件进入歌词滚动状态等逻辑。
+- 伴唱:加入频道,加载并播放歌曲,发布麦克风采集的音频流。KTV API 模块内部控制音乐播放器播放音乐,同步主唱的音乐播放进度,让歌词组件进入歌词滚动状态等逻辑。
+- 听众:加入频道,加载歌曲。KTV API 模块内部控制听众订阅主唱的人声和音乐的音频合流,订阅伴唱的人声,同步主唱的音乐播放进度,让歌词组件进入歌词滚动状态等逻辑。如果普通观众需要上麦聊天,可以更新媒体选项。
+
+![](https://web-cdn.agora.io/docs-files/1678784685024)
+
+下图展示合唱的 API 调用时序图:
+
+![](https://web-cdn.agora.io/docs-files/1678866367772)
+
+### 主唱实现
+
+#### 1. 设置合唱私有参数
+
+实时合唱场景对低延时和音质的要求很高。开启合唱前,你需要在初始化 `AgoraRtcEngineKit` 的方法里设置如下私有参数。
+
+```objective-c
+[self.RTCKit setParameters:@"{\"rtc.ntp_delay_drop_threshold\":1000}"];
+[self.RTCKit setParameters:@"{\"rtc.enable_nasa2\": false}"];
+[self.RTCKit setParameters:@"{\"rtc.video.enable_sync_render_ntp\": true}"];
+[self.RTCKit setParameters:@"{\"rtc.net.maxS2LDelay\": 800}"];
+```
+
+#### 2. 加入频道
+
+调用 [`joinChannelByToken`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让主唱加入频道。
+
+```objective-c
+// 加入频道
+[self.RTCKit joinChannelByToken:
+ channelId:
+ uid:
+ // 媒体选项详见第 5 步操作
+ mediaOptions:mediaOption
+ joinSuccess:nil];
+```
+
+#### 3. 开始合唱
+
+调用 `loadSong` 加载歌曲。歌曲加载结果会异步地通过 `block` 回调通知你。收到 `KTVLoadSongStateOK` 回调状态后,调用 `playSong` 开始播放歌曲,开始合唱。
+
+```objective-c
+KTVSongConfiguration *config = [[KTVSongConfiguration alloc] init];
+config.type = KTVSongTypeChorus;
+config.role = KTVSingRoleMainSinger;
+config.songCode = ;
+config.mainSingerUid = ;
+// 主唱可以不填伴唱 UID,传 0 即可。
+config.coSingerUid = 0;
+[self.ktvApi loadSong:songCode withConfig:config withCallback:^(NSInteger songCode, NSString * _Nonnull lyricUrl, KTVSingRole role, KTVLoadSongState state) {
+ KTVLogInfo(@"loadSong result: %ld", state);
+ if(state == KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ [self.ktvApi playSong:songCode];
+ } else if(state == KTVLoadSongStateNoLyricUrl) {
+ // 歌曲加载失败,则重试三次
+ ...
+ }
+}];
+```
+
+#### 4. 停止合唱
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放,停止合唱。
+
+```objective-c
+[self.ktvApi stopSong];
+```
+
+#### 5. 根据角色更新媒体选项
+
+通过 [`updateChannelWithMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在主播加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+```objective-c
+AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+// 发布本地麦克风流
+options.publishMicrophoneTrack = YES;
+// 启用音频采集和播放
+options.enableAudioRecordingOrPlayout = YES;
+// 设置角色为主播
+options.clientRoleType = AgoraClientRoleBroadcaster;
+// 更新媒体选项
+[self.RTCKit updateChannelWithMediaOptions:options];
+```
+
+### 伴唱实现
+
+#### 1. 设置合唱私有参数
+
+实时合唱场景对低延时和音质的要求很高。开启合唱前,你需要在初始化 AgoraRtcEngineKit 的方法里设置如下私有参数。
+
+```objective-c
+[self.RTCKit setParameters:@"{\"rtc.ntp_delay_drop_threshold\":1000}"];
+[self.RTCKit setParameters:@"{\"rtc.enable_nasa2\": false}"];
+[self.RTCKit setParameters:@"{\"rtc.video.enable_sync_render_ntp\": true}"];
+[self.RTCKit setParameters:@"{\"rtc.net.maxS2LDelay\": 800}"];
+```
+
+#### 2. 加入频道
+
+调用 [`joinChannelByToken`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让伴唱加入频道。
+
+```objective-c
+// 加入频道
+[self.RTCKit joinChannelByToken:
+ channelId:
+ uid:
+ // 媒体选项详见第 5 步操作
+ mediaOptions:mediaOption
+ joinSuccess:nil];
+```
+
+#### 3. 开始合唱
+
+调用 `loadSong` 加载歌曲。歌曲加载结果会异步地通过 `block` 回调通知你。收到 `KTVLoadSongStateOK` 回调状态后,调用 `playSong` 开始播放歌曲,开始合唱。
+
+```objective-c
+KTVSongConfiguration *config = [[KTVSongConfiguration alloc] init];
+config.type = KTVSongTypeChorus;
+config.role = KTVSingRoleCoSinger;
+config.songCode = ;
+// 伴唱一定要填主唱 UID
+config.mainSingerUid = ;
+config.coSingerUid = ;
+[self.ktvApi loadSong:songCode withConfig:config withCallback:^(NSInteger songCode, NSString * _Nonnull lyricUrl, KTVSingRole role, KTVLoadSongState state) {
+ KTVLogInfo(@"loadSong result: %ld", state);
+ if(state == KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ [self.ktvApi playSong:songCode];
+ } else if(state == KTVLoadSongStateNoLyricUrl) {
+ // 歌曲加载失败,则重试三次
+ ...
+ }
+}];
+```
+
+#### 4. 停止合唱
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放,停止合唱。
+
+```objective-c
+[self.ktvApi stopSong];
+```
+
+#### 5. 根据角色更新媒体选项
+
+通过 [`updateChannelWithMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在伴唱加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+```objective-c
+AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+// 发布本地麦克风流
+options.publishMicrophoneTrack = YES;
+// 启用音频采集和播放
+options.enableAudioRecordingOrPlayout = YES;
+// 设置角色为主播
+options.clientRoleType = AgoraClientRoleBroadcaster;
+// 更新媒体选项
+[self.RTCKit updateChannelWithMediaOptions:options];
+```
+
+### 听众实现
+
+#### 1. 加入频道
+
+调用 [`joinChannelByToken`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_joinchannel2) 让听众加入频道。
+
+```objective-c
+// 加入频道
+[self.RTCKit joinChannelByToken:
+ channelId:
+ uid:
+ // 媒体选项详见第 4 步操作
+ mediaOptions:mediaOption
+ joinSuccess:nil];
+```
+
+#### 2. 加载歌曲
+
+调用 `loadSong` 加载歌曲。加载结果会异步地通过 `block` 回调通知你。收到 `KTVLoadSongStateOK` 回调状态后,调用 `playSong`。
+
+听众加入频道后,默认订阅主唱人声和音乐混合的音频流,默认订阅伴唱人声。听众只需调用 `playSong` 进入歌曲播放状态即可。
+
+```objective-c
+KTVSongConfiguration *config = [[KTVSongConfiguration alloc] init];
+config.type = KTVSongTypeChorus;
+config.role = KTVSingRoleAudience;
+config.songCode = ;
+// 听众可以不传演唱者的 UID
+config.mainSingerUid = 0;
+config.coSingerUid = 0;
+[self.ktvApi loadSong:songCode withConfig:config withCallback:^(NSInteger songCode, NSString * _Nonnull lyricUrl, KTVSingRole role, KTVLoadSongState state) {
+ KTVLogInfo(@"loadSong result: %ld", state);
+ if(state == KTVLoadSongStateOK) {
+ // 歌曲加载成功,则播放歌曲
+ [self.ktvApi playSong:songCode];
+ }
+}];
+```
+
+#### 3. 停止播放
+
+当歌曲播放完成或用户中途结束播放时,你需要主动调用 `stopSong` 停止播放。
+
+```objective-c
+[self.ktvApi stopSong];
+```
+
+#### 4. 根据角色更新媒体选项
+
+通过 [`updateChannelWithMediaOptions`](https://docs.agora.io/cn/online-ktv/API%20Reference/ios_ng/API/toc_core_method.html#api_irtcengine_updatechannelmediaoptions) 方法在听众加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。
+
+听众的用户角色为 `AgoraClientRoleAudience`,因此无法在频道内发布音频流。如果听众想上麦与主唱/伴唱语聊,需要将用户角色修改为 `AgoraClientRoleBroadcaster`。修改角色后,SDK 默认发布该连麦听众的音频流,主唱、伴唱、其他听众都能听到连麦听众的声音。
+
+```objective-c
+// 对需要上麦聊天的听众更新媒体选项
+AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+// 发布本地麦克风流
+options.publishMicrophoneTrack = true
+// 启用音频采集和播放
+options.enableAudioRecordingOrPlayout = true
+// 设置角色为主播
+options.clientRoleType = AgoraClientRoleBroadcaster
+// 更新媒体选项
+[self.RTCKit updateChannelWithMediaOptions:options];
+
+
+// 对未上麦的听众更新媒体选项
+AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+// 不发布本地麦克风流
+options.publishMicrophoneTrack = false
+// 启用音频采集和播放
+options.enableAudioRecordingOrPlayout = true
+// 设置角色为用户
+options.clientRoleType = AgoraClientRoleAudience
+// 更新媒体选项
+[self.RTCKit updateChannelWithMediaOptions:options];
+```
+
+## API 参考
+
+本文集成步骤中使用如下 API:
+- [RTC API](/cn/online-ktv/API%20Reference/ios_ng/API/rtc_api_overview_ng.html)
+- [场景化 API](/cn/online-ktv/ktv_api_oc?platform=iOS)
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/img/\345\220\210\345\224\261.drawio.svg" "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/img/\345\220\210\345\224\261.drawio.svg"
new file mode 100644
index 00000000000..8f881ad5c7a
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/img/\345\220\210\345\224\261.drawio.svg"
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+ 主唱
+
+
+
+
+
+
+
+
+
+
+ 播放器
+
+
+
+
+
+
+
+
+
+
+ 声网 SD-RTN
+
+
+
+
+
+
+
+
+
+
+ 伴唱
+
+
+
+
+
+
+
+
+
+
+ 播放器
+
+
+
+
+
+
+
+
+
+
+ 连麦听众
+
+
+
+
+
+
+
+
+
+
+ 听众
+
+
+
+
+
+
+
+
+
+
+ Text is not SVG - cannot display
+
+
+
+
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/img/\347\202\271\346\255\214.drawio.svg" "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/img/\347\202\271\346\255\214.drawio.svg"
new file mode 100644
index 00000000000..e69de29bb2d
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/img/\347\213\254\345\224\261.drawio.svg" "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/img/\347\213\254\345\224\261.drawio.svg"
new file mode 100644
index 00000000000..852233e46e1
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/img/\347\213\254\345\224\261.drawio.svg"
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Text is not SVG - cannot display
+
+
+
+
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\345\220\210\345\224\261.md" "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\345\220\210\345\224\261.md"
new file mode 100644
index 00000000000..ee123758d79
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\345\220\210\345\224\261.md"
@@ -0,0 +1,424 @@
+## 简介
+
+本文介绍如何实现合唱功能。主唱点歌开唱后,伴唱可以和主唱一起唱歌,K 歌房内的听众都可以听到合唱。房间内想与主唱或伴唱连麦语聊的听众可以上麦。
+
+### 技术架构
+
+
+
+相比独唱,合唱场景中增加了一位或多位伴唱。主唱在发布音乐和人声的音频合流之外,还需要再发布一路人声的音频流,以供伴唱接收,方便伴唱跟随主唱人声进行合唱。
+
+// TODO
+
+## 实现方法(主唱)
+
+### 1. 设置私有参数
+
+实时合唱场景对低延时和音质的要求很高。开启合唱前,你需要在初始化引擎的方法里设置私有参数,具体设置请参考如下示例代码。
+
+```objective-c
+- (void)initializeEngine {
+ ....
+ // 设置声网私有参数,方便主唱通过 NTP 时间与伴唱进行实时进度同步
+ [engine setParameters:@"{\"rtc.ntp_delay_drop_threshold\":1000}"];
+ [engine setParameters:@"{\"che.audio.agc.enable\": true}"];
+ [engine setParameters:@"{\"rtc.video.enable_sync_render_ntp\": true}"];
+ [engine setParameters:@"{\"rtc.net.maxS2LDelay\": 800}"];
+}
+```
+
+### 2. 开始合唱
+
+本节展示主唱端开始合唱的代码逻辑:
+
+- 发布播放器和人声的音频混流,让 K 歌房内听众听到。
+- 在另一个频道(即示例代码中的 2nd 频道)中发布自定义音频,并将人声作为自定义音源,以让 2nd 频道内的伴唱能跟唱。
+- 为了更好的合唱音频效果,调节音频参数:
+ - 合唱场景使用 AgoraAudioScenarioChorus 音频属性,退出合唱时调整为 AgoraAudioScenarioGameStreaming 音频属性。
+ - 按需调节合唱场景下主唱、伴唱、听众端的本地播放音量。
+
+```objective-c
+- (void)playSong:(NSInteger)songCode
+{
+ KTVSingRole role = self.config.role;
+ KTVSongType type = self.config.type;
+ if(type == KTVSongTypeSolo) {
+ ....
+ } else {
+ if(role == KTVSingRoleMainSinger) {
+ [self.rtcMediaPlayer openMediaWithSongCode:songCode startPos:0];
+ AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+ options.autoSubscribeAudio = YES;
+ options.autoSubscribeVideo = YES;
+ options.publishMediaPlayerId = [self.rtcMediaPlayer getMediaPlayerId];
+ // 发布播放器音乐
+ options.publishMediaPlayerAudioTrack = YES;
+ // 发布主唱人声
+ options.publishMicrophoneTrack = YES;
+ // 合唱场景下,主唱端开启本地音频录制和采集
+ options.enableAudioRecordingOrPlayout = YES;
+ [self.engine updateChannelWithMediaOptions:options];
+ [self joinChorus2ndChannel];
+
+ // 调整播放器在本地所听见的音量
+ [self.rtcMediaPlayer adjustPlayoutVolume:50];
+ // 调节播放器在远端所听见的音量
+ [self.rtcMediaPlayer adjustPublishSignalVolume:50];
+ } else if(role == KTVSingRoleCoSinger) {
+ ....
+ } else {
+ ....
+ }
+ }
+}
+
+// 加入 2nd 频道
+- (void)joinChorus2ndChannel
+{
+ if(self.subChorusConnection) {
+ KTVLogWarn(@"joinChorus2ndChannel fail! rejoin!");
+ return;
+ }
+
+ KTVSingRole role = self.config.role;
+ AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+ // 主唱不订阅 2nd 频道的音频流
+ // 伴唱订阅 2nd 频道的音频流
+ options.autoSubscribeAudio = role == KTVSingRoleMainSinger ? NO : YES;
+ options.autoSubscribeVideo = NO;
+ options.publishMicrophoneTrack = NO;
+ // 主唱不开启 2nd 频道的本地录音和播放
+ // 伴唱开启 2nd 频道的本地录音和播放
+ options.enableAudioRecordingOrPlayout = role == KTVSingRoleMainSinger ? NO : YES;
+ options.clientRoleType = AgoraClientRoleBroadcaster;
+ // 主唱在 2nd 频道里发布自定义音频流
+ // 伴唱在 2nd 频道里不发布自定义音频流
+ options.publishDirectCustomAudioTrack = role == KTVSingRoleMainSinger ? YES : NO;;
+
+ AgoraRtcConnection* connection = [AgoraRtcConnection new];
+ connection.channelId = [NSString stringWithFormat:@"%@_ex", self.channelName];
+ connection.localUid = [VLLoginModel mediaPlayerUidWithUid:VLUserCenter.user.id];
+ self.subChorusConnection = connection;
+
+ VL(weakSelf);
+ // 设置自定义音频源
+ [self.engine setDirectExternalAudioSource:YES];
+ [self.engine setAudioFrameDelegate:self];
+ // 设置音频属性为合唱
+ [self.engine setAudioScenario:AgoraAudioScenarioChorus];
+ // 加入 2nd 频道
+ [self.engine joinChannelExByToken:VLUserCenter.user.agoraPlayerRTCToken connection:connection delegate:self mediaOptions:options joinSuccess:^(NSString * _Nonnull channel, NSUInteger uid, NSInteger elapsed) {
+ // 对合唱场景下的主唱端,设置 pushDirectAudioEnable 为 YES
+ if(weakSelf.config.type == KTVSongTypeChorus &&
+ weakSelf.config.role == KTVSingRoleMainSinger) {
+ weakSelf.pushDirectAudioEnable = YES;
+ }
+
+ [weakSelf updateRemotePlayBackVolumeIfNeed];
+ }];
+}
+
+// 根据音乐播放情况,设置主唱、伴唱、听众的本地播放音量
+- (void)updateRemotePlayBackVolumeIfNeed {
+ // 对合唱场景下的听众端,本地播放音乐音量设为 100
+ if (self.config.type != KTVSongTypeChorus || self.config.role == KTVSingRoleAudience) {
+ [self.engine adjustPlaybackSignalVolume:100];
+ return;
+ }
+
+ // 如果音乐播放器状态为 AgoraMediaPlayerStatePlaying,则将合唱场景下的主唱或伴唱本地播放音乐音量分别设为与远端伴唱或主唱的音量一致
+ int volume = self.playerState == AgoraMediaPlayerStatePlaying ? self.chorusRemoteUserVolume : 100;
+ if (self.config.role == KTVSingRoleMainSinger) {
+ [self.engine adjustPlaybackSignalVolume:volume];
+ } else if (self.config.role == KTVSingRoleCoSinger) {
+ [self.engine adjustPlaybackSignalVolume:volume];
+ }
+}
+
+#pragma mark - AgoraAudioFrameDelegate
+- (BOOL)onRecordAudioFrame:(AgoraAudioFrame *)frame channelId:(NSString *)channelId
+{
+ // 如果是合唱场景下的主唱端,发送自定义音频数据给 SDK
+ if(self.pushDirectAudioEnable) {
+ // 警告:你必须在成功加入 2nd 频道后调用 pushDirectAudioFrameRawData,否则 SDK 可能卡死
+ [self.engine pushDirectAudioFrameRawData:frame.buffer samples:frame.channels*frame.samplesPerChannel sampleRate:frame.samplesPerSec channels:frame.channels];
+ }
+ return true;
+}
+
+#pragma RTC delegate for chorus channel2
+- (void)rtcEngine:(AgoraRtcEngineKit *)engine didLeaveChannelWithStats:(AgoraChannelStats *)stats
+{
+ [engine setAudioScenario:AgoraAudioScenarioGameStreaming];
+}
+```
+
+### 3. 同步播放进度
+
+本节展示主唱通过 sendStreamMessageWithDict 方法同步音乐播放器的进度、状态、NPT 时间等字典信息。
+
+**Note**:audioPlayoutDelay 是音频播放延迟,即从音频数据发送至接收端,到数据在接收端开始播放所需的时间。因为不同机型设备上的 audioPlayoutDelay 不同,主唱端和听众端可能使用不同的机型设备,所以在发布主唱的音乐播放进度时,声网建议在主唱端将进度减去 audioPlayoutDelay,在接收端计算时再加上接收端的 audioPlayoutDelay。
+
+```objective-c
+// 获取主唱的音乐总时长
+- (NSTimeInterval)getTotalTime {
+ if (self.config.role == KTVSingRoleMainSinger) {
+ NSTimeInterval time = self.playerDuration;
+ return time;
+ }
+ ....
+}
+
+// 获取主唱和伴唱的音乐播放进度
+- (NSTimeInterval)getPlayerCurrentTime {
+ if (self.config.role == KTVSingRoleMainSinger || self.config.role == KTVSingRoleCoSinger) {
+ NSTimeInterval time = uptime() - self.localPlayerPosition;
+ return time;
+ }
+
+ ....
+}
+
+- (NSInteger)playerDuration {
+ if (_playerDuration == 0) {
+ _playerDuration = [_rtcMediaPlayer getDuration];
+ }
+
+ return _playerDuration;
+}
+
+- (void)AgoraRtcMediaPlayer:(id)playerKit didChangedToPosition:(NSInteger)position
+{
+ self.localPlayerPosition = uptime() - position;
+ // 如果当前角色是合唱场景下的主播,且播放进度大于音频播放延迟,则通过发送数据流来同步播放进度。
+ // audioPlayoutDelay 是音频播放延迟,即从音频数据发送至接收端,到数据在接收端开始播放所需的时间。
+ if (self.config.role == KTVSingRoleMainSinger && position > self.audioPlayoutDelay) {
+ NSDictionary *dict = @{
+ @"cmd":@"setLrcTime",
+ @"duration":@(self.playerDuration),
+ @"time":@(position - self.audioPlayoutDelay),
+ @"ntp":@([self getNtpTimeInMs]),
+ @"playerState":@(self.playerState)
+ };
+ [self sendStreamMessageWithDict:dict success:nil];
+ }
+
+ ....
+}
+```
+
+### 4.(可选)退出合唱
+
+本节展示主唱退出 2nd 频道并关闭自定义音频。
+
+```objective-c
+- (void)leaveChorus2ndChannel
+{
+ if(self.subChorusConnection == nil) {
+ KTVLogWarn(@"leaveChorus2ndChannel fail connection = nil");
+ return;
+ }
+
+ [self.engine setDirectExternalAudioSource:NO];
+ [self.engine setAudioFrameDelegate:nil];
+
+ KTVSingRole role = self.config.role;
+ if(role == KTVSingRoleMainSinger) {
+ AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+ options.publishDirectCustomAudioTrack = NO;
+ [self.engine updateChannelExWithMediaOptions:options connection:self.subChorusConnection];
+ [self.engine leaveChannelEx:self.subChorusConnection leaveChannelBlock:nil];
+ } else if(role == KTVSingRoleCoSinger) {
+ ....
+ }
+}
+```
+
+## 实现方法(伴唱)
+
+### 1. 加入合唱
+
+本节展示伴唱端加入合唱的代码逻辑:
+
+- 发布人声的音频流,让 K 歌房内听众听到。
+- 在另一个频道(即示例代码中的 2nd 频道)中订阅自定义音频流,即订阅主唱人声的音频流。伴唱可以参考主唱人声开始跟唱。
+- 为了更好的合唱音频效果,调节音频参数:
+ - 合唱场景使用 AgoraAudioScenarioChorus 音频属性,退出合唱时调整为 AgoraAudioScenarioGameStreaming 音频属性。
+ - 按需调节合唱场景下主唱、伴唱、听众端的本地播放音量。
+
+```objective-c
+- (void)playSong:(NSInteger)songCode
+{
+ KTVSingRole role = self.config.role;
+ KTVSongType type = self.config.type;
+ if(type == KTVSongTypeSolo) {
+ ....
+ } else {
+ if(role == KTVSingRoleMainSinger) {
+ ....
+ } else if(role == KTVSingRoleCoSinger) {
+ [self.rtcMediaPlayer openMediaWithSongCode:songCode startPos:0];
+ AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+ options.autoSubscribeAudio = YES;
+ options.autoSubscribeVideo = YES;
+ // 合唱场景下,伴唱不发布播放器音乐
+ options.publishMediaPlayerAudioTrack = NO;
+ [self.engine updateChannelWithMediaOptions:options];
+ [self joinChorus2ndChannel];
+
+ [self.rtcMediaPlayer adjustPlayoutVolume:50];
+ [self.rtcMediaPlayer adjustPublishSignalVolume:50];
+ } else {
+ ....
+ }
+ }
+}
+
+- (void)joinChorus2ndChannel
+{
+ if(self.subChorusConnection) {
+ KTVLogWarn(@"joinChorus2ndChannel fail! rejoin!");
+ return;
+ }
+
+ KTVSingRole role = self.config.role;
+ AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+ // 主唱不订阅 2nd 频道的音频流
+ // 伴唱订阅 2nd 频道的音频流
+ options.autoSubscribeAudio = role == KTVSingRoleMainSinger ? NO : YES;
+ options.autoSubscribeVideo = NO;
+ options.publishMicrophoneTrack = NO;
+ // 主唱不开启 2nd 频道的本地录音和播放
+ // 伴唱开启 2nd 频道的本地录音和播放
+ options.enableAudioRecordingOrPlayout = role == KTVSingRoleMainSinger ? NO : YES;
+ options.clientRoleType = AgoraClientRoleBroadcaster;
+ // 主唱在 2nd 频道里发布自定义音频流
+ // 伴唱在 2nd 频道里不发布自定义音频流
+ options.publishDirectCustomAudioTrack = role == KTVSingRoleMainSinger ? YES : NO;
+
+ AgoraRtcConnection* connection = [AgoraRtcConnection new];
+ connection.channelId = [NSString stringWithFormat:@"%@_ex", self.channelName];
+ connection.localUid = [VLLoginModel mediaPlayerUidWithUid:VLUserCenter.user.id];
+ self.subChorusConnection = connection;
+
+ VL(weakSelf);
+ // 设置自定义音频源
+ [self.engine setDirectExternalAudioSource:YES];
+ [self.engine setAudioFrameDelegate:self];
+ // 设置音频属性为合唱
+ [self.engine setAudioScenario:AgoraAudioScenarioChorus];
+ [self.engine joinChannelExByToken:VLUserCenter.user.agoraPlayerRTCToken connection:connection delegate:self mediaOptions:options joinSuccess:^(NSString * _Nonnull channel, NSUInteger uid, NSInteger elapsed) {
+ ....
+ }];
+}
+
+
+#pragma RTC delegate for chorus channel2
+- (void)rtcEngine:(AgoraRtcEngineKit *)engine didLeaveChannelWithStats:(AgoraChannelStats *)stats
+{
+ [self.engine setAudioScenario:AgoraAudioScenarioGameStreaming];
+}
+```
+
+### 2. 本地播放音乐并记录关键时间
+
+伴唱唱歌时需要在本地播放音乐,避免将音乐发布到频道内。播放音乐时,通过 localPlayerPosition 记录伴唱音乐播放器的开始播放的时间。通过 localAudioStats 获取音频播放延迟,以备后续校准播放进度时使用。
+
+```objective-c
+- (void)AgoraRtcMediaPlayer:(id)playerKit didChangedToPosition:(NSInteger)position
+{
+ // localPlayerPosition 是系统时间减去播放进度,即播放器开始播放的时间
+ self.localPlayerPosition = uptime() - position;
+
+ ....
+}
+
+// 获取伴唱的音乐播放器的音频播放延迟,即从音频数据发送至接收端,到数据在接收端开始播放所需的时间。
+- (void)rtcEngine:(AgoraRtcEngineKit *)engine localAudioStats:(AgoraRtcLocalAudioStats *)stats {
+ self.audioPlayoutDelay = stats.audioPlayoutDelay;
+}
+```
+
+### 3. 校准播放进度
+
+通过 receiveStreamMessageFromUid 回调让伴唱接收主唱的数据流。在该回调中,从接收到的数据解析出主唱的音乐播放进度、音乐总时长、NTP 时间、播放状态。
+
+当计算出伴唱的预期播放进度于主唱的实际播放进度差距大于 40 ms 时,通过 seekToPosition 方法将伴唱的播放进度朝着主唱的播放进度校准。
+
+**Note**:audioPlayoutDelay 是音频播放延迟,即从音频数据发送至接收端,到数据在接收端开始播放所需的时间。因为不同机型设备上的 audioPlayoutDelay 不同,主唱端和听众端可能使用不同的机型设备,所以在发布主唱的音乐播放进度时,声网建议在主唱端将进度减去 audioPlayoutDelay,在接收端计算时再加上接收端的 audioPlayoutDelay。
+
+```objective-c
+- (void)rtcEngine:(AgoraRtcEngineKit *)engine receiveStreamMessageFromUid:(NSUInteger)uid streamId:(NSInteger)streamId data:(NSData *)data
+{
+ // 将数据转换为字典
+ NSDictionary *dict = [VLGlobalHelper dictionaryForJsonData:data];
+ // 如果 cmd 是 playerPosition
+ if ([dict[@"cmd"] isEqualToString:@"playerPosition"]) {
+ // 从字典中获取主唱的播放进度、总时长、NTP 时间、播放状态
+ NSInteger position = [dict[@"time"] integerValue];
+ NSInteger duration = [dict[@"duration"] integerValue];
+ NSInteger remoteNtp = [dict[@"ntp"] integerValue];
+ AgoraMediaPlayerState state = [dict[@"playerState"] integerValue];
+ // 如果当前播放和 cmd 中不同,更新伴唱的播放状态
+ if (self.playerState != state) {
+ self.playerState = state;
+ [self updateCosingerPlayerStatusIfNeed];
+ [self.delegate controller:self song:self.config.songCode didChangedToState:state local:NO];
+ }
+
+ self.remotePlayerPosition = position;
+ self.remotePlayerDuration = duration;
+ // 对伴唱,如果音乐播放器正在播放,那么当伴唱的播放进度与主唱的播放进度差距大于 40 ms 时,伴唱音乐播放器通过 seekToPosition 方法调整播放进度。
+ if(self.config.type == KTVSongTypeChorus && self.config.role == KTVSingRoleCoSinger) {
+ if([self.rtcMediaPlayer getPlayerState] == AgoraMediaPlayerStatePlaying) {
+ NSInteger localNtpTime = [self getNtpTimeInMs];
+ NSInteger localPosition = uptime() - self.localPlayerPosition;
+ NSInteger expectPosition = position + localNtpTime - remoteNtp + self.audioPlayoutDelay;
+ NSInteger threshold = expectPosition - localPosition;
+ if(labs(threshold) > 40) {
+ [self.rtcMediaPlayer seekToPosition:expectPosition];
+ }
+ }
+ }
+ }
+}
+```
+
+### 4.(可选)退出合唱
+
+本节展示伴唱退出合唱的代码逻辑。伴唱需要结束自定义音频,调用 leaveChannelEx 离开 2nd 频道,并恢复订阅主唱的音乐和人声的音频混流,最后按需调节播放器在本地和远端所听见的音量。
+
+```objective-c
+- (void)leaveChorus2ndChannel
+{
+ if(self.subChorusConnection == nil) {
+ return;
+ }
+
+ [self.engine setDirectExternalAudioSource:NO];
+ [self.engine setAudioFrameDelegate:nil];
+
+ KTVSingRole role = self.config.role;
+ if(role == KTVSingRoleMainSinger) {
+ ....
+ } else if(role == KTVSingRoleCoSinger) {
+ [self.engine leaveChannelEx:self.subChorusConnection leaveChannelBlock:nil];
+ [self.engine muteRemoteAudioStream:self.config.mainSingerUid mute:NO];
+ }
+
+ [self adjustPlayoutVolume:self.playoutVolume];
+ [self adjustPublishSignalVolume:self.publishSignalVolume];
+ self.pushDirectAudioEnable = NO;
+ self.subChorusConnection = nil;
+}
+```
+
+## 实现方法(听众)
+
+不管是合唱还是独唱场景,听众的代码逻辑都是一样的,主要分为两部分:
+
+- 订阅频道内的音频流。
+- 歌词同步。
+
+示例代码请参考独唱场景下[实现方法(听众)](#audience)。
+
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\347\202\271\346\255\214.md" "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\347\202\271\346\255\214.md"
new file mode 100644
index 00000000000..5664ad0e2a0
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\347\202\271\346\255\214.md"
@@ -0,0 +1,270 @@
+## 简介
+
+本文介绍如何实现点歌功能。用户需要在唱歌前进行点歌。点歌指用户通过浏览榜单或搜索关键词选定想唱的正版音乐,然后下载播放音乐。
+
+## 示例项目
+
+声网在 GitHub 上提供开源的 //TODO 示例项目供你参考。
+
+## 准备开发环境
+
+### 前提条件
+
+- Xcode 12.0 或以上版本。
+- iOS 11.0 或以上版本的真机设备。
+- 有效的[声网开发者账号](/cn/Agora%20Platform/sign_in_and_sign_up?platform=iOS)。
+- 有效的声网项目,并获取项目的 App ID 和 RTM Token。详情请参考[开始使用 Agora 平台](/cn/Agora%20Platform/get_appid_token?platform=All%20Platforms)。
+- 联系 sales@agora.io 申请开通声网正版音乐内容中心服务。
+
+模拟器可能无法运行在线 K 歌房的全部功能,因此声网推荐你在真机上运行 K 歌房项目。 如果你的网络环境部署了防火墙,请参考应用企业防火墙限制 以正常使用声网服务。
+
+### 创建项目
+
+参考以下步骤创建一个 iOS 项目。
+
+
+ 创建 iOS 项目
+
+1. 打开 **Xcode** 并点击 **Create a new Xcode project**。
+2. 选择项目类型为 **Single View App**,并点击 **Next**。
+3. 输入项目信息,如项目名称、开发团队信息、组织名称和语言,并点击 **Next**。
+
+ **Note**:如果你没有添加过开发团队信息,会看到 **Add account…** 按钮。点击该按钮并按照屏幕提示登入 Apple ID,完成后即可选择你的账户作为开发团队。
+4. 选择项目存储路径,并点击 **Create**。
+5. 将你的 iOS 设备连接至电脑。
+6. 进入 **TARGETS > Project Name > Signing & Capabilities** 菜单,选择 **Automatically manage signing**,并在弹出菜单中点击 **Enable Automatic**。
+
+
+### 集成 SDK
+
+在线 K 歌房需要集成声网的两个 SDK:
+
+- 音频 SDK:主要提供在线音频互动、音频管理(例如,调节本地麦克风采集的歌声音量、混合用户歌声和背景音乐声、获取本地歌声的音高值)、NTP 时间戳同步功能。
+- 歌词组件:主要提供歌词文本随音乐播放而同步展示、标准音高线随音乐播放而同步展示、用户音准评分功能。
+
+下节介绍如何使用 Cocoapods 集成 SDK:
+
+1. 开始前确保你已安装 Cocoapods。参考 [Getting Started with CocoaPods](https://guides.cocoapods.org/using/getting-started.html#getting-started) 安装说明。
+
+2. 在终端里进入项目根目录,并运行 `pod init` 命令。项目文件夹下会生成一个 `Podfile` 文本文件。
+
+3. 打开 `Podfile` 文件,修改文件为如下内容。注意将 `Your App` 替换为你的 Target 名称。
+
+ ```ruby
+ target 'Your App' do
+ pod 'AgoraAudio_iOS', '~> 4.0.1'
+ pod 'AgoraLyricsScore', '~> 1.0.8'
+ end
+ ```
+
+ 示例代码中的版本号仅供参考。音频 SDK 最新版本号详见[发版说明](/cn/voice-call-4.x/release_ios_audio_ng?platform=iOS)。歌词组件最新版本号详见 [GitHub Tags](https://github.com/AgoraIO-Community/LrcView-iOS/tags)。
+
+
+4. 在终端内运行 `pod install` 命令安装 SDK。成功安装后,Terminal 中会显示 `Pod installation complete!`,此时项目文件夹下会生成一个 `xcworkspace` 文件。
+
+5. 打开新生成的 `xcworkspace` 文件。
+
+## 实现方法
+
+![](https://web-cdn.agora.io/docs-files/1673259817555)
+
+### 1. 初始化和创建
+
+调用 `sharedContentCenterWithConfig` 初始化声网正版音乐内容中心(`AgoraMusicContentCenter`)。音乐内容中心配置(`AgoraMusicContentCenterConfiguration`)中需要传入如下值:
+
+- `rtcEngine`: `AgoraRtcEngineKit` 实例
+- `appId`: 项目 App ID。请确保你已经为项目开通声网正版音乐内容中心服务。详见[前提条件](#前提条件)。//TODO add link
+- `token`: 音乐内容中心里用到的 Access Token。生成方式详见[使用 Access Token2 鉴权](/cn/Real-time-Messaging/token2_server_rtm?platform=All%20Platforms)。生成 Access Token 时需要传入的 `userId` 即为下面的 `mccUid`。//TODO RTM or RTC Token
+- `mccUid`: 音乐内容中心里用到的用户 ID。可以与加入 RTC 频道时使用的 UID 一致。不能为 0。
+
+
+```objective-c
+AgoraMusicContentCenterConfig *contentCenterConfiguration = [[AgoraMusicContentCenterConfig alloc] init];
+contentCenterConfiguration.rtcEngine = self.RTCkit;
+contentCenterConfiguration.appId = [[AppContext shared] appId];
+contentCenterConfiguration.mccUid = [VLUserCenter.user.id integerValue];
+contentCenterConfiguration.token = VLUserCenter.user.agoraRTMToken;
+self.AgoraMcc = [AgoraMusicContentCenter sharedContentCenterWithConfig:contentCenterConfiguration];
+```
+
+调用 `createMusicPlayerWithDelegate` 创建声网正版音乐内容中心专用的音乐播放器。向 `delegate` 传入的对象可以用来监听播放器相关的事件。
+
+```objective-c
+self.rtcMediaPlayer = [self.AgoraMcc createMusicPlayerWithDelegate:[AppContext shared]];
+```
+
+### 2. 获取音乐资源
+
+调用 `searchMusicWithKeyWord` 或 `getMusicCollectionWithMusicChartId` 获取音乐资源。方法调用成功后,SDK 会通过 `onMusicCollectionResult` 向你报告音乐资源的详细信息。你可以基于实际业务逻辑,将获取的音乐资源结果进行分页展示,展示的内容可以为专辑封面、歌手、歌曲时长、歌词类型等,详见 [`AgoraMusic`](./API%20Reference/ios_ng/API/class_music.html)。
+
+- 方式一:通过 `searchMusicWithKeyWord` 方法,传入歌曲或歌手名称,搜索到音乐资源。
+
+ ```objective-c
+ - (void)searchWithKeyWord:(NSString*)keyWord{
+ self.requestId = [self.AgoraMcc searchMusicWithKeyWord:keyWord
+ page:self.page
+ pageSize:5
+ jsonOption:nil];
+ }
+
+ #pragma mark - AgoraMusicContentCenterEventDelegate
+ - (void)onMusicCollectionResult:(NSString *)requestId
+ status:(AgoraMusicContentCenterStatusCode)status
+ result:(AgoraMusicCollection *)result {
+ if (![self.requestId isEqualToString:requestId]) {
+ return;
+ }
+
+ // 此处添加你的业务逻辑
+ ...
+ }
+ ```
+
+- 方式二:通过 `getMusicCollectionWithMusicChartId` 方法,传入音乐榜单的 ID,获取该榜单的音乐资源列表。//TODO 是否需要补充获取音乐榜单 ID 的步骤?
+
+ ```objective-c
+ - (void)searchWithRankingChartId:(NSString*)chartId atPage:(NSInteger)page{
+ self.requestId = [self.AgoraMcc getMusicCollectionWithMusicChartId:chartId
+ page:page
+ pageSize:20
+ jsonOption:nil];
+ }
+
+ #pragma mark - AgoraMusicContentCenterEventDelegate
+ - (void)onMusicCollectionResult:(NSString *)requestId
+ status:(AgoraMusicContentCenterStatusCode)status
+ result:(AgoraMusicCollection *)result {
+ if (![self.requestId isEqualToString:requestId]) {
+ return;
+ }
+
+ // 此处添加你的业务逻辑
+ ...
+ }
+ ```
+
+### 3. 预加载音乐资源
+
+调用 `isPreloadedWithSongCode` 检测获取到且待播放的音乐资源是否已在本地预加载:
+
+- 如果尚未预加载,调用 `preloadWithSongCode` 预加载音乐资源。其中,`songCode` 取值为上节操作中 `onMusicCollectionResult` 回调在 `result` 参数中报告的 `songCode`。等 `onPreLoadEvent` 回调报告加载状态为 `AgoraMusicContentCenterPreloadStatusOK(0)` 后,再进行下一节的打开与播放音乐操作。
+- 如果已经预加载,直接跳过本节操作。
+
+`isPreloadedWithSongCode` 方法可同步调用且不包含耗时操作。
+
+
+```objective-c
+typedef void (^LoadMusicCallback)(AgoraMusicContentCenterPreloadStatus);
+
+- (void)preloadMusic:(NSInteger)songCode withCallback:(LoadMusicCallback)block {
+ NSInteger error = [self.musicCenter isPreloadedWithSongCode:songCode];
+ NSString* songCodeKey = [NSString stringWithFormat: @"%ld", songCode];
+ if(error == 0) {
+ if(block) {
+ [self.musicCallbacks removeObjectForKey:songCodeKey];
+ block(AgoraMusicContentCenterPreloadStatusOK);
+ }
+
+ return;
+ }
+
+ error = [self.musicCenter preloadWithSongCode:songCode jsonOption:nil];
+ if (error != 0) {
+ if(block) {
+ [self.musicCallbacks removeObjectForKey:songCodeKey];
+ block(AgoraMusicContentCenterPreloadStatusError);
+ }
+ return;
+ }
+ [self.musicCallbacks setObject:block forKey:songCodeKey];
+}
+
+#pragma mark - AgoraMusicContentCenterEventDelegate
+- (void)onPreLoadEvent:(NSInteger)songCode
+ percent:(NSInteger)percent
+ status:(AgoraMusicContentCenterPreloadStatus)status
+ msg:(nonnull NSString *)msg
+ lyricUrl:(nonnull NSString *)lyricUrl {
+ if (status == AgoraMusicContentCenterPreloadStatusPreloading) {
+ return;
+ }
+ NSString* songCodeKey = [NSString stringWithFormat: @"%ld", songCode];
+ LoadMusicCallback block = [self.musicCallbacks objectForKey:songCodeKey];
+ if(!block) {
+ return;
+ }
+ [self.musicCallbacks removeObjectForKey:songCodeKey];
+ block(status);
+}
+```
+
+### 4. 打开并播放音乐资源
+
+调用 `AgoraMusicPlayerProtocol.openMediaWithSongCode` 打开已经预加载完的音乐资源。通过 `AgoraRtcMediaPlayerDelegate.didChangedToState` 回调监听音乐播放器的状态变化,并作出相应处理:
+ - 状态为 AgoraMediaPlayerStateOpenCompleted 时,将系统时间作为播放器开始播放的时间,将 0 作为播放进度。对主唱,使用 dispatch_after 函数在 0.5 秒后在主线程中调用 play 播放音乐。推迟 0.5 秒播放可以避免对齐问题。//TODO
+ - 状态为 AgoraMediaPlayerStateStopped 和 AgoraMediaPlayerStatePlaying 时,将系统时间作为播放器开始播放的时间,将 0 作为播放进度。
+
+
+收到 `AgoraRtcMediaPlayerDelegate.didChangedToState` 回调报告的音乐播放器状态为 `AgoraMediaPlayerStateOpenCompleted(5)` 后,调用 `AgoraRtcMediaPlayerProtocol.play` 播放音乐。
+
+```objective-c
+- (void)openMusicMedia:(NSInteger)songCode{
+ [self.rtcMediaPlayer openMediaWithSongCode:songCode startPos:0];
+}
+
+
+#pragma mark - AgoraRtcMediaPlayerDelegate
+- (void)AgoraRtcMediaPlayer:(id)playerKit didChangedToState:(AgoraMediaPlayerState)state error:(AgoraMediaPlayerError)error
+{
+ if (state == AgoraMediaPlayerStateOpenCompleted) {
+ self.localPlayerPosition = uptime();
+ self.playerDuration = 0;
+ if (self.config.role == KTVSingRoleMainSinger) {
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+ [playerKit play];
+ });
+ }
+ } else if (state == AgoraMediaPlayerStateStopped) {
+ self.localPlayerPosition = uptime();
+ self.playerDuration = 0;
+ } else if (state == AgoraMediaPlayerStatePlaying) {
+ self.localPlayerPosition = uptime();
+ self.playerDuration = 0;
+ }
+ if (self.config.role == KTVSingRoleMainSinger) {
+ [self syncPlayState:state];
+ }
+ self.playerState = state;
+ ....
+}
+```
+
+## API 参考
+
+本节列出本文提到的 API:
+
+- AgoraMusicContentCenter:
+ - [sharedContentCenterWithConfig](./API%20Reference/ios_ng/API/toc_drm.html?platform=iOS#api_imusiccontentcenter_initialize)
+ - [createMusicPlayerWithDelegate](./API%20Reference/ios_ng/API/toc_drm.html?platform=iOS#api_imusiccontentcenter_createmusicplayer)
+ - [searchMusicWithKeyWord](./API%20Reference/ios_ng/API/toc_drm.html?platform=iOS#api_imusiccontentcenter_searchmusic)
+ - [getMusicCollectionWithMusicChartId](./API%20Reference/ios_ng/API/toc_drm.html?platform=iOS#api_imusiccontentcenter_getmusiccollectionbymusicchartid)
+ - [isPreloadedWithSongCode](./API%20Reference/ios_ng/API/toc_drm.html?platform=iOS#api_imusiccontentcenter_ispreloaded)
+ - [preloadWithSongCode](./API%20Reference/ios_ng/API/toc_drm.html?platform=iOS#api_imusicontentcenter_preload)
+- AgoraMusicContentCenterEventDelegate:
+ - [onMusicCollectionResult](./API%20Reference/ios_ng/API/toc_drm.html#callback_imusiccontentcentereventhandler_onmusiccollectionresult)
+ - [onPreLoadEvent](./API%20Reference/ios_ng/API/toc_drm.html#callback_imusiccontentcentereventhandler_onpreloadevent)
+- AgoraMusicPlayerProtocol:
+ - [openMediaWithSongCode](./API%20Reference/ios_ng/API/toc_drm.html?platform=iOS#api_imusicplayer_open)
+- AgoraRtcMediaPlayerProtocol:
+ - [play](./API%20Reference/ios_ng/v4.1.0/API/toc_mediaplayer.html#api_imediaplayer_play)
+- AgoraRtcMediaPlayerDelegate:
+ - [didChangedToState](./API%20Reference/ios_ng/v4.1.0/API/toc_mediaplayer.html#callback_imediaplayersourceobserver_onplayersourcestatechanged)
+
+如需了解更多,请参考[音频 SDK API 参考文档](./API%20Reference/ios_ng/API/rtc_api_overview_ng.html)。
+
+## 下一步
+//TODO
+点歌后,点歌用户可以进行如下操作:
+- 切换原唱和伴奏,分别调节原唱和伴奏的音量,设置混响、美声、音效,对歌曲进行升调或降调,开启或关闭耳返功能。
+- 开始独唱或和房间其他上麦用户合唱。
+- 多次点歌后,在已点歌曲列表中对歌曲进行排序,调整演唱顺序。在已点歌曲列表中删除歌曲,放弃不想演唱的歌曲。在 K 歌界面点击切歌按钮,放弃演唱当前播放的歌曲。
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\347\202\271\346\255\214.wsd" "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\347\202\271\346\255\214.wsd"
new file mode 100644
index 00000000000..415cc27b0fd
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\347\202\271\346\255\214.wsd"
@@ -0,0 +1,25 @@
+@startuml
+title 点歌 API 时序图
+autonumber
+skinparam monochrome true
+participant "iOS 应用" as a
+participant "声网 SDK" as b
+== 初始化和创建 ==
+a -> b: **AgoraMusicContentCenter**.sharedContentCenterWithConfig
+a -> b: AgoraMusicContentCenter.createMusicPlayerWithDelegate
+== 获取音乐资源(方式一) ==
+a -> b: AgoraMusicContentCenter.searchMusicWithKeyWord
+b -->> a: AgoraMusicContentCenterEventDelegate.onMusicCollectionResult
+== 获取音乐资源(方式二) ==
+a -> b: AgoraMusicContentCenter.getMusicCollectionWithMusicChartId
+b -->> a: AgoraMusicContentCenterEventDelegate.onMusicCollectionResult
+== 预加载音乐资源 ==
+a -> b: AgoraMusicContentCenter.isPreloadedWithSongCode
+a -> b: AgoraMusicContentCenter.preloadWithSongCode
+b -->> a: AgoraMusicContentCenterEventDelegate.onPreLoadEvent (AgoraMusicContentCenterPreloadStatus = 0)
+== 打开并播放音乐资源 ==
+a -> b: **AgoraMusicPlayerProtocol**.openMediaWithSongCode
+b -->> a: AgoraRtcMediaPlayerDelegate.didChangedToState (AgoraMediaPlayerState = 5)
+a ->b: **AgoraRtcMediaPlayerProtocol**.play
+b -->> a: AgoraRtcMediaPlayerDelegate.didChangedToState (AgoraMediaPlayerState = 3)
+@enduml
\ No newline at end of file
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\347\213\254\345\224\261.md" "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\347\213\254\345\224\261.md"
new file mode 100644
index 00000000000..66ca8195fab
--- /dev/null
+++ "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\347\213\254\345\224\261.md"
@@ -0,0 +1,194 @@
+## 简介
+
+本文介绍如何实现独唱功能。主唱点歌后,可以开始独唱,K 歌房内的听众都可以听到主唱唱歌。房间内想与主唱连麦语聊的听众可以上麦。
+
+### 技术架构
+
+
+
+## 实现方法(主唱)
+
+### 1. 获取歌词
+
+调用 `getLyricWithSongCode`,SDK 会触发 `onLyricResult` 回调报告歌词的下载地址(`lyricUrl`)。
+
+```objective-c
+- (void)loadLyric:(NSInteger)songNo withCallback:(void (^ _Nullable)(NSString* lyricUrl))block {
+ NSString* requestId = [self.musicCenter getLyricWithSongCode:songNo lyricType:0];
+ if ([requestId length] == 0) {
+ if (block) {
+ block(nil);
+ }
+ return;
+ }
+ [self.lyricCallbacks setObject:block forKey:requestId];
+}
+
+#pragma mark AgoraMusicContentCenterEventDelegate
+- (void)onLyricResult:(nonnull NSString *)requestId
+ lyricUrl:(nonnull NSString *)lyricUrl {
+ LyricCallback callback = [self.lyricCallbacks objectForKey:requestId];
+ if(!callback) {
+ return;
+ }
+ [self.lyricCallbacks removeObjectForKey:requestId];
+
+ if ([lyricUrl length] == 0) {
+ callback(nil);
+ return;
+ }
+
+ callback(lyricUrl);
+}
+```
+
+### 2. 发布音乐和人声
+
+发布音乐和人声指发布音乐播放器的音频流和麦克风采集到主唱的音频流。主唱在频道内的角色为 AgoraClientRoleBroadcaster,因此 SDK 默认发布主唱的音频流。发布音乐播放器的音频流需要你先调用 `AgoraMusicPlayerProtocol.openMediaWithSongCode` 方法打开音乐资源,再通过 AgoraRtcChannelMediaOptions 对象设置 publishMediaPlayerAudioTrack 为 true 并在 publishMediaPlayerId 里传入播放器 ID。
+
+```objective-c
+- (void)playSong:(NSInteger)songCode
+{
+ KTVSingRole role = self.config.role;
+ KTVSongType type = self.config.type;
+ if(type == KTVSongTypeSolo) {
+ if(role == KTVSingRoleMainSinger) {
+ [self.rtcMediaPlayer openMediaWithSongCode:songCode startPos:0];
+ AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+ options.autoSubscribeAudio = YES;
+ options.autoSubscribeVideo = YES;
+ options.publishMediaPlayerId = [self.rtcMediaPlayer getMediaPlayerId];
+ options.publishMediaPlayerAudioTrack = YES;
+ [self.engine updateChannelWithMediaOptions:options];
+ } else {
+ ....
+ }
+ } else {
+ ....
+ }
+}
+```
+
+### 3. 发布音乐播放进度
+
+自行定义 syncPlayerPosition 方法,在该方法中创建一个 dict 字典对象,用于存储三个键值对:
+
+- cmd:消息的命令。
+- duration:音乐文件的总时长。
+- time:音乐播放进度。
+
+将 dict 字典对象序列化成 JSON 数据,然后调用 sendStreamMessage 将 JSON 数据发送到数据流中。用户接收到数据流,可以解析出音乐播放进度和总时长。
+
+SDK 会在音乐播放时每秒触发一次 didChangedToPosition 回调,你可以通过该回调调用 syncPlayerPosition 方法,达到每秒同步音乐播放进度的效果。
+
+**Note**:audioPlayoutDelay 是音频播放延迟,即从音频数据发送至接收端,到数据在接收端开始播放所需的时间。因为不同机型设备上的 audioPlayoutDelay 不同,主唱端和听众端可能使用不同的机型设备,所以在发布主唱的音乐播放进度时,声网建议在主唱端将进度减去 audioPlayoutDelay,在接收端计算时再加上接收端的 audioPlayoutDelay。
+
+```objective-c
+- (void)AgoraRtcMediaPlayer:(id)playerKit didChangedToPosition:(NSInteger)position
+{
+ self.localPlayerPosition = uptime() - position;
+
+ if (self.config.role == KTVSingRoleMainSinger && position > self.audioPlayoutDelay) {
+ //if i am main singer
+ NSDictionary *dict = @{
+ @"cmd":@"setLrcTime",
+ @"duration":@(self.playerDuration),
+ // 发布主唱的音乐播放进度时,声网建议在主唱端将进度减去 audioPlayoutDelay,在接收端计算时再加上接收端的 audioPlayoutDelay。
+ @"time":@(position - self.audioPlayoutDelay),
+ @"ntp":@([self getNtpTimeInMs]),
+ @"playerState":@(self.playerState)
+ };
+ [self sendStreamMessageWithDict:dict success:nil];
+ }
+
+ ....
+}
+
+
+- (void)rtcEngine:(AgoraRtcEngineKit *)engine localAudioStats:(AgoraRtcLocalAudioStats *)stats {
+ self.audioPlayoutDelay = stats.audioPlayoutDelay;
+}
+```
+
+### 4.(可选)关闭麦克风
+
+主唱停止唱歌或希望暂时关闭麦克风时,可以调用 adjustRecordingSignalVolume,将音频采集信号音量设置为 0。
+
+```objective-c
+[self.RTCkit adjustRecordingSignalVolume:isNowMicMuted ? 0 : 100];
+```
+
+
+## 实现方法(听众)
+
+### 1. 订阅音频流
+
+听众需要将 autoSubscribeAudio 设为 YES,以订阅主唱发布的音乐和人声。为了不影响主唱的音乐播放,听众需要确保本地不播放音乐,即将 publishMediaPlayerAudioTrack 设为 NO。
+
+听众的用户角色为 AgoraClientRoleAudience,因此无法在频道内发布音频流。如果听众想上麦与主唱语聊,需要将用户角色修改为 AgoraClientRoleBroadcaster。修改角色后,SDK 默认发布该连麦听众的音频流,主唱和其他听众都能听到连麦听众的声音。
+
+```objective-c
+- (void)playSong:(NSInteger)songCode
+{
+ KTVSingRole role = self.config.role;
+ KTVSongType type = self.config.type;
+ if(type == KTVSongTypeSolo) {
+ if(role == KTVSingRoleMainSinger) {
+ ....
+ } else {
+ AgoraRtcChannelMediaOptions* options = [AgoraRtcChannelMediaOptions new];
+ options.autoSubscribeAudio = YES;
+ options.autoSubscribeVideo = YES;
+ options.publishMediaPlayerAudioTrack = NO;
+ [self.engine updateChannelWithMediaOptions:options];
+ }
+ } else {
+ ....
+ }
+}
+```
+
+### 2. 歌词同步
+
+听众通过 receiveStreamMessageFromUid 回调接收数据流,并在回调中通过自行定义的 dictionaryForJsonData 方法将接收到的数据转换为字典对象。当字典中的 cmd 值为 setLrcTime 时,将字典中的 time 值转换成整数并赋值给 remotePlayerPosition,将字典中的 duration 值转换成整数并赋值给 remotePlayerDuration,从而解析出主唱同步的音乐播放进度和总时长。最后,歌词组件通过音乐播放进度和总时长,在歌词界面中渲染出代表歌词播放进度的视图。
+
+```objective-c
+- (void)setLrcView:(AgoraLrcScoreView *)lrcView
+{
+ _lrcView = lrcView;
+ lrcView.delegate = self;
+}
+
+
+#pragma mark - AgoraLrcViewDelegate
+- (NSTimeInterval)getTotalTime {
+ if (self.config.role == KTVSingRoleMainSinger) {
+ // 调用 getDuration 会耗时,建议只在播放、暂停等情况下调用
+ NSTimeInterval time = [_rtcMediaPlayer getDuration];
+ return time;
+ }
+ return self.remotePlayerDuration;
+}
+
+- (NSTimeInterval)getPlayerCurrentTime {
+ if (self.config.role == KTVSingRoleMainSinger) {
+ NSTimeInterval time = [_rtcMediaPlayer getPosition];
+ return time;
+ }
+
+ return self.remotePlayerPosition;
+}
+
+#pragma mark - AgoraRtcEngineDelegate
+- (void)rtcEngine:(AgoraRtcEngineKit *)engine receiveStreamMessageFromUid:(NSUInteger)uid streamId:(NSInteger)streamId data:(NSData *)data
+{
+ NSDictionary *dict = [VLGlobalHelper dictionaryForJsonData:data];
+ if ([dict[@"cmd"] isEqualToString:@"setLrcTime"]) {
+ NSInteger position = [dict[@"time"] integerValue];
+ NSInteger duration = [dict[@"duration"] integerValue];
+ self.remotePlayerPosition = position;
+ self.remotePlayerDuration = duration;
+ }
+}
+```
+
diff --git "a/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\351\233\206\346\210\220\346\246\202\350\277\260" "b/markdown/online-ktv/\346\224\271\350\277\233/\351\235\236\345\234\272\346\231\257\345\214\226\346\226\271\346\241\210 (WIP)/iOS/\351\233\206\346\210\220\346\246\202\350\277\260"
new file mode 100644
index 00000000000..e69de29bb2d