diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj b/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj index 5b4dfb74c..fbb660d8b 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj @@ -131,6 +131,8 @@ F728BA222CA93EF9007813BB /* VoiceChangerRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728BA212CA93EF9007813BB /* VoiceChangerRTC.swift */; }; F728BA2E2CB53E04007813BB /* RTMPStreamRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728BA2D2CB53E04007813BB /* RTMPStreamRTC.swift */; }; F728BA302CB53E13007813BB /* RTMPStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728BA2F2CB53E13007813BB /* RTMPStream.swift */; }; + F728BA352CB6576F007813BB /* CustomAudioRenderRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728BA342CB6576F007813BB /* CustomAudioRenderRTC.swift */; }; + F728BA372CB6577E007813BB /* CustomAudioRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728BA362CB6577E007813BB /* CustomAudioRender.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -316,6 +318,8 @@ F728BA212CA93EF9007813BB /* VoiceChangerRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceChangerRTC.swift; sourceTree = ""; }; F728BA2D2CB53E04007813BB /* RTMPStreamRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTMPStreamRTC.swift; sourceTree = ""; }; F728BA2F2CB53E13007813BB /* RTMPStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTMPStream.swift; sourceTree = ""; }; + F728BA342CB6576F007813BB /* CustomAudioRenderRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAudioRenderRTC.swift; sourceTree = ""; }; + F728BA362CB6577E007813BB /* CustomAudioRender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAudioRender.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -476,6 +480,7 @@ E73F240E2BA82D4B0000B523 /* Advanced */ = { isa = PBXGroup; children = ( + F728BA332CB65703007813BB /* CustomAudioRender */, F728BA1E2CA93EDA007813BB /* VoiceChanger */, F728BA162CA9398E007813BB /* RTMPStream */, F728BA112CA90732007813BB /* LocalVideoTranscoding */, @@ -918,6 +923,15 @@ path = VoiceChanger; sourceTree = ""; }; + F728BA332CB65703007813BB /* CustomAudioRender */ = { + isa = PBXGroup; + children = ( + F728BA342CB6576F007813BB /* CustomAudioRenderRTC.swift */, + F728BA362CB6577E007813BB /* CustomAudioRender.swift */, + ); + path = CustomAudioRender; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1132,6 +1146,7 @@ F728B9F32CA3FB12007813BB /* LiveStreamingRTC.swift in Sources */, E71D54722BCD270800656537 /* StreamEncryptionRTC.swift in Sources */, E73F248D2BA851870000B523 /* KFMP4Demuxer.m in Sources */, + F728BA352CB6576F007813BB /* CustomAudioRenderRTC.swift in Sources */, E73F24F02BB6645C0000B523 /* RhythmPlayer.swift in Sources */, E73F24882BA851870000B523 /* NetworkManager.swift in Sources */, E73F24AD2BAAC6E70000B523 /* JoinChannelVideoRecorder.swift in Sources */, @@ -1160,6 +1175,7 @@ E73F248A2BA851870000B523 /* ARVideoRenderer.swift in Sources */, E71D547C2BCE1EB900656537 /* QuickSwitchChannelRTC.swift in Sources */, E71D546E2BCD176B00656537 /* AudioMixing.swift in Sources */, + F728BA372CB6577E007813BB /* CustomAudioRender.swift in Sources */, E73F24AE2BAAC6E70000B523 /* JoinChannelVideoRecorderRTC.swift in Sources */, E79DFB622BBA545300904B08 /* PickerView.swift in Sources */, F728B9F12CA3FAFE007813BB /* LiveStreaming.swift in Sources */, diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift index a48e4e3fd..3669a9a17 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift @@ -52,6 +52,8 @@ struct ContentView: View { view: AnyView(VoiceChangerEntry())), MenuItem(name: "RTMP Streaming".localized, view: AnyView(RTMPStreamEntry())), + MenuItem(name: "Custom Audio Render".localized, + view: AnyView(CustomAudioRenderEntry())), MenuItem(name: "Picture In Picture".localized, view: AnyView(PictureInPictureEntry())), MenuItem(name: "Quick Switch Channel".localized, diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift new file mode 100644 index 000000000..a75115efa --- /dev/null +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift @@ -0,0 +1,68 @@ +// +// CustomAudioRender.swift +// APIExample-SwiftUI +// +// Created by qinhui on 2024/10/9. +// + +import SwiftUI + +struct CustomAudioRenderEntry: View { + @State private var channelName: String = "" + @State private var isActive = false + @State private var configs: [String: Any] = [:] + + var body: some View { + VStack { + Spacer() + TextField("Enter channel name".localized, text: $channelName).textFieldStyle(.roundedBorder).padding() + Button { + configs = ["channelName": channelName] + self.isActive = true + } label: { + Text("Join".localized) + }.disabled(channelName.isEmpty) + Spacer() + NavigationLink(destination: CustomAudioRender(configs: configs).navigationTitle(channelName).navigationBarTitleDisplayMode(.inline), isActive: $isActive) { + EmptyView() + } + Spacer() + } + .navigationBarTitleDisplayMode(.inline) + } +} + +struct CustomAudioRender: View { + @State var configs: [String: Any] = [:] + @ObservedObject private var agoraKit = CustomAudioRenderRTC() + + var body: some View { + VStack { + GeometryReader { geometry in + ScrollView { + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]) { + ForEach(agoraKit.audioInfos, id: \.self) { info in + VStack { + Text(info.content) + .font(.system(size: 12)) + .foregroundColor(.gray) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(.gray.opacity(0.5)) + } + .frame(width: geometry.size.width / 3, height: 200) + } + } + } + } + } + .onAppear(perform: { + agoraKit.setupRTC(configs: configs) + }).onDisappear(perform: { + agoraKit.onDestory() + }) + } +} + +#Preview { + CustomAudioRender() +} diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRenderRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRenderRTC.swift new file mode 100644 index 000000000..ef03afdfc --- /dev/null +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRenderRTC.swift @@ -0,0 +1,143 @@ +// +// CustomAudioRenderRTC.swift +// APIExample-SwiftUI +// +// Created by qinhui on 2024/10/9. +// + +import Foundation +import AgoraRtcKit + +struct CustomAudioRenderViewInfo: Hashable { + var uid: UInt + var content: String +} + +class CustomAudioRenderRTC: NSObject, ObservableObject { + @Published var isJoined: Bool = false + @Published var audioInfos: [CustomAudioRenderViewInfo] = [] + private var agoraKit: AgoraRtcEngineKit! + private var exAudio: ExternalAudio = ExternalAudio.shared() + + func setupRTC(configs: [String: Any]) { + let sampleRate: UInt = 44100, channel: UInt = 1 + + // set up agora instance when view loaded + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area + config.channelProfile = .liveBroadcasting + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + // Configuring Privatization Parameters + Util.configPrivatization(agoraKit: agoraKit) + agoraKit.setLogFile(LogUtils.sdkLogPath()) + + guard let channelName = configs["channelName"] as? String else {return} + + // make myself a broadcaster + agoraKit.setClientRole(GlobalSettings.shared.getUserRole()) + + // disable video module + agoraKit.disableVideo() + agoraKit.enableAudio() + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + exAudio.setupExternalAudio(withAgoraKit: agoraKit, + sampleRate: UInt32(sampleRate), + channels: UInt32(channel), + trackId: 1, + audioCRMode: .sdkCaptureExterRender, + ioType: .remoteIO) + agoraKit.setParameters("{\"che.audio.external_render\": true}") + agoraKit.setParameters("{\"che.audio.keep.audiosession\": true}") + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + option.publishCameraTrack = false + option.publishMicrophoneTrack = true + option.clientRoleType = GlobalSettings.shared.getUserRole() + + NetworkManager.shared.generateToken(channelName: channelName, success: { token in + let result = self.agoraKit.joinChannel(byToken: token, channelId: channelName, uid: 0, mediaOptions: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + ToastView.show(text: "joinChannel call failed: \(result), please check your params") + } + }) + } + + func onDestory() { + agoraKit.disableAudio() + agoraKit.disableVideo() + if isJoined { + agoraKit.stopPreview() + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + AgoraRtcEngineKit.destroy() + } + + private func getAudioLabel(uid: UInt, isLocal: Bool) -> String { + return "AUDIO ONLY\n\(isLocal ? "Local" : "Remote")\n\(uid)" + } +} + +extension CustomAudioRenderRTC: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en:https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + ToastView.show(text: "Error \(errorCode.description) occur") + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + self.isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + audioInfos.append(CustomAudioRenderViewInfo(uid: uid, content: self.getAudioLabel(uid: uid, isLocal: true))) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + audioInfos.append(CustomAudioRenderViewInfo(uid: uid, content: self.getAudioLabel(uid: uid, isLocal: false))) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // remove remote audio view + audioInfos.removeAll { info in + return info.uid == uid + } + } +} diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LiveStreaming/LiveStreamingRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LiveStreaming/LiveStreamingRTC.swift index dc64ce001..3f83a0152 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LiveStreaming/LiveStreamingRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LiveStreaming/LiveStreamingRTC.swift @@ -58,6 +58,12 @@ class LiveStreamingRTC: NSObject, ObservableObject { self.configs = configs self.backgroundView = localView self.foregroundView = remoteView + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.channelProfile = .liveBroadcasting + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + Util.configPrivatization(agoraKit: agoraKit) + agoraKit.setLogFile(LogUtils.sdkLogPath()) if let isFirstFrame = configs["isFirstFrame"] as? Bool, isFirstFrame == true { agoraKit.enableInstantMediaRendering() @@ -70,7 +76,6 @@ class LiveStreamingRTC: NSObject, ObservableObject { showUltraLowEntry = role == .audience showLinkStreamEntry = role == .audience - self.agoraKit.addDelegate(self) updateClientRole(role) // enable video module and set up video encoding configs diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/PictureInPicture/SDKRender/SDKRenderViewModel.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/PictureInPicture/SDKRender/SDKRenderViewModel.swift index e303fab8d..c6ccf7c82 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/PictureInPicture/SDKRender/SDKRenderViewModel.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/PictureInPicture/SDKRender/SDKRenderViewModel.swift @@ -72,6 +72,7 @@ class SDKRenderViewModel: NSObject, ObservableObject { rtcEngine.stopPreview() rtcEngine.leaveChannel(nil) } + AgoraRtcEngineKit.destroy() } func addRenderView(renderView: VideoView) {