diff --git a/android/build.gradle b/android/build.gradle index f8c8b82b0..c862f1dfe 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -127,7 +127,7 @@ def kotlin_version = getExtOrDefault('kotlinVersion') dependencies { // noinspection GradleDynamicVersion api 'com.facebook.react:react-native:+' - api 'com.github.agorabuilder:native-full-sdk:3.5.0.4' + api 'com.github.agorabuilder:native-full-sdk:3.5.2' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } diff --git a/android/src/main/java/io/agora/rtc/base/Annotations.java b/android/src/main/java/io/agora/rtc/base/Annotations.java index e29e42994..a4fc0fbba 100644 --- a/android/src/main/java/io/agora/rtc/base/Annotations.java +++ b/android/src/main/java/io/agora/rtc/base/Annotations.java @@ -327,10 +327,10 @@ public class Annotations { Constants.RELAY_EVENT_PACKET_UPDATE_DEST_CHANNEL_NOT_CHANGE, Constants.RELAY_EVENT_PACKET_UPDATE_DEST_CHANNEL_IS_NULL, Constants.RELAY_EVENT_VIDEO_PROFILE_UPDATE, -// Constants.RELAY_EVENT_PAUSE_SEND_PACKET_TO_DEST_CHANNEL_SUCCESS, -// Constants.RELAY_EVENT_PAUSE_SEND_PACKET_TO_DEST_CHANNEL_FAILED, -// Constants.RELAY_EVENT_RESUME_SEND_PACKET_TO_DEST_CHANNEL_SUCCESS, -// Constants.RELAY_EVENT_RESUME_SEND_PACKET_TO_DEST_CHANNEL_FAILED, + Constants.RELAY_EVENT_PAUSE_SEND_PACKET_TO_DEST_CHANNEL_SUCCESS, + Constants.RELAY_EVENT_PAUSE_SEND_PACKET_TO_DEST_CHANNEL_FAILED, + Constants.RELAY_EVENT_RESUME_SEND_PACKET_TO_DEST_CHANNEL_SUCCESS, + Constants.RELAY_EVENT_RESUME_SEND_PACKET_TO_DEST_CHANNEL_FAILED, }) @Retention(RetentionPolicy.SOURCE) public @interface AgoraChannelMediaRelayEvent { @@ -973,6 +973,7 @@ public class Annotations { @IntDef({ VirtualBackgroundSource.BACKGROUND_COLOR, VirtualBackgroundSource.BACKGROUND_IMG, + VirtualBackgroundSource.BACKGROUND_BLUR, }) @Retention(RetentionPolicy.SOURCE) public @interface AgoraVirtualBackgroundSourceType { diff --git a/android/src/main/java/io/agora/rtc/base/BeanCovertor.kt b/android/src/main/java/io/agora/rtc/base/BeanCovertor.kt index 3769938d0..97c15edff 100644 --- a/android/src/main/java/io/agora/rtc/base/BeanCovertor.kt +++ b/android/src/main/java/io/agora/rtc/base/BeanCovertor.kt @@ -12,6 +12,7 @@ import io.agora.rtc.live.LiveTranscoding.TranscodingUser import io.agora.rtc.models.ChannelMediaOptions import io.agora.rtc.models.ClientRoleOptions import io.agora.rtc.models.DataStreamConfig +import io.agora.rtc.models.EchoTestConfiguration import io.agora.rtc.video.* fun mapToVideoDimensions(map: Map<*, *>): VideoEncoderConfiguration.VideoDimensions { @@ -261,5 +262,15 @@ fun mapToVirtualBackgroundSource(map: Map<*, *>): VirtualBackgroundSource { (map["backgroundSourceType"] as? Number)?.let { backgroundSourceType = it.toInt() } (map["color"] as? Map<*, *>)?.let { color = mapToColor(it) } (map["source"] as? String)?.let { source = it } + (map["blur_degree"] as? Int)?.let { blur_degree = it } + } +} + +fun mapToEchoTestConfiguration(map: Map<*, *>): EchoTestConfiguration { + return EchoTestConfiguration().apply { + (map["enableAudio"] as? Boolean)?.let { enableAudio = it } + (map["enableVideo"] as? Boolean)?.let { enableVideo = it } + (map["token"] as? String)?.let { token = it } + (map["channelId"] as? String)?.let { channelId = it } } } diff --git a/android/src/main/java/io/agora/rtc/base/Extensions.kt b/android/src/main/java/io/agora/rtc/base/Extensions.kt index fb8df0607..cb24c448c 100644 --- a/android/src/main/java/io/agora/rtc/base/Extensions.kt +++ b/android/src/main/java/io/agora/rtc/base/Extensions.kt @@ -155,6 +155,13 @@ fun AgoraFacePositionInfo.toMap(): Map { ) } +fun AudioFileInfo.toMap(): Map { + return hashMapOf( + "filePath" to filePath, + "durationMs" to durationMs + ) +} + fun Array.toMapList(): List> { return List(size) { this[it].toMap() } } diff --git a/android/src/main/java/io/agora/rtc/base/RtcEngine.kt b/android/src/main/java/io/agora/rtc/base/RtcEngine.kt index f1b09ca2b..ab510b994 100644 --- a/android/src/main/java/io/agora/rtc/base/RtcEngine.kt +++ b/android/src/main/java/io/agora/rtc/base/RtcEngine.kt @@ -68,6 +68,8 @@ class IRtcEngine { fun setLocalAccessPoint(params: Map, callback: Callback) fun enableVirtualBackground(params: Map, callback: Callback) + + fun takeSnapshot(params: Map, callback: Callback) } interface RtcUserInfoInterface { @@ -161,11 +163,21 @@ class IRtcEngine { fun getAudioMixingDuration(params: Map, callback: Callback) + fun getAudioFileInfo(params: Map, callback: Callback) + fun getAudioMixingCurrentPosition(callback: Callback) fun setAudioMixingPosition(params: Map, callback: Callback) fun setAudioMixingPitch(params: Map, callback: Callback) + + fun setAudioMixingPlaybackSpeed(params: Map, callback: Callback) + + fun getAudioTrackCount(callback: Callback) + + fun selectAudioTrack(params: Map, callback: Callback) + + fun setAudioMixingDualMonoMode(params: Map, callback: Callback) } interface RtcAudioEffectInterface { @@ -371,31 +383,51 @@ class IRtcEngine { } } -class RtcEngineManager( - private val emit: (methodName: String, data: Map?) -> Unit +open class RtcEngineFactory { + open fun create( + params: Map, + rtcEngineEventHandler: RtcEngineEventHandler + ): RtcEngine? { + val engine = RtcEngineEx.create(mapToRtcEngineConfig(params["config"] as Map<*, *>).apply { + mContext = params["context"] as Context + mEventHandler = rtcEngineEventHandler + }) + + return engine + } +} + +open class RtcEngineManager( + private val emit: (methodName: String, data: Map?) -> Unit, + private val rtcEngineFactory: RtcEngineFactory = RtcEngineFactory() ) : IRtcEngine.RtcEngineInterface { var engine: RtcEngine? = null private set private var mediaObserver: MediaObserver? = null fun release() { - RtcEngine.destroy() - engine = null + if (engine != null) { + RtcEngine.destroy() + engine = null + } mediaObserver = null } override fun create(params: Map, callback: Callback) { - engine = RtcEngineEx.create(mapToRtcEngineConfig(params["config"] as Map<*, *>).apply { - mContext = params["context"] as Context - mEventHandler = RtcEngineEventHandler { methodName, data -> - emit(methodName, data) - } + engine = rtcEngineFactory.create(params, RtcEngineEventHandler { methodName, data -> + emit(methodName, data) }) - callback.code((engine as RtcEngineEx).setAppType((params["appType"] as Number).toInt())) + callback.code((engine as RtcEngineEx).setAppType((params["appType"] as Number).toInt())) { + RtcEngineRegistry.instance.onRtcEngineCreated(engine) + it + } } override fun destroy(callback: Callback) { - callback.resolve(engine) { release() } + callback.resolve(engine) { + release() + RtcEngineRegistry.instance.onRtcEngineDestroyed() + } } override fun setChannelProfile(params: Map, callback: Callback) { @@ -553,6 +585,16 @@ class RtcEngineManager( ) } + override fun takeSnapshot(params: Map, callback: Callback) { + callback.code( + engine?.takeSnapshot( + params["channel"] as String, + (params["uid"] as Number).toNativeUInt(), + params["filePath"] as String + ) + ) + } + override fun registerLocalUserAccount(params: Map, callback: Callback) { callback.code( engine?.registerLocalUserAccount( @@ -802,13 +844,13 @@ class RtcEngineManager( } override fun getAudioMixingDuration(params: Map, callback: Callback) { - (params["filePath"] as? String)?.let { file -> - callback.code(engine?.getAudioMixingDuration(file)) { it } - return@getAudioMixingDuration - } callback.code(engine?.audioMixingDuration) { it } } + override fun getAudioFileInfo(params: Map, callback: Callback) { + callback.code(engine?.getAudioFileInfo(params["filePath"] as String)) + } + override fun getAudioMixingCurrentPosition(callback: Callback) { callback.code(engine?.audioMixingCurrentPosition) { it } } @@ -821,6 +863,22 @@ class RtcEngineManager( callback.code(engine?.setAudioMixingPitch((params["pitch"] as Number).toInt())) } + override fun setAudioMixingPlaybackSpeed(params: Map, callback: Callback) { + callback.code(engine?.setAudioMixingPlaybackSpeed((params["speed"] as Number).toInt())) + } + + override fun getAudioTrackCount(callback: Callback) { + callback.code(engine?.audioTrackCount) { it } + } + + override fun selectAudioTrack(params: Map, callback: Callback) { + callback.code(engine?.selectAudioTrack((params["index"] as Number).toInt())) + } + + override fun setAudioMixingDualMonoMode(params: Map, callback: Callback) { + callback.code(engine?.setAudioMixingDualMonoMode((params["mode"] as Number).toInt())) + } + override fun getEffectsVolume(callback: Callback) { callback.resolve(engine) { it.audioEffectManager.effectsVolume } } @@ -1033,13 +1091,11 @@ class RtcEngineManager( } override fun pauseAllChannelMediaRelay(callback: Callback) { - callback.code(-Constants.ERR_NOT_SUPPORTED) -// callback.code(engine?.pauseAllChannelMediaRelay()) + callback.code(engine?.pauseAllChannelMediaRelay()) } override fun resumeAllChannelMediaRelay(callback: Callback) { - callback.code(-Constants.ERR_NOT_SUPPORTED) -// callback.code(engine?.resumeAllChannelMediaRelay()) + callback.code(engine?.resumeAllChannelMediaRelay()) } override fun setDefaultAudioRoutetoSpeakerphone(params: Map, callback: Callback) { @@ -1097,7 +1153,15 @@ class RtcEngineManager( } override fun startEchoTest(params: Map, callback: Callback) { - callback.code(engine?.startEchoTest((params["intervalInSeconds"] as Number).toInt())) + (params["intervalInSeconds"] as? Number)?.let { intervalInSeconds -> + callback.code(engine?.startEchoTest(intervalInSeconds.toInt())) + return@startEchoTest + } + (params["config"] as? Map<*, *>)?.let { config -> + callback.code(engine?.startEchoTest(mapToEchoTestConfiguration(config))) + return@startEchoTest + } + callback.code(engine?.startEchoTest()) } override fun stopEchoTest(callback: Callback) { diff --git a/android/src/main/java/io/agora/rtc/base/RtcEngineEvent.kt b/android/src/main/java/io/agora/rtc/base/RtcEngineEvent.kt index d6df22bd0..15ac9c19d 100644 --- a/android/src/main/java/io/agora/rtc/base/RtcEngineEvent.kt +++ b/android/src/main/java/io/agora/rtc/base/RtcEngineEvent.kt @@ -33,6 +33,7 @@ class RtcEngineEvents { const val LocalVideoStateChanged = "LocalVideoStateChanged" const val RemoteAudioStateChanged = "RemoteAudioStateChanged" const val LocalAudioStateChanged = "LocalAudioStateChanged" + const val RequestAudioFileInfo = "RequestAudioFileInfo" const val LocalPublishFallbackToAudioOnly = "LocalPublishFallbackToAudioOnly" const val RemoteSubscribeFallbackToAudioOnly = "RemoteSubscribeFallbackToAudioOnly" const val AudioRouteChanged = "AudioRouteChanged" @@ -87,6 +88,7 @@ class RtcEngineEvents { const val UserSuperResolutionEnabled = "UserSuperResolutionEnabled" const val UploadLogResult = "UploadLogResult" const val VirtualBackgroundSourceEnabled = "VirtualBackgroundSourceEnabled" + const val SnapshotTaken = "SnapshotTaken" fun toMap(): Map { return hashMapOf( @@ -116,6 +118,7 @@ class RtcEngineEvents { "LocalVideoStateChanged" to LocalVideoStateChanged, "RemoteAudioStateChanged" to RemoteAudioStateChanged, "LocalAudioStateChanged" to LocalAudioStateChanged, + "RequestAudioFileInfo" to RequestAudioFileInfo, "LocalPublishFallbackToAudioOnly" to LocalPublishFallbackToAudioOnly, "RemoteSubscribeFallbackToAudioOnly" to RemoteSubscribeFallbackToAudioOnly, "AudioRouteChanged" to AudioRouteChanged, @@ -169,7 +172,8 @@ class RtcEngineEvents { "RtmpStreamingEvent" to RtmpStreamingEvent, "UserSuperResolutionEnabled" to UserSuperResolutionEnabled, "UploadLogResult" to UploadLogResult, - "VirtualBackgroundSourceEnabled" to VirtualBackgroundSourceEnabled + "VirtualBackgroundSourceEnabled" to VirtualBackgroundSourceEnabled, + "SnapshotTaken" to SnapshotTaken ) } } @@ -325,6 +329,10 @@ class RtcEngineEventHandler( callback(RtcEngineEvents.LocalAudioStateChanged, state, error) } + override fun onRequestAudioFileInfo(info: AudioFileInfo?, error: Int) { + callback(RtcEngineEvents.RequestAudioFileInfo, info?.toMap(), error) + } + override fun onLocalPublishFallbackToAudioOnly(isFallbackOrRecover: Boolean) { callback(RtcEngineEvents.LocalPublishFallbackToAudioOnly, isFallbackOrRecover) } @@ -688,4 +696,23 @@ class RtcEngineEventHandler( ) { callback(RtcEngineEvents.VirtualBackgroundSourceEnabled, enabled, reason) } + + override fun onSnapshotTaken( + channel: String?, + uid: Int, + filePath: String?, + width: Int, + height: Int, + errCode: Int + ) { + callback( + RtcEngineEvents.SnapshotTaken, + channel, + uid.toUInt().toLong(), + filePath, + width, + height, + errCode + ) + } } diff --git a/android/src/main/java/io/agora/rtc/base/RtcEnginePlugin.kt b/android/src/main/java/io/agora/rtc/base/RtcEnginePlugin.kt new file mode 100644 index 000000000..0c434437a --- /dev/null +++ b/android/src/main/java/io/agora/rtc/base/RtcEnginePlugin.kt @@ -0,0 +1,47 @@ +package io.agora.rtc.base + +import io.agora.rtc.RtcEngine + +/** + * A [RtcEnginePlugin] allows developers to interact with the [RtcEngine] which created from flutter + * side. + */ +interface RtcEnginePlugin { + + /** + * This callback will be called when the [RtcEngine] is created by + * [RtcEngine.createWithContext](https://docs.agora.io/cn/Video/API%20Reference/flutter/agora_rtc_engine/RtcEngine/createWithContext.html) + * function from flutter. + * + * NOTE that you should not call [RtcEngine.destroy], because it will also destroy the `RtcEngine` + * used by flutter side. + * + * @param rtcEngine The same [RtcEngine] used by flutter side + */ + fun onRtcEngineCreated(rtcEngine: RtcEngine?) + + /** + * This callback will be called when the [RtcEngine.destroy](https://docs.agora.io/cn/Video/API%20Reference/flutter/v4.0.7/rtc_channel/RtcChannel/destroy.html) + * function is called from flutter. + */ + fun onRtcEngineDestroyed() + + companion object Registrant { + /** + * Register a [RtcEnginePlugin]. The [plugin] will be called when the [RtcEngine] is created from + * flutter side. + */ + fun register(plugin: RtcEnginePlugin) { + RtcEngineRegistry.instance.add(plugin = plugin) + } + + /** + * Unregister a previously registered [RtcEnginePlugin]. + */ + fun unregister(plugin: RtcEnginePlugin) { + RtcEngineRegistry.instance.remove(pluginClass = plugin.javaClass) + } + } +} + + diff --git a/android/src/main/java/io/agora/rtc/base/RtcEngineRegistry.kt b/android/src/main/java/io/agora/rtc/base/RtcEngineRegistry.kt new file mode 100644 index 000000000..e06866bfd --- /dev/null +++ b/android/src/main/java/io/agora/rtc/base/RtcEngineRegistry.kt @@ -0,0 +1,43 @@ +package io.agora.rtc.base + +import io.agora.rtc.RtcEngine +import java.util.HashMap + +/** + * The [RtcEngineRegistry] is response to add, remove and notify the callback when [RtcEngine] is created + * from flutter side. + */ +internal class RtcEngineRegistry private constructor() : RtcEnginePlugin { + companion object { + val instance: RtcEngineRegistry by lazy { RtcEngineRegistry() } + } + + private val plugins: MutableMap, RtcEnginePlugin> = HashMap() + + /** + * Add a [RtcEnginePlugin]. + */ + fun add(plugin: RtcEnginePlugin) { + if (plugins.containsKey(plugin.javaClass)) return + plugins[plugin.javaClass] = plugin + } + + /** + * Remove the previously added [RtcEnginePlugin]. + */ + fun remove(pluginClass: Class) { + plugins.remove(pluginClass) + } + + override fun onRtcEngineCreated(rtcEngine: RtcEngine?) { + for (plugin in plugins.values) { + plugin.onRtcEngineCreated(rtcEngine) + } + } + + override fun onRtcEngineDestroyed() { + for (plugin in plugins.values) { + plugin.onRtcEngineDestroyed() + } + } +} diff --git a/android/src/main/java/io/agora/rtc/react/RCTAgoraRtcEngineModule.kt b/android/src/main/java/io/agora/rtc/react/RCTAgoraRtcEngineModule.kt index 44a5bc90e..c9ce6a409 100644 --- a/android/src/main/java/io/agora/rtc/react/RCTAgoraRtcEngineModule.kt +++ b/android/src/main/java/io/agora/rtc/react/RCTAgoraRtcEngineModule.kt @@ -16,7 +16,7 @@ class RCTAgoraRtcEngineModule( const val REACT_CLASS = "RCTAgoraRtcEngineModule" } - private val manager = RtcEngineManager { methodName, data -> emit(methodName, data) } + private val manager = RtcEngineManager(emit = { methodName, data -> emit(methodName, data) }) override fun getName(): String { return REACT_CLASS diff --git a/example/ios/AgoraExample.xcodeproj/project.pbxproj b/example/ios/AgoraExample.xcodeproj/project.pbxproj index 9f8ac5577..49d858ea4 100644 --- a/example/ios/AgoraExample.xcodeproj/project.pbxproj +++ b/example/ios/AgoraExample.xcodeproj/project.pbxproj @@ -232,6 +232,7 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-AgoraExample/Pods-AgoraExample-frameworks.sh", "${PODS_ROOT}/AgoraRtcEngine_iOS/AgoraAIDenoiseExtension.framework", + "${PODS_ROOT}/AgoraRtcEngine_iOS/AgoraCIExtension.framework", "${PODS_ROOT}/AgoraRtcEngine_iOS/AgoraCore.framework", "${PODS_ROOT}/AgoraRtcEngine_iOS/AgoraDav1dExtension.framework", "${PODS_ROOT}/AgoraRtcEngine_iOS/AgoraFDExtension.framework", @@ -241,11 +242,12 @@ "${PODS_ROOT}/AgoraRtcEngine_iOS/AgoraVideoSegmentationExtension.framework", "${PODS_ROOT}/AgoraRtcEngine_iOS/Agorafdkaac.framework", "${PODS_ROOT}/AgoraRtcEngine_iOS/Agoraffmpeg.framework", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL/OpenSSL.framework/OpenSSL", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AgoraAIDenoiseExtension.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AgoraCIExtension.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AgoraCore.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AgoraDav1dExtension.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AgoraFDExtension.framework", diff --git a/ios/RCTAgora/Base/BeanCovertor.swift b/ios/RCTAgora/Base/BeanCovertor.swift index 59b2041e9..ae6c5240f 100644 --- a/ios/RCTAgora/Base/BeanCovertor.swift +++ b/ios/RCTAgora/Base/BeanCovertor.swift @@ -430,5 +430,27 @@ func mapToVirtualBackgroundSource(_ map: [String: Any]) -> AgoraVirtualBackgroun backgroundSource.color = UInt(red * 255.0) << 16 + UInt(green * 255.0) << 8 + UInt(blue * 255.0) } backgroundSource.source = map["source"] as? String + if let blurDegree = map["blur_degree"] as? NSNumber { + if let blurDegree = AgoraBlurDegree(rawValue: blurDegree.uintValue) { + backgroundSource.blur_degree = blurDegree + } + } return backgroundSource } + +func mapToEchoTestConfiguration(_ map: [String: Any]) -> AgoraEchoTestConfiguration { + let config = AgoraEchoTestConfiguration() + if let enableAudio = map["enableAudio"] as? NSNumber { + config.enableAudio = enableAudio.boolValue + } + if let enableVideo = map["enableVideo"] as? NSNumber { + config.enableVideo = enableVideo.boolValue + } + if let token = map["token"] as? String { + config.token = token + } + if let channelId = map["channelId"] as? String { + config.channelId = channelId + } + return config +} diff --git a/ios/RCTAgora/Base/Callback.swift b/ios/RCTAgora/Base/Callback.swift index 1def7da25..8c5bc130a 100644 --- a/ios/RCTAgora/Base/Callback.swift +++ b/ios/RCTAgora/Base/Callback.swift @@ -25,11 +25,7 @@ extension Callback { } let res = runnable?(code) - if res is Void { - success(nil) - } else { - success(res) - } + success(res) } func resolve(_ source: T?, _ runnable: (T) -> Any?) { @@ -40,10 +36,6 @@ extension Callback { } let res = runnable(source) - if res is Void { - success(nil) - } else { - success(res) - } + success(res) } } diff --git a/ios/RCTAgora/Base/Extensions.swift b/ios/RCTAgora/Base/Extensions.swift index 70a4b31b8..d3c6b9f9d 100644 --- a/ios/RCTAgora/Base/Extensions.swift +++ b/ios/RCTAgora/Base/Extensions.swift @@ -188,6 +188,15 @@ extension AgoraFacePositionInfo { } } +extension AgoraRtcAudioFileInfo { + func toMap() -> [String: Any?] { + return [ + "filePath": filePath, + "durationMs": durationMs, + ] + } +} + extension Array where Element: AgoraFacePositionInfo { func toMapList() -> [[String: Any?]] { var list = [[String: Any?]]() diff --git a/ios/RCTAgora/Base/RtcChannelEvent.swift b/ios/RCTAgora/Base/RtcChannelEvent.swift index 69fa31e6f..eca5b4da8 100644 --- a/ios/RCTAgora/Base/RtcChannelEvent.swift +++ b/ios/RCTAgora/Base/RtcChannelEvent.swift @@ -223,27 +223,27 @@ extension RtcChannelEventHandler: AgoraRtcChannelDelegate { callback(RtcChannelEvents.ChannelMediaRelayEvent, rtcChannel, event.rawValue) } - func rtcChannel(_ rtcChannel: AgoraRtcChannel, didAudioPublishStateChange oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int) { + public func rtcChannel(_ rtcChannel: AgoraRtcChannel, didAudioPublishStateChange oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int) { callback(RtcChannelEvents.AudioPublishStateChanged, rtcChannel, rtcChannel.getId(), oldState.rawValue, newState.rawValue, elapseSinceLastState) } - func rtcChannel(_ rtcChannel: AgoraRtcChannel, didVideoPublishStateChange oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int) { + public func rtcChannel(_ rtcChannel: AgoraRtcChannel, didVideoPublishStateChange oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int) { callback(RtcChannelEvents.VideoPublishStateChanged, rtcChannel, rtcChannel.getId(), oldState.rawValue, newState.rawValue, elapseSinceLastState) } - func rtcChannel(_ rtcChannel: AgoraRtcChannel, didAudioSubscribeStateChange uid: UInt, oldState: AgoraStreamSubscribeState, newState: AgoraStreamSubscribeState, elapseSinceLastState: Int) { + public func rtcChannel(_ rtcChannel: AgoraRtcChannel, didAudioSubscribeStateChange uid: UInt, oldState: AgoraStreamSubscribeState, newState: AgoraStreamSubscribeState, elapseSinceLastState: Int) { callback(RtcChannelEvents.AudioSubscribeStateChanged, rtcChannel, rtcChannel.getId(), uid, oldState.rawValue, newState.rawValue, elapseSinceLastState) } - func rtcChannel(_ rtcChannel: AgoraRtcChannel, didVideoSubscribeStateChange uid: UInt, oldState: AgoraStreamSubscribeState, newState: AgoraStreamSubscribeState, elapseSinceLastState: Int) { + public func rtcChannel(_ rtcChannel: AgoraRtcChannel, didVideoSubscribeStateChange uid: UInt, oldState: AgoraStreamSubscribeState, newState: AgoraStreamSubscribeState, elapseSinceLastState: Int) { callback(RtcChannelEvents.VideoSubscribeStateChanged, rtcChannel, rtcChannel.getId(), uid, oldState.rawValue, newState.rawValue, elapseSinceLastState) } - func rtcChannel(_ rtcChannel: AgoraRtcChannel, rtmpStreamingEventWithUrl url: String, eventCode: AgoraRtmpStreamingEvent) { + public func rtcChannel(_ rtcChannel: AgoraRtcChannel, rtmpStreamingEventWithUrl url: String, eventCode: AgoraRtmpStreamingEvent) { callback(RtcChannelEvents.RtmpStreamingEvent, rtcChannel, url, eventCode.rawValue) } - func rtcChannel(_ rtcChannel: AgoraRtcChannel, superResolutionEnabledOfUid uid: UInt, enabled: Bool, reason: AgoraSuperResolutionStateReason) { + public func rtcChannel(_ rtcChannel: AgoraRtcChannel, superResolutionEnabledOfUid uid: UInt, enabled: Bool, reason: AgoraSuperResolutionStateReason) { callback(RtcChannelEvents.UserSuperResolutionEnabled, rtcChannel, uid, enabled, reason.rawValue) } } diff --git a/ios/RCTAgora/Base/RtcEngine.swift b/ios/RCTAgora/Base/RtcEngine.swift index 7682ac50d..c304f39f6 100644 --- a/ios/RCTAgora/Base/RtcEngine.swift +++ b/ios/RCTAgora/Base/RtcEngine.swift @@ -86,6 +86,8 @@ protocol RtcEngineInterface: func setLocalAccessPoint(_ params: NSDictionary, _ callback: Callback) func enableVirtualBackground(_ params: NSDictionary, _ callback: Callback) + + func takeSnapshot(_ params: NSDictionary, _ callback: Callback) } protocol RtcEngineUserInfoInterface { @@ -179,11 +181,21 @@ protocol RtcEngineAudioMixingInterface { func getAudioMixingDuration(_ params: NSDictionary, _ callback: Callback) + func getAudioFileInfo(_ params: NSDictionary, _ callback: Callback) + func getAudioMixingCurrentPosition(_ callback: Callback) func setAudioMixingPosition(_ params: NSDictionary, _ callback: Callback) func setAudioMixingPitch(_ params: NSDictionary, _ callback: Callback) + + func setAudioMixingPlaybackSpeed(_ params: NSDictionary, _ callback: Callback) + + func getAudioTrackCount(_ callback: Callback) + + func selectAudioTrack(_ params: NSDictionary, _ callback: Callback) + + func setAudioMixingDualMonoMode(_ params: NSDictionary, _ callback: Callback) } protocol RtcEngineAudioEffectInterface { @@ -388,20 +400,35 @@ protocol RtcEngineStreamMessageInterface { func sendStreamMessage(_ params: NSDictionary, _ callback: Callback) } +internal class AgoraRtcEngineKitFactory { + func create(_ params: NSDictionary, _ delegate: RtcEngineEventHandler) -> AgoraRtcEngineKit? { + let engine = AgoraRtcEngineKit.sharedEngine( + with: mapToRtcEngineConfig(params["config"] as! [String: Any]), + delegate: delegate) + + return engine + } +} + @objc class RtcEngineManager: NSObject, RtcEngineInterface { private var emitter: (_ methodName: String, _ data: [String: Any?]?) -> Void + private var agoraRtcEngineKitFactory: AgoraRtcEngineKitFactory private(set) var engine: AgoraRtcEngineKit? private var delegate: RtcEngineEventHandler? private var mediaObserver: MediaObserver? - init(_ emitter: @escaping (_ methodName: String, _ data: [String: Any?]?) -> Void) { + init(_ emitter: @escaping (_ methodName: String, _ data: [String: Any?]?) -> Void, + _ agoraRtcEngineKitFactory: AgoraRtcEngineKitFactory = AgoraRtcEngineKitFactory()) { self.emitter = emitter + self.agoraRtcEngineKitFactory = agoraRtcEngineKitFactory } func Release() { - AgoraRtcEngineKit.destroy() - engine = nil + if (engine != nil) { + AgoraRtcEngineKit.destroy() + engine = nil + } delegate = nil mediaObserver = nil } @@ -410,13 +437,18 @@ class RtcEngineManager: NSObject, RtcEngineInterface { delegate = RtcEngineEventHandler { [weak self] in self?.emitter($0, $1) } - engine = AgoraRtcEngineKit.sharedEngine(with: mapToRtcEngineConfig(params["config"] as! [String: Any]), delegate: delegate) - callback.code(engine?.setAppType(AgoraRtcAppType(rawValue: (params["appType"] as! NSNumber).uintValue)!)) + engine = agoraRtcEngineKitFactory.create(params, delegate!) + callback.code(engine?.setAppType(AgoraRtcAppType(rawValue: (params["appType"] as! NSNumber).uintValue)!)) { + RtcEngineRegistry.shared.onRtcEngineCreated(self.engine) + return $0 + } } @objc func destroy(_ callback: Callback) { callback.resolve(engine) { [weak self] _ in self?.Release() + RtcEngineRegistry.shared.onRtcEngineDestroyed() + return nil } } @@ -689,17 +721,15 @@ class RtcEngineManager: NSObject, RtcEngineInterface { } @objc func getAudioMixingDuration(_ params: NSDictionary, _ callback: Callback) { - if let filePath = (params["filePath"] as? String) { - callback.code(engine?.getAudioMixingDuration(filePath)) { - $0 - } - return - } callback.code(engine?.getAudioMixingDuration()) { $0 } } + @objc func getAudioFileInfo(_ params: NSDictionary, _ callback: Callback) { + callback.code(engine?.getAudioFileInfo(params["filePath"] as! String)) + } + @objc func getAudioMixingCurrentPosition(_ callback: Callback) { callback.code(engine?.getAudioMixingCurrentPosition()) { $0 @@ -714,6 +744,25 @@ class RtcEngineManager: NSObject, RtcEngineInterface { callback.code(engine?.setAudioMixingPitch((params["pitch"] as! NSNumber).intValue)) } + @objc func setAudioMixingPlaybackSpeed(_ params: NSDictionary, _ callback: Callback) { + callback.code(engine?.setAudioMixingPlaybackSpeed(Int32((params["speed"] as! NSNumber).intValue))) + } + + @objc func getAudioTrackCount(_ callback: Callback) { + callback.code(engine?.getAudioTrackCount()) { + $0 + } + } + + @objc func selectAudioTrack(_ params: NSDictionary, _ callback: Callback) { + callback.code(engine?.selectAudioTrack((params["index"] as! NSNumber).intValue)) + } + + @objc func setAudioMixingDualMonoMode(_ params: NSDictionary, _ callback: Callback) { + let mode = AgoraAudioMixingDualMonoMode(rawValue: UInt((params["mode"] as! NSNumber).intValue)) + callback.code(engine?.setAudioMixingDualMonoMode(mode!)) + } + @objc func getEffectsVolume(_ callback: Callback) { callback.resolve(engine) { $0.getEffectsVolume() @@ -905,7 +954,15 @@ class RtcEngineManager: NSObject, RtcEngineInterface { } @objc func startEchoTest(_ params: NSDictionary, _ callback: Callback) { - callback.code(engine?.startEchoTest(withInterval: (params["intervalInSeconds"] as! NSNumber).intValue)) + if let intervalInSeconds = (params["intervalInSeconds"] as? NSNumber) { + callback.code(engine?.startEchoTest(withInterval: intervalInSeconds.intValue)) + return + } + if let config = (params["config"] as? [String: Any]) { + callback.code(engine?.startEchoTest(withConfig: mapToEchoTestConfiguration(config))) + return + } + callback.code(engine?.startEchoTest()) } @objc func stopEchoTest(_ callback: Callback) { @@ -1150,16 +1207,22 @@ class RtcEngineManager: NSObject, RtcEngineInterface { } @objc func pauseAllChannelMediaRelay(_ callback: Callback) { - callback.code(-Int32(AgoraErrorCode.notSupported.rawValue)) -// callback.code(engine?.pauseAllChannelMediaRelay()) + callback.code(engine?.pauseAllChannelMediaRelay()) } @objc func resumeAllChannelMediaRelay(_ callback: Callback) { - callback.code(-Int32(AgoraErrorCode.notSupported.rawValue)) -// callback.code(engine?.resumeAllChannelMediaRelay()) + callback.code(engine?.resumeAllChannelMediaRelay()) } @objc func enableVirtualBackground(_ params: NSDictionary, _ callback: Callback) { callback.code(engine?.enableVirtualBackground(params["enabled"] as! Bool, backData: mapToVirtualBackgroundSource(params["backgroundSource"] as! [String: Any]))) } + + @objc func takeSnapshot(_ params: NSDictionary, _ callback: Callback) { + var code: Int32? + if let ret = engine?.takeSnapshot(params["channel"] as! String, uid: (params["uid"] as! NSNumber).intValue, filePath: params["filePath"] as! String) { + code = Int32(ret); + } + callback.code(code) + } } diff --git a/ios/RCTAgora/Base/RtcEngineEvent.swift b/ios/RCTAgora/Base/RtcEngineEvent.swift index 1c10842cd..dd4f555ed 100644 --- a/ios/RCTAgora/Base/RtcEngineEvent.swift +++ b/ios/RCTAgora/Base/RtcEngineEvent.swift @@ -36,6 +36,7 @@ class RtcEngineEvents { static let LocalVideoStateChanged = "LocalVideoStateChanged" static let RemoteAudioStateChanged = "RemoteAudioStateChanged" static let LocalAudioStateChanged = "LocalAudioStateChanged" + static let RequestAudioFileInfo = "RequestAudioFileInfo" static let LocalPublishFallbackToAudioOnly = "LocalPublishFallbackToAudioOnly" static let RemoteSubscribeFallbackToAudioOnly = "RemoteSubscribeFallbackToAudioOnly" static let AudioRouteChanged = "AudioRouteChanged" @@ -91,6 +92,7 @@ class RtcEngineEvents { static let UploadLogResult = "UploadLogResult" static let AirPlayIsConnected = "AirPlayIsConnected" static let VirtualBackgroundSourceEnabled = "VirtualBackgroundSourceEnabled" + static let SnapshotTaken = "SnapshotTaken" static func toMap() -> [String: String] { return [ @@ -120,6 +122,7 @@ class RtcEngineEvents { "LocalVideoStateChanged": LocalVideoStateChanged, "RemoteAudioStateChanged": RemoteAudioStateChanged, "LocalAudioStateChanged": LocalAudioStateChanged, + "RequestAudioFileInfo": RequestAudioFileInfo, "LocalPublishFallbackToAudioOnly": LocalPublishFallbackToAudioOnly, "RemoteSubscribeFallbackToAudioOnly": RemoteSubscribeFallbackToAudioOnly, "AudioRouteChanged": AudioRouteChanged, @@ -175,6 +178,7 @@ class RtcEngineEvents { "UploadLogResult": UploadLogResult, "AirPlayIsConnected": AirPlayIsConnected, "VirtualBackgroundSourceEnabled": VirtualBackgroundSourceEnabled, + "SnapshotTaken": SnapshotTaken, ] } } @@ -298,6 +302,10 @@ extension RtcEngineEventHandler: AgoraRtcEngineDelegate { callback(RtcEngineEvents.LocalAudioStateChanged, state.rawValue, error.rawValue) } + public func rtcEngine(_ engine: AgoraRtcEngineKit, didRequest info: AgoraRtcAudioFileInfo, error: AgoraAudioFileInfoError) { + callback(RtcEngineEvents.RequestAudioFileInfo, info.toMap(), error.rawValue) + } + public func rtcEngine(_: AgoraRtcEngineKit, didLocalPublishFallbackToAudioOnly isFallbackOrRecover: Bool) { callback(RtcEngineEvents.LocalPublishFallbackToAudioOnly, isFallbackOrRecover) } @@ -318,7 +326,7 @@ extension RtcEngineEventHandler: AgoraRtcEngineDelegate { callback(RtcEngineEvents.CameraExposureAreaChanged, rect.toMap()) } - func rtcEngine(_: AgoraRtcEngineKit, facePositionDidChangeWidth width: Int32, previewHeight height: Int32, faces: [AgoraFacePositionInfo]?) { + public func rtcEngine(_: AgoraRtcEngineKit, facePositionDidChangeWidth width: Int32, previewHeight height: Int32, faces: [AgoraFacePositionInfo]?) { callback(RtcEngineEvents.FacePositionChanged, width, height, faces?.toMapList()) } @@ -358,7 +366,7 @@ extension RtcEngineEventHandler: AgoraRtcEngineDelegate { callback(RtcEngineEvents.AudioMixingFinished) } - func rtcEngine(_: AgoraRtcEngineKit, localAudioMixingStateDidChanged state: AgoraAudioMixingStateCode, reason: AgoraAudioMixingReasonCode) { + public func rtcEngine(_: AgoraRtcEngineKit, localAudioMixingStateDidChanged state: AgoraAudioMixingStateCode, reason: AgoraAudioMixingReasonCode) { callback(RtcEngineEvents.AudioMixingStateChanged, state.rawValue, reason.rawValue) } @@ -478,47 +486,51 @@ extension RtcEngineEventHandler: AgoraRtcEngineDelegate { callback(RtcEngineEvents.VideoStopped) } - func rtcEngine(_: AgoraRtcEngineKit, firstLocalAudioFramePublished elapsed: Int) { + public func rtcEngine(_: AgoraRtcEngineKit, firstLocalAudioFramePublished elapsed: Int) { callback(RtcEngineEvents.FirstLocalAudioFramePublished, elapsed) } - func rtcEngine(_: AgoraRtcEngineKit, firstLocalVideoFramePublished elapsed: Int) { + public func rtcEngine(_: AgoraRtcEngineKit, firstLocalVideoFramePublished elapsed: Int) { callback(RtcEngineEvents.FirstLocalVideoFramePublished, elapsed) } - func rtcEngine(_: AgoraRtcEngineKit, didAudioPublishStateChange channel: String, oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int) { + public func rtcEngine(_: AgoraRtcEngineKit, didAudioPublishStateChange channel: String, oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int) { callback(RtcEngineEvents.AudioPublishStateChanged, channel, oldState.rawValue, newState.rawValue, elapseSinceLastState) } - func rtcEngine(_: AgoraRtcEngineKit, didVideoPublishStateChange channel: String, oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int) { + public func rtcEngine(_: AgoraRtcEngineKit, didVideoPublishStateChange channel: String, oldState: AgoraStreamPublishState, newState: AgoraStreamPublishState, elapseSinceLastState: Int) { callback(RtcEngineEvents.VideoPublishStateChanged, channel, oldState.rawValue, newState.rawValue, elapseSinceLastState) } - func rtcEngine(_: AgoraRtcEngineKit, didAudioSubscribeStateChange channel: String, withUid uid: UInt, oldState: AgoraStreamSubscribeState, newState: AgoraStreamSubscribeState, elapseSinceLastState: Int) { + public func rtcEngine(_: AgoraRtcEngineKit, didAudioSubscribeStateChange channel: String, withUid uid: UInt, oldState: AgoraStreamSubscribeState, newState: AgoraStreamSubscribeState, elapseSinceLastState: Int) { callback(RtcEngineEvents.AudioSubscribeStateChanged, channel, uid, oldState.rawValue, newState.rawValue, elapseSinceLastState) } - func rtcEngine(_: AgoraRtcEngineKit, didVideoSubscribeStateChange channel: String, withUid uid: UInt, oldState: AgoraStreamSubscribeState, newState: AgoraStreamSubscribeState, elapseSinceLastState: Int) { + public func rtcEngine(_: AgoraRtcEngineKit, didVideoSubscribeStateChange channel: String, withUid uid: UInt, oldState: AgoraStreamSubscribeState, newState: AgoraStreamSubscribeState, elapseSinceLastState: Int) { callback(RtcEngineEvents.VideoSubscribeStateChanged, channel, uid, oldState.rawValue, newState.rawValue, elapseSinceLastState) } - func rtcEngine(_: AgoraRtcEngineKit, rtmpStreamingEventWithUrl url: String, eventCode: AgoraRtmpStreamingEvent) { + public func rtcEngine(_: AgoraRtcEngineKit, rtmpStreamingEventWithUrl url: String, eventCode: AgoraRtmpStreamingEvent) { callback(RtcEngineEvents.RtmpStreamingEvent, url, eventCode.rawValue) } - func rtcEngine(_: AgoraRtcEngineKit, superResolutionEnabledOfUid uid: UInt, enabled: Bool, reason: AgoraSuperResolutionStateReason) { + public func rtcEngine(_: AgoraRtcEngineKit, superResolutionEnabledOfUid uid: UInt, enabled: Bool, reason: AgoraSuperResolutionStateReason) { callback(RtcEngineEvents.UserSuperResolutionEnabled, uid, enabled, reason.rawValue) } - func rtcEngine(_: AgoraRtcEngineKit, uploadLogResultRequestId requestId: String, success: Bool, reason: AgoraUploadErrorReason) { + public func rtcEngine(_: AgoraRtcEngineKit, uploadLogResultRequestId requestId: String, success: Bool, reason: AgoraUploadErrorReason) { callback(RtcEngineEvents.UploadLogResult, requestId, success, reason.rawValue) } - func rtcEngineAirPlayIsConnected(_ engine: AgoraRtcEngineKit) { + public func rtcEngineAirPlayIsConnected(_ engine: AgoraRtcEngineKit) { callback(RtcEngineEvents.AirPlayIsConnected) } - func rtcEngine(_ engine: AgoraRtcEngineKit, virtualBackgroundSourceEnabled enabled: Bool, reason: AgoraVirtualBackgroundSourceStateReason) { + public func rtcEngine(_ engine: AgoraRtcEngineKit, virtualBackgroundSourceEnabled enabled: Bool, reason: AgoraVirtualBackgroundSourceStateReason) { callback(RtcEngineEvents.VirtualBackgroundSourceEnabled, enabled, reason.rawValue) } + + public func rtcEngine(_ engine: AgoraRtcEngineKit, snapshotTaken channel: String, uid: UInt, filePath: String, width: Int, height: Int, errCode: Int) { + callback(RtcEngineEvents.SnapshotTaken, channel, uid, filePath, width, height, errCode) + } } diff --git a/ios/RCTAgora/Base/RtcEnginePlugin.h b/ios/RCTAgora/Base/RtcEnginePlugin.h new file mode 100644 index 000000000..93d89eef8 --- /dev/null +++ b/ios/RCTAgora/Base/RtcEnginePlugin.h @@ -0,0 +1,31 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A `RtcEnginePlugin` allows developers to interact with the `RtcEngine` which created from flutter + * side. + */ +@protocol RtcEnginePlugin + +/** + * This callback will be called when the `RtcEngine` is created by + [RtcEngine.createWithContext](https://docs.agora.io/cn/Video/API%20Reference/flutter/agora_rtc_engine/RtcEngine/createWithContext.html) + * function from flutter. + + * NOTE that you should not call `AgoraRtcEngineKit.destroy`, because it will also destroy the `RtcEngine` + * used by flutter side. + * + * @param rtcEngine The same `AgoraRtcEngineKit` used by flutter side + */ +- (void)onRtcEngineCreated:(AgoraRtcEngineKit *_Nullable)rtcEngine; + +/** + * This callback will be called when the [RtcEngine.destroy](https://docs.agora.io/cn/Video/API%20Reference/flutter/v4.0.7/rtc_channel/RtcChannel/destroy.html) + * function is called from flutter. + */ +- (void)onRtcEngineDestroyed; +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RCTAgora/Base/RtcEnginePluginRegistrant.h b/ios/RCTAgora/Base/RtcEnginePluginRegistrant.h new file mode 100644 index 000000000..27dc5e688 --- /dev/null +++ b/ios/RCTAgora/Base/RtcEnginePluginRegistrant.h @@ -0,0 +1,25 @@ +#ifndef RtcEnginePluginRegistrant_h +#define RtcEnginePluginRegistrant_h + +#import +#import "RtcEnginePlugin.h" + +/** + * Class for register the `RtcEnginePlugin`. + */ +@interface RtcEnginePluginRegistrant : NSObject + +/** + * Register a `RtcEnginePlugin`. The `plugin` will be called when the `RtcEngine` is created from + * flutter side. + */ ++(void)register:(NSObject *_Nonnull)plugin; + +/** + * Unregister a previously registered `RtcEnginePlugin`. + */ ++(void)unregister:(NSObject *_Nonnull)plugin; + +@end + +#endif /* RtcEnginePluginRegistrant_h */ diff --git a/ios/RCTAgora/Base/RtcEnginePluginRegistrantSwift.swift b/ios/RCTAgora/Base/RtcEnginePluginRegistrantSwift.swift new file mode 100644 index 000000000..d6ef15334 --- /dev/null +++ b/ios/RCTAgora/Base/RtcEnginePluginRegistrantSwift.swift @@ -0,0 +1,12 @@ +import Foundation + +@objc(RtcEnginePluginRegistrant) +class RtcEnginePluginRegistrantSwift: NSObject { + @objc public static func register(_ plugin: RtcEnginePlugin) { + RtcEngineRegistry.shared.add(plugin) + } + + @objc public static func unregister(_ plugin: RtcEnginePlugin) { + RtcEngineRegistry.shared.remove(plugin) + } +} diff --git a/ios/RCTAgora/Base/RtcEngineRegistry.swift b/ios/RCTAgora/Base/RtcEngineRegistry.swift new file mode 100644 index 000000000..ca4c7ebd9 --- /dev/null +++ b/ios/RCTAgora/Base/RtcEngineRegistry.swift @@ -0,0 +1,61 @@ +import Foundation +import AgoraRtcKit + +fileprivate struct PluginKey: Hashable, Equatable { + let type: AnyClass + + public static func == (lhs: PluginKey, rhs: PluginKey) -> Bool { + return lhs.type == rhs.type + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(type)) + } +} + +/** + * The `RtcEngineRegistry` is response to add, remove and notify the callback when `RtcEngine` is created + * from flutter side. + */ +internal class RtcEngineRegistry : NSObject { + + private override init() {} + + static let shared = RtcEngineRegistry() + + private var plugins: [PluginKey : RtcEnginePlugin] = [PluginKey: RtcEnginePlugin]() + + /** + * Add a `RtcEnginePlugin`. + */ + func add(_ plugin: RtcEnginePlugin) { + let key = PluginKey(type: type(of: plugin)) + guard plugins[key] == nil else { + return + } + plugins[key] = plugin + } + + /** + * Remove the previously added `RtcEnginePlugin`. + */ + func remove(_ plugin: RtcEnginePlugin) { + plugins.removeValue(forKey: PluginKey(type: type(of: plugin))) + } +} + +extension RtcEngineRegistry : RtcEnginePlugin { + // MARK: - protocol from RtcEnginePlugin + public func onRtcEngineCreated(_ rtcEngine: AgoraRtcEngineKit?) { + for (_, plugin) in plugins { + plugin.onRtcEngineCreated(rtcEngine) + } + } + + // MARK: - protocol from RtcEnginePlugin + public func onRtcEngineDestroyed() { + for (_, plugin) in plugins { + plugin.onRtcEngineDestroyed() + } + } +} diff --git a/ios/RCTAgora/Base/RtcSurfaceView.swift b/ios/RCTAgora/Base/RtcSurfaceView.swift index 4fa9e85b9..1cb7f72f9 100644 --- a/ios/RCTAgora/Base/RtcSurfaceView.swift +++ b/ios/RCTAgora/Base/RtcSurfaceView.swift @@ -1,11 +1,3 @@ -// -// RtcSurfaceView.swift -// RCTAgora -// -// Created by LXH on 2020/4/15. -// Copyright © 2020 Syan. All rights reserved. -// - import AgoraRtcKit import Foundation import UIKit diff --git a/react-native-agora.podspec b/react-native-agora.podspec index 785482935..dfe78a246 100644 --- a/react-native-agora.podspec +++ b/react-native-agora.podspec @@ -20,5 +20,5 @@ Pod::Spec.new do |s| s.swift_version = "4.0" s.dependency "React" - s.dependency "AgoraRtcEngine_iOS", "3.5.0.4" + s.dependency "AgoraRtcEngine_iOS", "3.5.2" end diff --git a/src/common/Classes.ts b/src/common/Classes.ts index bef9998c0..2bb9adcbc 100644 --- a/src/common/Classes.ts +++ b/src/common/Classes.ts @@ -24,6 +24,7 @@ import type { VideoOutputOrientationMode, VideoQualityAdaptIndication, VideoStreamType, + VirtualBackgroundBlurDegree, VirtualBackgroundSourceType, } from './Enums'; @@ -1721,6 +1722,14 @@ export class AudioRecordingConfiguration { * */ recordingSampleRate?: AudioSampleRateType; + /** + * The degree of blurring applied to the custom background image. See #BACKGROUND_BLUR_DEGREE. + * + * @note This parameter takes effect only when the type of the custom background image is `BACKGROUND_BLUR`. + * + * @since v3.5.1 + */ + blur_degree?: VirtualBackgroundBlurDegree; constructor( filePath: string, @@ -1779,3 +1788,66 @@ export class VirtualBackgroundSource { } } } + +/** + * The information of an audio file. This struct is reported + * in \ref IRtcEngineEventHandler::onRequestAudioFileInfo "onRequestAudioFileInfo". + * + * @since v3.5.1 + */ +export interface AudioFileInfo { + /** The file path. + */ + filePath: string; + /** The file duration (ms). + */ + durationMs: number; +} + +/** + * The configuration of the audio and video call loop test. + * + * @since v3.5.2 + */ +export class EchoTestConfiguration { + /** + * Whether to enable the audio device for the call loop test: + * - true: (Default) Enables the audio device. To test the audio device, set this parameter as `true`. + * - false: Disables the audio device. + */ + enableAudio?: boolean; + /** + * Whether to enable the video device for the call loop test: + * - true: (Default) Enables the video device. To test the video device, set this parameter as `true`. + * - false: Disables the video device. + */ + enableVideo?: boolean; + /** + * The token used to secure the audio and video call loop test. If you do not enable App Certificate in Agora + * Console, you do not need to pass a value in this parameter; if you have enabled App Certificate in Agora Console, + * you must pass a token in this parameter, the `uid` used when you generate the token must be 0xFFFFFFFF, and the + * channel name used must be the channel name that identifies each audio and video call loop tested. For server-side + * token generation, see [Authenticate Your Users with Tokens](https://docs.agora.io/en/Interactive%20Broadcast/token_server?platform=All%20Platforms). + */ + token?: string; + /** + * The channel name that identifies each audio and video call loop. To ensure proper loop test functionality, the + * channel name passed in to identify each loop test cannot be the same when users of the same project (App ID) + * perform audio and video call loop tests on different devices. + */ + channelId?: string; + + constructor(params?: { + enableAudio?: boolean; + enableVideo?: boolean; + token?: string; + channelId?: string; + }) { + if (params) { + this.enableAudio = params.enableAudio; + this.enableVideo = params.enableVideo; + this.token = params.token; + this.channelId = params.channelId; + } + } +} diff --git a/src/common/Enums.ts b/src/common/Enums.ts index 805de56f2..132921629 100644 --- a/src/common/Enums.ts +++ b/src/common/Enums.ts @@ -1704,6 +1704,10 @@ export enum NetworkType { * 5: The network type is mobile 4G. */ Mobile4G = 5, + /** + * 6: The network type is mobile 5G. + */ + Mobile5G = 6, } /** @@ -3071,6 +3075,10 @@ export enum VirtualBackgroundSourceType { * The background image is a file in PNG or JPG format. */ Img = 2, + /** + * The degree of blurring applied to the custom background image. + */ + Blur = 3, } /** @@ -3094,3 +3102,70 @@ export enum VirtualBackgroundSourceStateReason { */ DeviceNotSupported = 3, } + +/** The information acquisition state. This enum is reported + * in \ref IRtcEngineEventHandler::onRequestAudioFileInfo "onRequestAudioFileInfo". + * + * @since v3.5.1 + */ +export enum AudioFileInfoError { + /** 0: Successfully get the information of an audio file. + */ + Ok = 0, + /** 1: Fail to get the information of an audio file. + */ + Failure = 1, +} + +/** + * The channel mode. Set in \ref agora::rtc::IRtcEngine::setAudioMixingDualMonoMode "setAudioMixingDualMonoMode". + * + * @since v3.5.1 + */ +export enum AudioMixingDualMonoMode { + /** + * 0: Original mode. + */ + AUTO = 0, + /** + * 1: Left channel mode. This mode replaces the audio of the right channel + * with the audio of the left channel, which means the user can only hear + * the audio of the left channel. + */ + L = 1, + /** + * 2: Right channel mode. This mode replaces the audio of the left channel with + * the audio of the right channel, which means the user can only hear the audio + * of the right channel. + */ + R = 2, + /** + * 3: Mixed channel mode. This mode mixes the audio of the left channel and + * the right channel, which means the user can hear the audio of the left + * channel and the right channel at the same time. + */ + MIX = 3, +} + +/** + * The degree of blurring applied to the custom background image. + * + * @since v3.5.1 + */ +export enum VirtualBackgroundBlurDegree { + /** + * 1: The degree of blurring applied to the custom background image is low. + * The user can almost see the background clearly. + */ + Low = 1, + /** + * The degree of blurring applied to the custom background image is medium. + * It is difficult for the user to recognize details in the background. + */ + Medium = 2, + /** + * (Default) The degree of blurring applied to the custom background image is high. + * The user can barely see any distinguishing features in the background. + */ + High = 3, +} diff --git a/src/common/RtcChannel.native.ts b/src/common/RtcChannel.native.ts index 834286a91..f3f4c4ac7 100644 --- a/src/common/RtcChannel.native.ts +++ b/src/common/RtcChannel.native.ts @@ -1109,10 +1109,46 @@ export default class RtcChannel implements RtcChannelInterface { return this._callMethod('muteLocalVideoStream', { muted }); } + /** + * Pauses the media stream relay to all destination channels. + * + * @since v3.5.1 + * + * After the cross-channel media stream relay starts, you can call this method + * to pause relaying media streams to all destination channels; after the pause, + * if you want to resume the relay, call \ref IChannel::resumeAllChannelMediaRelay "resumeAllChannelMediaRelay". + * + * After a successful method call, the SDK triggers the + * \ref IChannelEventHandler::onChannelMediaRelayEvent "onChannelMediaRelayEvent" + * callback to report whether the media stream relay is successfully paused. + * + * @note Call this method after the \ref IChannel::startChannelMediaRelay "startChannelMediaRelay" method. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ pauseAllChannelMediaRelay(): Promise { return this._callMethod('pauseAllChannelMediaRelay'); } + /** Resumes the media stream relay to all destination channels. + * + * @since v3.5.1 + * + * After calling the \ref IChannel::pauseAllChannelMediaRelay "pauseAllChannelMediaRelay" method, + * you can call this method to resume relaying media streams to all destination channels. + * + * After a successful method call, the SDK triggers the + * \ref IChannelEventHandler::onChannelMediaRelayEvent "onChannelMediaRelayEvent" + * callback to report whether the media stream relay is successfully resumed. + * + * @note Call this method after the \ref IChannel::pauseAllChannelMediaRelay "pauseAllChannelMediaRelay" method. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ resumeAllChannelMediaRelay(): Promise { return this._callMethod('resumeAllChannelMediaRelay'); } diff --git a/src/common/RtcEngine.native.ts b/src/common/RtcEngine.native.ts index 97b552515..3867db0c8 100644 --- a/src/common/RtcEngine.native.ts +++ b/src/common/RtcEngine.native.ts @@ -12,6 +12,7 @@ import { ChannelMediaRelayConfiguration, ClientRoleOptions, DataStreamConfig, + EchoTestConfiguration, EncryptionConfig, LastmileProbeConfig, LiveInjectStreamConfig, @@ -47,6 +48,7 @@ import type { VideoStreamType, VoiceBeautifierPreset, VoiceConversionPreset, + AudioMixingDualMonoMode, } from './Enums'; import type { Listener, RtcEngineEvents, Subscription } from './RtcEvents'; import RtcChannel from './RtcChannel.native'; @@ -3404,14 +3406,44 @@ export default class RtcEngine implements RtcEngineInterface { } /** - * @ignore + * Pauses the media stream relay to all destination channels. + * + * @since v3.5.1 + * + * After the cross-channel media stream relay starts, you can call this method + * to pause relaying media streams to all destination channels; after the pause, + * if you want to resume the relay, call \ref IRtcEngine::resumeAllChannelMediaRelay "resumeAllChannelMediaRelay". + * + * After a successful method call, the SDK triggers the + * \ref IRtcEngineEventHandler::onChannelMediaRelayEvent "onChannelMediaRelayEvent" + * callback to report whether the media stream relay is successfully paused. + * + * @note Call this method after the \ref IRtcEngine::startChannelMediaRelay "startChannelMediaRelay" method. + * + * @return + * - 0: Success. + * - < 0: Failure. */ pauseAllChannelMediaRelay(): Promise { return RtcEngine._callMethod('pauseAllChannelMediaRelay'); } - /** - * @ignore + /** Resumes the media stream relay to all destination channels. + * + * @since v3.5.1 + * + * After calling the \ref IRtcEngine::pauseAllChannelMediaRelay "pauseAllChannelMediaRelay" method, + * you can call this method to resume relaying media streams to all destination channels. + * + * After a successful method call, the SDK triggers the + * \ref IRtcEngineEventHandler::onChannelMediaRelayEvent "onChannelMediaRelayEvent" + * callback to report whether the media stream relay is successfully resumed. + * + * @note Call this method after the \ref IRtcEngine::pauseAllChannelMediaRelay "pauseAllChannelMediaRelay" method. + * + * @return + * - 0: Success. + * - < 0: Failure. */ resumeAllChannelMediaRelay(): Promise { return RtcEngine._callMethod('resumeAllChannelMediaRelay'); @@ -3461,6 +3493,179 @@ export default class RtcEngine implements RtcEngineInterface { backgroundSource, }); } + + /** Gets the information of a specified audio file. + * + * @since v3.5.1 + * + * After calling this method successfully, the SDK triggers the + * \ref IRtcEngineEventHandler::onRequestAudioFileInfo "onRequestAudioFileInfo" + * callback to report the information of an audio file, such as audio duration. + * You can call this method multiple times to get the information of multiple audio files. + * + * @note + * - Call this method after joining a channel. + * - For the audio file formats supported by this method, see [What formats of audio files does the Agora RTC SDK support](https://docs.agora.io/en/faq/audio_format). + * + * @param filePath The file path: + * - Windows: The absolute path or URL address (including the filename extensions) of + * the audio file. For example: `C:\music\audio.mp4`. + * - Android: The file path, including the filename extensions. To access an online file, + * Agora supports using a URL address; to access a local file, Agora supports using a URI + * address, an absolute path, or a path that starts with `/assets/`. You might encounter + * permission issues if you use an absolute path to access a local file, so Agora recommends + * using a URI address instead. For example: `content://com.android.providers.media.documents/document/audio%3A14441`. + * - iOS or macOS: The absolute path or URL address (including the filename extensions) of the audio file. + * For example: `/var/mobile/Containers/Data/audio.mp4`. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + getAudioFileInfo(filePath: string): Promise { + return RtcEngine._callMethod('getAudioFileInfo', { + filePath, + }); + } + + /** + * Gets the audio track index of the current music file. + * + * @since v3.5.1 + * + * @note + * - This method is for Android, iOS, and Windows only. + * - Call this method after calling \ref IRtcEngine::startAudioMixing(const char*,bool,bool,int,int) "startAudioMixing" [2/2] + * and receiving the \ref IRtcEngineEventHandler::onAudioMixingStateChanged "onAudioMixingStateChanged" (AUDIO_MIXING_STATE_PLAYING) callback. + * - For the audio file formats supported by this method, see [What formats of audio files does the Agora RTC SDK support](https://docs.agora.io/en/faq/audio_format). + * + * @return + * - ≥ 0: The audio track index of the current music file, if this method call succeeds. + * - < 0: Failure. + */ + getAudioTrackCount(): Promise { + return RtcEngine._callMethod('getAudioTrackCount'); + } + + /** + * Specifies the playback track of the current music file. + * + * @since v3.5.1 + * + * After getting the audio track index of the current music file, call this + * method to specify any audio track to play. For example, if different tracks + * of a multitrack file store songs in different languages, you can call this + * method to set the language of the music file to play. + * + * @note + * - This method is for Android, iOS, and Windows only. + * - Call this method after calling \ref IRtcEngine::startAudioMixing(const char*,bool,bool,int,int) "startAudioMixing" [2/2] + * and receiving the \ref IRtcEngineEventHandler::onAudioMixingStateChanged "onAudioMixingStateChanged" (AUDIO_MIXING_STATE_PLAYING) callback. + * - For the audio file formats supported by this method, see [What formats of audio files does the Agora RTC SDK support](https://docs.agora.io/en/faq/audio_format). + * + * @param index The specified playback track. This parameter must be less than or equal to the return value + * of \ref IRtcEngine::getAudioTrackCount "getAudioTrackCount". + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + selectAudioTrack(index: number): Promise { + return RtcEngine._callMethod('selectAudioTrack', { + index, + }); + } + + /** + * Sets the channel mode of the current music file. + * + * @since v3.5.1 + * + * In a stereo music file, the left and right channels can store different audio data. + * According to your needs, you can set the channel mode to original mode, left channel mode, + * right channel mode, or mixed channel mode. For example, in the KTV scenario, the left + * channel of the music file stores the musical accompaniment, and the right channel + * stores the singing voice. If you only need to listen to the accompaniment, call this + * method to set the channel mode of the music file to left channel mode; if you need to + * listen to the accompaniment and the singing voice at the same time, call this method + * to set the channel mode to mixed channel mode. + * + * @note + * - Call this method after calling \ref IRtcEngine::startAudioMixing(const char*,bool,bool,int,int) "startAudioMixing" [2/2] + * and receiving the \ref IRtcEngineEventHandler::onAudioMixingStateChanged "onAudioMixingStateChanged" (AUDIO_MIXING_STATE_PLAYING) callback. + * - This method only applies to stereo audio files. + * + * @param mode The channel mode. See \ref agora::media::AUDIO_MIXING_DUAL_MONO_MODE "AUDIO_MIXING_DUAL_MONO_MODE". + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + setAudioMixingDualMonoMode(mode: AudioMixingDualMonoMode): Promise { + return RtcEngine._callMethod('setAudioMixingDualMonoMode', { + mode, + }); + } + + /** + * Sets the playback speed of the current music file. + * + * @since v3.5.1 + * + * @note Call this method after calling \ref IRtcEngine::startAudioMixing(const char*,bool,bool,int,int) "startAudioMixing" [2/2] + * and receiving the \ref IRtcEngineEventHandler::onAudioMixingStateChanged "onAudioMixingStateChanged" (AUDIO_MIXING_STATE_PLAYING) callback. + * + * @param speed The playback speed. Agora recommends that you limit this value to between 50 and 400, defined as follows: + * - 50: Half the original speed. + * - 100: The original speed. + * - 400: 4 times the original speed. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + setAudioMixingPlaybackSpeed(speed: number): Promise { + return RtcEngine._callMethod('setAudioMixingPlaybackSpeed', { + speed, + }); + } + + /** + * Takes a snapshot of a video stream. + * + * @since v3.5.2 + * + * This method takes a snapshot of a video stream from the specified user, generates a JPG image, + * and saves it to the specified path. + * + * The method is asynchronous, and the SDK has not taken the snapshot when the method call returns. + * After a successful method call, the SDK triggers the \ref IRtcEngineEventHandler::onSnapshotTaken "onSnapshotTaken" + * callback to report whether the snapshot is successfully taken as well as the details of the snapshot taken. + * + * @note + * - Call this method after joining a channel. + * - If the video of the specified user is pre-processed, for example, added with watermarks or image enhancement + * effects, the generated snapshot also includes the pre-processing effects. + * + * @param channel The channel name. + * @param uid The user ID of the user. Set `uid` as 0 if you want to take a snapshot of the local user's video. + * @param filePath The local path (including the filename extensions) of the snapshot. For example, + * `C:\Users\\AppData\Local\Agora\\example.jpg` on Windows, + * `/App Sandbox/Library/Caches/example.jpg` on iOS, `~/Library/Logs/example.jpg` on macOS, and + * `/storage/emulated/0/Android/data//files/example.jpg` on Android. Ensure that the path you specify + * exists and is writable. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + takeSnapshot(channel: string, uid: number, filePath: string): Promise { + return RtcEngine._callMethod('takeSnapshot', { + channel, + uid, + filePath, + }); + } } /** @@ -3552,6 +3757,8 @@ interface RtcEngineInterface enabled: boolean, backgroundSource: VirtualBackgroundSource ): Promise; + + takeSnapshot(channel: string, uid: number, filePath: string): Promise; } /** @@ -3677,6 +3884,16 @@ interface RtcAudioMixingInterface { setAudioMixingPosition(pos: number): Promise; setAudioMixingPitch(pitch: number): Promise; + + getAudioFileInfo(filePath: string): Promise; + + setAudioMixingPlaybackSpeed(speed: number): Promise; + + getAudioTrackCount(): Promise; + + selectAudioTrack(audioIndex: number): Promise; + + setAudioMixingDualMonoMode(mode: AudioMixingDualMonoMode): Promise; } /** @@ -3853,7 +4070,10 @@ interface RtcFallbackInterface { * @ignore */ interface RtcTestInterface { - startEchoTest(intervalInSeconds: number): Promise; + startEchoTest( + intervalInSeconds?: number, + config?: EchoTestConfiguration + ): Promise; stopEchoTest(): Promise; diff --git a/src/common/RtcEvents.ts b/src/common/RtcEvents.ts index fd1caeed0..fc94ad134 100644 --- a/src/common/RtcEvents.ts +++ b/src/common/RtcEvents.ts @@ -1,4 +1,5 @@ import type { + AudioFileInfo, AudioVolumeInfo, FacePositionInfo, LastmileProbeResult, @@ -11,6 +12,7 @@ import type { UserInfo, } from './Classes'; import type { + AudioFileInfoError, AudioLocalError, AudioLocalState, AudioMixingReason, @@ -517,6 +519,34 @@ export type VirtualBackgroundSourceEnabledCallback = * @param reason The reason why the virtual background is not successfully enabled or the message that confirms success. See [`VirtualBackgroundSourceStateReason`]{@link VirtualBackgroundSourceStateReason}. */ (enabled: boolean, reason: VirtualBackgroundSourceStateReason) => void; +export type RequestAudioFileInfoCallback = + /** + * @param info The information of an audio file. See AudioFileInfo. + * @param error The information acquisition state. See #AUDIO_FILE_INFO_ERROR. + */ + (info: AudioFileInfo, error: AudioFileInfoError) => void; +export type SnapshotTakenCallback = + /** + * @param channel The channel name. + * @param uid The user ID of the user. A `uid` of 0 indicates the local user. + * @param filePath The local path of the snapshot. + * @param width The width (px) of the snapshot. + * @param height The height (px) of the snapshot. + * @param errCode The message that confirms success or the reason why the snapshot is not successfully taken: + * - `0`: Success. + * - < 0: Failure: + * - `-1`: The SDK fails to write data to a file or encode a JPEG image. + * - `-2`: The SDK does not find the video stream of the specified user within one second after + * the \ref IRtcEngine::takeSnapshot "takeSnapshot" method call succeeds. + */ + ( + channel: string, + uid: number, + filePath: string, + width: number, + height: number, + errCode: number + ) => void; /** * Callbacks. @@ -1421,6 +1451,8 @@ export interface RtcEngineEvents { * @since v3.3.1 (later) * * After calling `enableRemoteSuperResolution`, the SDK triggers this callback to report whether the super-resolution algorithm is successfully enabled. If not successfully enabled, you can use reason for troubleshooting. + * + * @event UserSuperResolutionEnabled */ UserSuperResolutionEnabled: UserSuperResolutionEnabledCallback; @@ -1432,8 +1464,34 @@ export interface RtcEngineEvents { * @since v3.3.1 (later) * * After the method call of `uploadLogFile`, the SDK triggers this callback to report the result of uploading the log files. If the upload fails, refer to the `reason` parameter to troubleshoot. + * + * @event UploadLogResultCallback */ UploadLogResult: UploadLogResultCallback; + + /** + * Reports the information of an audio file. + * + * @since v3.5.1 + * + * After successfully calling \ref IRtcEngine::getAudioFileInfo "getAudioFileInfo", the SDK triggers this + * callback to report the information of the audio file, such as the file path and duration. + * + * @event UploadLogResultCallback + */ + RequestAudioFileInfo: RequestAudioFileInfoCallback; + + /** + * Reports the result of taking a video snapshot. + * + * @since v3.5.2 + * + * After a successful \ref IRtcEngine::takeSnapshot "takeSnapshot" method call, the SDK triggers this callback to + * report whether the snapshot is successfully taken as well as the details for the snapshot taken. + * + * @event SnapshotTaken + */ + SnapshotTaken: SnapshotTakenCallback; } /**