diff --git a/.gitignore b/.gitignore index ab85156f7d..15bfdccc4f 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ android/.settings/org.eclipse.buildship.core.prefs # vs *.pdb ios/Frameworks +macos/Frameworks # SPM .build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index af3515a83f..be459f3781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,13 @@ This release introduces a fully isolated per-call PC factory model: each call no * `WebRTC.initialize`: replaced `options['reinitialize']` with `bool refresh = false`. Re-initialization no longer disposes existing PeerConnections, tracks, or streams. It refreshes build defaults for the next factory. * [iOS / macOS] Removed `FlutterWebRTCPlugin.peerConnectionFactory`. Use `factories[factoryId]`, `resolveFactoryForId()`, or `resolveFactoryForStreamId()`. * [Android] `MethodCallHandlerImpl` no longer lazily creates a default `AudioProcessingController`. Register one via `FlutterWebRTCPlugin.sharedSingleton.setAudioProcessingFactoryProvider(...)` if you need an external APM. +* Temporarily removed FrameCryptor support across all platforms (Dart, C++, iOS, Android). The `frameCryptorFactory` getter now throws `UnimplementedError`. FrameCryptor will be re-added in a future release. **New** * `NativePeerConnectionFactory` — representing one native factory with an isolated ADM per call. +* Bumped WebRTC to M145 (`stream-video-swift-webrtc` 145.9.0). +* Added Swift Package Manager support for macOS. **Audio lifecycle** @@ -27,8 +30,8 @@ This release introduces a fully isolated per-call PC factory model: each call no **Fixes** -* [Android] Fixed multi-call audio corruption caused by the shared `ExternalAudioProcessingFactory` leaking APM state across concurrent factories. -* [iOS] `setAppleAudioConfiguration` now uses the atomic `setCategory:mode:options:error:` API, preventing Bluetooth route drops during mid-transition category changes. +* Fixed multi-call audio corruption caused by the leaking APM state across concurrent factories. +* [iOS] `setAppleAudioConfiguration` now uses the atomic `setCategory` API, preventing Bluetooth route drops during mid-transition category changes. [2.2.7] - 2026-04-27 * [iOS] Fixed missing result callback in video effects method. diff --git a/Package.resolved b/Package.resolved index b2f56e4405..1757ba4052 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "7ee5623db3cce3ac924c3268452331eccfbd5018f0f2c8dc993b63ffbbbc91f6", + "originHash" : "f460b0f01ea083628a181a64701e7fabef0f3c0af018e2a5ae3f13252b822c75", "pins" : [ { "identity" : "stream-video-swift-webrtc", "kind" : "remoteSourceControl", "location" : "https://github.com/GetStream/stream-video-swift-webrtc.git", "state" : { - "revision" : "af8bb8ff4da92953567d777baba55369f998dc01", - "version" : "137.0.54" + "revision" : "f8689677681ed6b8d0f17b409cbbde67273017b2", + "version" : "145.9.0" } } ], diff --git a/Package.swift b/Package.swift index 792112b1f5..ffddb2138e 100644 --- a/Package.swift +++ b/Package.swift @@ -10,15 +10,16 @@ let package = Package( .library(name: "stream-webrtc-flutter", targets: ["stream_webrtc_flutter"]) ], dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework"), .package( - url: "https://github.com/GetStream/stream-video-swift-webrtc.git", exact: "137.0.54" + url: "https://github.com/GetStream/stream-video-swift-webrtc.git", exact: "145.9.0" ) - ], targets: [ .target( name: "stream_webrtc_flutter", dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework"), .product(name: "StreamWebRTC", package: "stream-video-swift-webrtc") ], path: "ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter", diff --git a/android/build.gradle b/android/build.gradle index 9595bf0559..f711ec3636 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,6 +6,7 @@ buildscript { repositories { google() mavenCentral() + maven { url = uri("https://central.sonatype.com/repository/maven-snapshots/") } } dependencies { @@ -19,6 +20,7 @@ rootProject.allprojects { google() mavenCentral() maven { url 'https://jitpack.io' } + maven { url = uri("https://central.sonatype.com/repository/maven-snapshots/") } } } @@ -52,7 +54,7 @@ android { } dependencies { - implementation 'io.getstream:stream-video-webrtc-android:137.0.1' + implementation("io.getstream:stream-video-webrtc-android:145.9.0") implementation 'com.github.davidliu:audioswitch:89582c47c9a04c62f90aa5e57251af4800a62c9a' implementation 'androidx.annotation:annotation:1.1.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 0d909c3afd..6adf4b3f19 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -213,7 +213,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F3FBAC538468FF33051E9409 /* [CP] Copy Pods Resources */, - 0E2A4621236E319E6196FD19 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -289,23 +288,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0E2A4621236E319E6196FD19 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 2AF6F0BE0DE220B6C4838EC9 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..0b1fba10bd --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "stream-video-swift-webrtc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GetStream/stream-video-swift-webrtc.git", + "state" : { + "revision" : "f8689677681ed6b8d0f17b409cbbde67273017b2", + "version" : "145.9.0" + } + } + ], + "version" : 2 +} diff --git a/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..0b1fba10bd --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "stream-video-swift-webrtc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GetStream/stream-video-swift-webrtc.git", + "state" : { + "revision" : "f8689677681ed6b8d0f17b409cbbde67273017b2", + "version" : "145.9.0" + } + } + ], + "version" : 2 +} diff --git a/example/macos/Podfile b/example/macos/Podfile index b52666a103..dbccf89c90 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.15' +platform :osx, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index ed70686a07..3543c52c27 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -83,13 +83,13 @@ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 6C71FB95414757DD39C78C06 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 811369FC2B504DB38C8FD91C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; B6C988E1EDA0500EDF915A78 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; E58FC4135E1382C65E2EEFF9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; FE789675E9B5707AFCFA2C07 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -235,9 +235,6 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -247,7 +244,6 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - DFEA526B9A053BB11393E2F2 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -255,6 +251,9 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* flutter_webrtc_example.app */; productType = "com.apple.product-type.application"; @@ -263,9 +262,6 @@ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, - ); isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; @@ -301,6 +297,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -414,23 +413,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - DFEA526B9A053BB11393E2F2 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -799,12 +781,14 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency; diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..0b1fba10bd --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "stream-video-swift-webrtc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GetStream/stream-video-swift-webrtc.git", + "state" : { + "revision" : "f8689677681ed6b8d0f17b409cbbde67273017b2", + "version" : "145.9.0" + } + } + ], + "version" : 2 +} diff --git a/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..0b1fba10bd --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "stream-video-swift-webrtc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GetStream/stream-video-swift-webrtc.git", + "state" : { + "revision" : "f8689677681ed6b8d0f17b409cbbde67273017b2", + "version" : "145.9.0" + } + } + ], + "version" : 2 +} diff --git a/ios/stream_webrtc_flutter.podspec b/ios/stream_webrtc_flutter.podspec index e007ddebc2..2a191c8041 100644 --- a/ios/stream_webrtc_flutter.podspec +++ b/ios/stream_webrtc_flutter.podspec @@ -18,7 +18,7 @@ A new flutter plugin project. s.vendored_frameworks = 'Frameworks/StreamWebRTC.xcframework' s.prepare_command = <<-CMD mkdir -p Frameworks/ - curl -sL "https://github.com/GetStream/stream-video-swift-webrtc/releases/download/137.0.54/StreamWebRTC.xcframework.zip" -o Frameworks/StreamWebRTC.zip + curl -sL "https://github.com/GetStream/stream-video-swift-webrtc/releases/download/145.9.0/StreamWebRTC.xcframework.zip" -o Frameworks/StreamWebRTC.zip unzip -o Frameworks/StreamWebRTC.zip -d Frameworks/ rm Frameworks/StreamWebRTC.zip CMD diff --git a/ios/stream_webrtc_flutter/Package.resolved b/ios/stream_webrtc_flutter/Package.resolved index c592478afa..0ee9919c67 100644 --- a/ios/stream_webrtc_flutter/Package.resolved +++ b/ios/stream_webrtc_flutter/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "ad41c3d56dda287a270dba5e52af6c780f66e85134941e0d843e63d453073fee", + "originHash" : "b77b679ed19e077be2b1ee480ddf5f5fd0299e27ac7fff4074edb850b601fc81", "pins" : [ { "identity" : "stream-video-swift-webrtc", "kind" : "remoteSourceControl", "location" : "https://github.com/GetStream/stream-video-swift-webrtc.git", "state" : { - "revision" : "af8bb8ff4da92953567d777baba55369f998dc01", - "version" : "137.0.54" + "revision" : "133a8a74c645faebc74dcaea2766731f3a142f1f", + "version" : "145.6.0" } } ], diff --git a/ios/stream_webrtc_flutter/Package.swift b/ios/stream_webrtc_flutter/Package.swift index cd36cc2343..be374d9461 100644 --- a/ios/stream_webrtc_flutter/Package.swift +++ b/ios/stream_webrtc_flutter/Package.swift @@ -10,15 +10,16 @@ let package = Package( .library(name: "stream-webrtc-flutter", targets: ["stream_webrtc_flutter"]) ], dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework"), .package( - url: "https://github.com/GetStream/stream-video-swift-webrtc.git", exact: "137.0.54" + url: "https://github.com/GetStream/stream-video-swift-webrtc.git", exact: "145.9.0" ) - ], targets: [ .target( name: "stream_webrtc_flutter", dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework"), .product(name: "StreamWebRTC", package: "stream-video-swift-webrtc") ], resources: [] diff --git a/lib/stream_webrtc_flutter.dart b/lib/stream_webrtc_flutter.dart index 0be8f00717..b36075ce9c 100644 --- a/lib/stream_webrtc_flutter.dart +++ b/lib/stream_webrtc_flutter.dart @@ -27,5 +27,5 @@ export 'src/native/ios/audio_management.dart'; export 'src/native/rtc_video_platform_view_controller.dart'; export 'src/native/rtc_video_platform_view.dart'; -const String androidWebRTCVersion = '137.0.1'; -const String iosWebRTCVersion = '137.0.54'; +const String androidWebRTCVersion = '145.9.0'; +const String iosWebRTCVersion = '145.9.0'; diff --git a/macos/Classes/AudioManager.h b/macos/Classes/AudioManager.h deleted file mode 120000 index 1c21c3e586..0000000000 --- a/macos/Classes/AudioManager.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/AudioManager.h \ No newline at end of file diff --git a/macos/Classes/AudioManager.m b/macos/Classes/AudioManager.m deleted file mode 120000 index bbcfb519ed..0000000000 --- a/macos/Classes/AudioManager.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/AudioManager.m \ No newline at end of file diff --git a/macos/Classes/AudioProcessingAdapter.h b/macos/Classes/AudioProcessingAdapter.h deleted file mode 120000 index f3048db2f1..0000000000 --- a/macos/Classes/AudioProcessingAdapter.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/AudioProcessingAdapter.h \ No newline at end of file diff --git a/macos/Classes/AudioProcessingAdapter.m b/macos/Classes/AudioProcessingAdapter.m deleted file mode 120000 index 803efdda50..0000000000 --- a/macos/Classes/AudioProcessingAdapter.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/AudioProcessingAdapter.m \ No newline at end of file diff --git a/macos/Classes/AudioUtils.h b/macos/Classes/AudioUtils.h deleted file mode 120000 index efc6c758c8..0000000000 --- a/macos/Classes/AudioUtils.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/AudioUtils.h \ No newline at end of file diff --git a/macos/Classes/AudioUtils.m b/macos/Classes/AudioUtils.m deleted file mode 120000 index 5023efe9fd..0000000000 --- a/macos/Classes/AudioUtils.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/AudioUtils.m \ No newline at end of file diff --git a/macos/Classes/CameraUtils.h b/macos/Classes/CameraUtils.h deleted file mode 120000 index a31c2baab2..0000000000 --- a/macos/Classes/CameraUtils.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/CameraUtils.h \ No newline at end of file diff --git a/macos/Classes/CameraUtils.m b/macos/Classes/CameraUtils.m deleted file mode 120000 index 336e1ea963..0000000000 --- a/macos/Classes/CameraUtils.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/CameraUtils.m \ No newline at end of file diff --git a/macos/Classes/FlutterDataPacketCryptor.h b/macos/Classes/FlutterDataPacketCryptor.h deleted file mode 120000 index 089d791551..0000000000 --- a/macos/Classes/FlutterDataPacketCryptor.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterDataPacketCryptor.h \ No newline at end of file diff --git a/macos/Classes/FlutterDataPacketCryptor.m b/macos/Classes/FlutterDataPacketCryptor.m deleted file mode 120000 index 975b6b964b..0000000000 --- a/macos/Classes/FlutterDataPacketCryptor.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterDataPacketCryptor.m \ No newline at end of file diff --git a/macos/Classes/FlutterRPScreenRecorder.h b/macos/Classes/FlutterRPScreenRecorder.h deleted file mode 120000 index a34a3193c9..0000000000 --- a/macos/Classes/FlutterRPScreenRecorder.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRPScreenRecorder.h \ No newline at end of file diff --git a/macos/Classes/FlutterRPScreenRecorder.m b/macos/Classes/FlutterRPScreenRecorder.m deleted file mode 120000 index f4e4d34067..0000000000 --- a/macos/Classes/FlutterRPScreenRecorder.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRPScreenRecorder.m \ No newline at end of file diff --git a/macos/Classes/FlutterRTCDataChannel.h b/macos/Classes/FlutterRTCDataChannel.h deleted file mode 120000 index ca751533c4..0000000000 --- a/macos/Classes/FlutterRTCDataChannel.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCDataChannel.h \ No newline at end of file diff --git a/macos/Classes/FlutterRTCDataChannel.m b/macos/Classes/FlutterRTCDataChannel.m deleted file mode 120000 index 2c6a822406..0000000000 --- a/macos/Classes/FlutterRTCDataChannel.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCDataChannel.m \ No newline at end of file diff --git a/macos/Classes/FlutterRTCDesktopCapturer.h b/macos/Classes/FlutterRTCDesktopCapturer.h deleted file mode 120000 index eff4773160..0000000000 --- a/macos/Classes/FlutterRTCDesktopCapturer.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCDesktopCapturer.h \ No newline at end of file diff --git a/macos/Classes/FlutterRTCDesktopCapturer.m b/macos/Classes/FlutterRTCDesktopCapturer.m deleted file mode 120000 index 5388e628f4..0000000000 --- a/macos/Classes/FlutterRTCDesktopCapturer.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCDesktopCapturer.m \ No newline at end of file diff --git a/macos/Classes/FlutterRTCFrameCapturer.h b/macos/Classes/FlutterRTCFrameCapturer.h deleted file mode 120000 index b732660b2f..0000000000 --- a/macos/Classes/FlutterRTCFrameCapturer.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCFrameCapturer.h \ No newline at end of file diff --git a/macos/Classes/FlutterRTCFrameCapturer.m b/macos/Classes/FlutterRTCFrameCapturer.m deleted file mode 120000 index 36b15d7c6a..0000000000 --- a/macos/Classes/FlutterRTCFrameCapturer.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCFrameCapturer.m \ No newline at end of file diff --git a/macos/Classes/FlutterRTCFrameCryptor.h b/macos/Classes/FlutterRTCFrameCryptor.h deleted file mode 120000 index ad3e0de33e..0000000000 --- a/macos/Classes/FlutterRTCFrameCryptor.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCFrameCryptor.h \ No newline at end of file diff --git a/macos/Classes/FlutterRTCFrameCryptor.m b/macos/Classes/FlutterRTCFrameCryptor.m deleted file mode 120000 index bd62d3db56..0000000000 --- a/macos/Classes/FlutterRTCFrameCryptor.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCFrameCryptor.m \ No newline at end of file diff --git a/macos/Classes/FlutterRTCMediaStream.h b/macos/Classes/FlutterRTCMediaStream.h deleted file mode 120000 index a56c382c17..0000000000 --- a/macos/Classes/FlutterRTCMediaStream.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCMediaStream.h \ No newline at end of file diff --git a/macos/Classes/FlutterRTCMediaStream.m b/macos/Classes/FlutterRTCMediaStream.m deleted file mode 120000 index 2e988ad614..0000000000 --- a/macos/Classes/FlutterRTCMediaStream.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCMediaStream.m \ No newline at end of file diff --git a/macos/Classes/FlutterRTCPeerConnection.h b/macos/Classes/FlutterRTCPeerConnection.h deleted file mode 120000 index c4907a3db8..0000000000 --- a/macos/Classes/FlutterRTCPeerConnection.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCPeerConnection.h \ No newline at end of file diff --git a/macos/Classes/FlutterRTCPeerConnection.m b/macos/Classes/FlutterRTCPeerConnection.m deleted file mode 120000 index 363aecf0c7..0000000000 --- a/macos/Classes/FlutterRTCPeerConnection.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCPeerConnection.m \ No newline at end of file diff --git a/macos/Classes/FlutterRTCVideoRenderer.h b/macos/Classes/FlutterRTCVideoRenderer.h deleted file mode 120000 index 2e68777e02..0000000000 --- a/macos/Classes/FlutterRTCVideoRenderer.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCVideoRenderer.h \ No newline at end of file diff --git a/macos/Classes/FlutterRTCVideoRenderer.m b/macos/Classes/FlutterRTCVideoRenderer.m deleted file mode 120000 index 77a0efd6d2..0000000000 --- a/macos/Classes/FlutterRTCVideoRenderer.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterRTCVideoRenderer.m \ No newline at end of file diff --git a/macos/Classes/FlutterWebRTCPlugin.h b/macos/Classes/FlutterWebRTCPlugin.h deleted file mode 120000 index b8713b38ef..0000000000 --- a/macos/Classes/FlutterWebRTCPlugin.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterWebRTCPlugin.h \ No newline at end of file diff --git a/macos/Classes/FlutterWebRTCPlugin.m b/macos/Classes/FlutterWebRTCPlugin.m deleted file mode 120000 index 7d5cc6ca16..0000000000 --- a/macos/Classes/FlutterWebRTCPlugin.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/FlutterWebRTCPlugin.m \ No newline at end of file diff --git a/macos/Classes/LocalAudioTrack.h b/macos/Classes/LocalAudioTrack.h deleted file mode 120000 index 421b56b2af..0000000000 --- a/macos/Classes/LocalAudioTrack.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/LocalAudioTrack.h \ No newline at end of file diff --git a/macos/Classes/LocalAudioTrack.m b/macos/Classes/LocalAudioTrack.m deleted file mode 120000 index 71fa724d15..0000000000 --- a/macos/Classes/LocalAudioTrack.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/LocalAudioTrack.m \ No newline at end of file diff --git a/macos/Classes/LocalTrack.h b/macos/Classes/LocalTrack.h deleted file mode 120000 index 7d41789949..0000000000 --- a/macos/Classes/LocalTrack.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/LocalTrack.h \ No newline at end of file diff --git a/macos/Classes/LocalVideoTrack.h b/macos/Classes/LocalVideoTrack.h deleted file mode 120000 index 5069f7dd17..0000000000 --- a/macos/Classes/LocalVideoTrack.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/LocalVideoTrack.h \ No newline at end of file diff --git a/macos/Classes/LocalVideoTrack.m b/macos/Classes/LocalVideoTrack.m deleted file mode 120000 index 182490a4fb..0000000000 --- a/macos/Classes/LocalVideoTrack.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/LocalVideoTrack.m \ No newline at end of file diff --git a/macos/Classes/NativePeerConnectionFactory.h b/macos/Classes/NativePeerConnectionFactory.h deleted file mode 120000 index 202f51c398..0000000000 --- a/macos/Classes/NativePeerConnectionFactory.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/NativePeerConnectionFactory.h \ No newline at end of file diff --git a/macos/Classes/NativePeerConnectionFactory.m b/macos/Classes/NativePeerConnectionFactory.m deleted file mode 120000 index c13a776f17..0000000000 --- a/macos/Classes/NativePeerConnectionFactory.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/NativePeerConnectionFactory.m \ No newline at end of file diff --git a/macos/Classes/ProcessorProvider.h b/macos/Classes/ProcessorProvider.h deleted file mode 120000 index 6212492e97..0000000000 --- a/macos/Classes/ProcessorProvider.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/ProcessorProvider.h \ No newline at end of file diff --git a/macos/Classes/ProcessorProvider.m b/macos/Classes/ProcessorProvider.m deleted file mode 120000 index 724d8ea942..0000000000 --- a/macos/Classes/ProcessorProvider.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/ProcessorProvider.m \ No newline at end of file diff --git a/macos/Classes/StreamMacAudioDevices.h b/macos/Classes/StreamMacAudioDevices.h deleted file mode 120000 index a9e3dfd4c7..0000000000 --- a/macos/Classes/StreamMacAudioDevices.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/StreamMacAudioDevices.h \ No newline at end of file diff --git a/macos/Classes/StreamMacAudioDevices.m b/macos/Classes/StreamMacAudioDevices.m deleted file mode 120000 index 59a58efd53..0000000000 --- a/macos/Classes/StreamMacAudioDevices.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/StreamMacAudioDevices.m \ No newline at end of file diff --git a/macos/Classes/VideoEffectProcessor.h b/macos/Classes/VideoEffectProcessor.h deleted file mode 120000 index 231b99672b..0000000000 --- a/macos/Classes/VideoEffectProcessor.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/VideoEffectProcessor.h \ No newline at end of file diff --git a/macos/Classes/VideoEffectProcessor.m b/macos/Classes/VideoEffectProcessor.m deleted file mode 120000 index 14d99a7e07..0000000000 --- a/macos/Classes/VideoEffectProcessor.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/VideoEffectProcessor.m \ No newline at end of file diff --git a/macos/Classes/VideoFactoriesPrivate.h b/macos/Classes/VideoFactoriesPrivate.h deleted file mode 120000 index 3d1430bd05..0000000000 --- a/macos/Classes/VideoFactoriesPrivate.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/VideoFactoriesPrivate.h \ No newline at end of file diff --git a/macos/Classes/VideoFrameProcessor.h b/macos/Classes/VideoFrameProcessor.h deleted file mode 120000 index 3e244a030d..0000000000 --- a/macos/Classes/VideoFrameProcessor.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/VideoFrameProcessor.h \ No newline at end of file diff --git a/macos/Classes/VideoProcessingAdapter.h b/macos/Classes/VideoProcessingAdapter.h deleted file mode 120000 index d93141230c..0000000000 --- a/macos/Classes/VideoProcessingAdapter.h +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/VideoProcessingAdapter.h \ No newline at end of file diff --git a/macos/Classes/VideoProcessingAdapter.m b/macos/Classes/VideoProcessingAdapter.m deleted file mode 120000 index c80ad1ca73..0000000000 --- a/macos/Classes/VideoProcessingAdapter.m +++ /dev/null @@ -1 +0,0 @@ -../../common/darwin/Classes/VideoProcessingAdapter.m \ No newline at end of file diff --git a/macos/stream_webrtc_flutter.podspec b/macos/stream_webrtc_flutter.podspec index ee0809d009..a35803f062 100644 --- a/macos/stream_webrtc_flutter.podspec +++ b/macos/stream_webrtc_flutter.podspec @@ -12,9 +12,18 @@ A new flutter plugin project. s.license = { :file => '../LICENSE' } s.author = { 'getstream.io' => 'support@getstream.io' } s.source = { :path => '.' } - s.source_files = ['Classes/**/*'] + s.source_files = 'stream_webrtc_flutter/Sources/stream_webrtc_flutter/**/*.{h,hpp,m,mm,c,cpp}' + s.public_header_files = 'stream_webrtc_flutter/Sources/stream_webrtc_flutterinclude/stream_webrtc_flutter/**/*.h' s.dependency 'FlutterMacOS' - s.dependency 'WebRTC-SDK', '137.7151.04' - s.osx.deployment_target = '10.15' + s.vendored_frameworks = 'Frameworks/StreamWebRTC.xcframework' + s.prepare_command = <<-CMD + mkdir -p Frameworks/ + curl -sL "https://github.com/GetStream/stream-video-swift-webrtc/releases/download/145.9.0/StreamWebRTC.xcframework.zip" -o Frameworks/StreamWebRTC.zip + unzip -o Frameworks/StreamWebRTC.zip -d Frameworks/ + rm Frameworks/StreamWebRTC.zip + CMD + s.ios.deployment_target = '13.0' + s.static_framework = true + s.osx.deployment_target = '11.0' end diff --git a/macos/stream_webrtc_flutter/Package.resolved b/macos/stream_webrtc_flutter/Package.resolved new file mode 100644 index 0000000000..0ee9919c67 --- /dev/null +++ b/macos/stream_webrtc_flutter/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "b77b679ed19e077be2b1ee480ddf5f5fd0299e27ac7fff4074edb850b601fc81", + "pins" : [ + { + "identity" : "stream-video-swift-webrtc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GetStream/stream-video-swift-webrtc.git", + "state" : { + "revision" : "133a8a74c645faebc74dcaea2766731f3a142f1f", + "version" : "145.6.0" + } + } + ], + "version" : 3 +} diff --git a/macos/stream_webrtc_flutter/Package.swift b/macos/stream_webrtc_flutter/Package.swift new file mode 100644 index 0000000000..bb8b709fdf --- /dev/null +++ b/macos/stream_webrtc_flutter/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "stream_webrtc_flutter", + platforms: [ + .macOS("11.0") + ], + products: [ + .library(name: "stream-webrtc-flutter", targets: ["stream_webrtc_flutter"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework"), + .package( + url: "https://github.com/GetStream/stream-video-swift-webrtc.git", exact: "145.9.0" + ) + ], + targets: [ + .target( + name: "stream_webrtc_flutter", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework"), + .product(name: "StreamWebRTC", package: "stream-video-swift-webrtc") + ], + resources: [] + ) + ] +) diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioManager.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioManager.m new file mode 100644 index 0000000000..0d6b959341 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioManager.m @@ -0,0 +1,50 @@ +#import "include/stream_webrtc_flutter/AudioManager.h" +#import "include/stream_webrtc_flutter/AudioProcessingAdapter.h" + +@implementation AudioManager { + RTCDefaultAudioProcessingModule* _audioProcessingModule; + AudioProcessingAdapter* _capturePostProcessingAdapter; + AudioProcessingAdapter* _renderPreProcessingAdapter; +} + +@synthesize capturePostProcessingAdapter = _capturePostProcessingAdapter; +@synthesize renderPreProcessingAdapter = _renderPreProcessingAdapter; +@synthesize audioProcessingModule = _audioProcessingModule; + ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + static AudioManager* sharedInstance = nil; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + if (self = [super init]) { + _audioProcessingModule = [[RTCDefaultAudioProcessingModule alloc] init]; + _capturePostProcessingAdapter = [[AudioProcessingAdapter alloc] init]; + _renderPreProcessingAdapter = [[AudioProcessingAdapter alloc] init]; + _audioProcessingModule.capturePostProcessingDelegate = _capturePostProcessingAdapter; + _audioProcessingModule.renderPreProcessingDelegate = _renderPreProcessingAdapter; + } + return self; +} + +- (void)addLocalAudioRenderer:(nonnull id)renderer { + [_capturePostProcessingAdapter addAudioRenderer:renderer]; +} + +- (void)removeLocalAudioRenderer:(nonnull id)renderer { + [_capturePostProcessingAdapter removeAudioRenderer:renderer]; +} + +- (void)addRemoteAudioSink:(nonnull id)sink { + [_renderPreProcessingAdapter addAudioRenderer:sink]; +} + +- (void)removeRemoteAudioSink:(nonnull id)sink { + [_renderPreProcessingAdapter removeAudioRenderer:sink]; +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioProcessingAdapter.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioProcessingAdapter.m new file mode 100644 index 0000000000..0b1aa7a2a8 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioProcessingAdapter.m @@ -0,0 +1,105 @@ +#import "include/stream_webrtc_flutter/AudioProcessingAdapter.h" +#import +#import + +@implementation AudioProcessingAdapter { + NSMutableArray>* _renderers; + NSMutableArray>* _processors; + os_unfair_lock _lock; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _lock = OS_UNFAIR_LOCK_INIT; + _renderers = [[NSMutableArray> alloc] init]; + _processors = [[NSMutableArray> alloc] init]; + } + return self; +} + +- (void)addProcessing:(id _Nonnull)processor { + os_unfair_lock_lock(&_lock); + [_processors addObject:processor]; + os_unfair_lock_unlock(&_lock); +} + +- (void)removeProcessing:(id _Nonnull)processor { + os_unfair_lock_lock(&_lock); + _processors = [[_processors + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, + NSDictionary* bindings) { + return evaluatedObject != processor; + }]] mutableCopy]; + os_unfair_lock_unlock(&_lock); +} + +- (void)addAudioRenderer:(nonnull id)renderer { + os_unfair_lock_lock(&_lock); + [_renderers addObject:renderer]; + os_unfair_lock_unlock(&_lock); +} + +- (void)removeAudioRenderer:(nonnull id)renderer { + os_unfair_lock_lock(&_lock); + _renderers = [[_renderers + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, + NSDictionary* bindings) { + return evaluatedObject != renderer; + }]] mutableCopy]; + os_unfair_lock_unlock(&_lock); +} + +- (void)audioProcessingInitializeWithSampleRate:(size_t)sampleRateHz channels:(size_t)channels { + os_unfair_lock_lock(&_lock); + for (id processor in _processors) { + [processor audioProcessingInitializeWithSampleRate:sampleRateHz channels:channels]; + } + os_unfair_lock_unlock(&_lock); +} + +- (AVAudioPCMBuffer*)toPCMBuffer:(RTC_OBJC_TYPE(RTCAudioBuffer) *)audioBuffer { + AVAudioFormat* format = + [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16 + sampleRate:audioBuffer.frames * 100.0 + channels:(AVAudioChannelCount)audioBuffer.channels + interleaved:NO]; + AVAudioPCMBuffer* pcmBuffer = + [[AVAudioPCMBuffer alloc] initWithPCMFormat:format + frameCapacity:(AVAudioFrameCount)audioBuffer.frames]; + if (!pcmBuffer) { + NSLog(@"Failed to create AVAudioPCMBuffer"); + return nil; + } + pcmBuffer.frameLength = (AVAudioFrameCount)audioBuffer.frames; + for (int i = 0; i < audioBuffer.channels; i++) { + float* sourceBuffer = [audioBuffer rawBufferForChannel:i]; + int16_t* targetBuffer = (int16_t*)pcmBuffer.int16ChannelData[i]; + for (int frame = 0; frame < audioBuffer.frames; frame++) { + targetBuffer[frame] = sourceBuffer[frame]; + } + } + return pcmBuffer; +} + +- (void)audioProcessingProcess:(RTC_OBJC_TYPE(RTCAudioBuffer) *)audioBuffer { + os_unfair_lock_lock(&_lock); + for (id processor in _processors) { + [processor audioProcessingProcess:audioBuffer]; + } + + for (id renderer in _renderers) { + [renderer renderPCMBuffer:[self toPCMBuffer:audioBuffer]]; + } + os_unfair_lock_unlock(&_lock); +} + +- (void)audioProcessingRelease { + os_unfair_lock_lock(&_lock); + for (id processor in _processors) { + [processor audioProcessingRelease]; + } + os_unfair_lock_unlock(&_lock); +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/CameraUtils.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/CameraUtils.m new file mode 100644 index 0000000000..6e5ae4efc7 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/CameraUtils.m @@ -0,0 +1,211 @@ +#import "include/stream_webrtc_flutter/CameraUtils.h" + +@implementation FlutterWebRTCPlugin (CameraUtils) + +- (AVCaptureDevice*)currentDevice { + if (!self.videoCapturer) { + return nil; + } + if (self.videoCapturer.captureSession.inputs.count == 0) { + return nil; + } + AVCaptureDeviceInput* deviceInput = [self.videoCapturer.captureSession.inputs objectAtIndex:0]; + return deviceInput.device; +} + +- (void)mediaStreamTrackHasTorch:(RTCMediaStreamTrack*)track result:(FlutterResult)result { + NSLog(@"Not supported on macOS. Can't check torch"); + result(@NO); +} + +- (void)mediaStreamTrackSetTorch:(RTCMediaStreamTrack*)track + torch:(BOOL)torch + result:(FlutterResult)result { + AVCaptureDevice* device = [self currentDevice]; + if (!device) { + NSLog(@"Video capturer is null. Can't set torch"); + result([FlutterError errorWithCode:@"mediaStreamTrackSetTorchFailed" + message:@"device is nil" + details:nil]); + return; + } + + if (![device isTorchModeSupported:AVCaptureTorchModeOn]) { + NSLog(@"Current capture device does not support torch. Can't set torch"); + result([FlutterError errorWithCode:@"mediaStreamTrackSetTorchFailed" + message:@"device does not support torch" + details:nil]); + return; + } + + NSError* error; + if ([device lockForConfiguration:&error] == NO) { + NSLog(@"Failed to aquire configuration lock. %@", error.localizedDescription); + result([FlutterError errorWithCode:@"mediaStreamTrackSetTorchFailed" + message:error.localizedDescription + details:nil]); + return; + } + + device.torchMode = torch ? AVCaptureTorchModeOn : AVCaptureTorchModeOff; + [device unlockForConfiguration]; + + result(nil); +} + +- (void)mediaStreamTrackSetZoom:(RTCMediaStreamTrack*)track + zoomLevel:(double)zoomLevel + result:(FlutterResult)result { + NSLog(@"Not supported on macOS. Can't set zoom"); + result([FlutterError errorWithCode:@"mediaStreamTrackSetZoomFailed" + message:@"Not supported on macOS" + details:nil]); +} + +- (void)applyFocusMode:(NSString*)focusMode onDevice:(AVCaptureDevice*)captureDevice { +} + +- (void)mediaStreamTrackSetFocusMode:(nonnull RTCMediaStreamTrack*)track + focusMode:(nonnull NSString*)focusMode + result:(nonnull FlutterResult)result { + NSLog(@"Not supported on macOS. Can't focusMode"); + result([FlutterError errorWithCode:@"mediaStreamTrackSetFocusModeFailed" + message:@"Not supported on macOS" + details:nil]); +} + +- (void)mediaStreamTrackSetFocusPoint:(nonnull RTCMediaStreamTrack*)track + focusPoint:(nonnull NSDictionary*)focusPoint + result:(nonnull FlutterResult)result { + NSLog(@"Not supported on macOS. Can't focusPoint"); + result([FlutterError errorWithCode:@"mediaStreamTrackSetFocusPointFailed" + message:@"Not supported on macOS" + details:nil]); +} + +- (void)applyExposureMode:(NSString*)exposureMode onDevice:(AVCaptureDevice*)captureDevice { +} + +- (void)mediaStreamTrackSetExposureMode:(nonnull RTCMediaStreamTrack*)track + exposureMode:(nonnull NSString*)exposureMode + result:(nonnull FlutterResult)result { + NSLog(@"Not supported on macOS. Can't exposureMode"); + result([FlutterError errorWithCode:@"mediaStreamTrackSetExposureModeFailed" + message:@"Not supported on macOS" + details:nil]); +} + +- (void)mediaStreamTrackSetExposurePoint:(nonnull RTCMediaStreamTrack*)track + exposurePoint:(nonnull NSDictionary*)exposurePoint + result:(nonnull FlutterResult)result { + NSLog(@"Not supported on macOS. Can't exposurePoint"); + result([FlutterError errorWithCode:@"mediaStreamTrackSetExposurePointFailed" + message:@"Not supported on macOS" + details:nil]); +} + +- (void)mediaStreamTrackSwitchCamera:(RTCMediaStreamTrack*)track result:(FlutterResult)result { + NSString* trackId = track.trackId; + NSMutableDictionary* captureState = self.videoCaptureState[trackId]; + + if (!self.videoCapturer) { + NSLog(@"Video capturer is null. Can't switch camera"); + result([FlutterError errorWithCode:@"Error while switching camera" + message:@"Video capturer not found" + details:nil]); + return; + } + + BOOL usingFrontCamera; + NSInteger targetWidth; + NSInteger targetHeight; + NSInteger targetFps; + + if (captureState) { + // Use per-track state + usingFrontCamera = [captureState[@"usingFrontCamera"] boolValue]; + targetWidth = [captureState[@"targetWidth"] integerValue]; + targetHeight = [captureState[@"targetHeight"] integerValue]; + targetFps = [captureState[@"targetFps"] integerValue]; + } else { + // Use global state for backward compatibility + usingFrontCamera = self._usingFrontCamera; + targetWidth = self._lastTargetWidth; + targetHeight = self._lastTargetHeight; + targetFps = self._lastTargetFps; + } + + usingFrontCamera = !usingFrontCamera; + AVCaptureDevicePosition position = + usingFrontCamera ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack; + AVCaptureDevice* videoDevice = [self findDeviceForPosition:position]; + AVCaptureDeviceFormat* selectedFormat = [self selectFormatForDevice:videoDevice + targetWidth:targetWidth + targetHeight:targetHeight]; + [self.videoCapturer startCaptureWithDevice:videoDevice + format:selectedFormat + fps:[self selectFpsForFormat:selectedFormat + targetFps:targetFps] + completionHandler:^(NSError* error) { + if (error != nil) { + result([FlutterError errorWithCode:@"Error while switching camera" + message:@"Error while switching camera" + details:error]); + } else { + // Update per-track state + if (captureState) { + captureState[@"usingFrontCamera"] = @(usingFrontCamera); + } + // Update global state for backward compatibility + self._usingFrontCamera = usingFrontCamera; + result([NSNumber numberWithBool:usingFrontCamera]); + } + }]; +} + +- (AVCaptureDevice*)findDeviceForPosition:(AVCaptureDevicePosition)position { + if (position == AVCaptureDevicePositionUnspecified) { + return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + } + NSArray* captureDevices = [RTCCameraVideoCapturer captureDevices]; + for (AVCaptureDevice* device in captureDevices) { + if (device.position == position) { + return device; + } + } + return captureDevices[0]; +} + +- (AVCaptureDeviceFormat*)selectFormatForDevice:(AVCaptureDevice*)device + targetWidth:(NSInteger)targetWidth + targetHeight:(NSInteger)targetHeight { + NSArray* formats = + [RTCCameraVideoCapturer supportedFormatsForDevice:device]; + AVCaptureDeviceFormat* selectedFormat = nil; + long currentDiff = INT_MAX; + for (AVCaptureDeviceFormat* format in formats) { + CMVideoDimensions dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription); + FourCharCode pixelFormat = CMFormatDescriptionGetMediaSubType(format.formatDescription); + // NSLog(@"AVCaptureDeviceFormats,fps %d, dimension: %dx%d", + // format.videoSupportedFrameRateRanges, dimension.width, dimension.height); + long diff = labs(targetWidth - dimension.width) + labs(targetHeight - dimension.height); + if (diff < currentDiff) { + selectedFormat = format; + currentDiff = diff; + } else if (diff == currentDiff && + pixelFormat == [self.videoCapturer preferredOutputPixelFormat]) { + selectedFormat = format; + } + } + return selectedFormat; +} + +- (NSInteger)selectFpsForFormat:(AVCaptureDeviceFormat*)format targetFps:(NSInteger)targetFps { + Float64 maxSupportedFramerate = 0; + for (AVFrameRateRange* fpsRange in format.videoSupportedFrameRateRanges) { + maxSupportedFramerate = fmax(maxSupportedFramerate, fpsRange.maxFrameRate); + } + return fmin(maxSupportedFramerate, targetFps); +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCAudioSink.mm b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCAudioSink.mm new file mode 100644 index 0000000000..8748af1b66 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCAudioSink.mm @@ -0,0 +1,75 @@ +#import "include/stream_webrtc_flutter/FlutterRTCAudioSink.h" +#import +#include "audio_sink_bridge.cpp" +#import "include/stream_webrtc_flutter/RTCAudioSource+Private.h" +#include "include/stream_webrtc_flutter/media_stream_interface.h" + +@implementation FlutterRTCAudioSink { + AudioSinkBridge* _bridge; + webrtc::AudioSourceInterface* _audioSource; +} + +- (instancetype)initWithAudioTrack:(RTCAudioTrack*)audio { + self = [super init]; + rtc::scoped_refptr audioSourcePtr = audio.source.nativeAudioSource; + _audioSource = audioSourcePtr.get(); + _bridge = new AudioSinkBridge((void*)CFBridgingRetain(self)); + _audioSource->AddSink(_bridge); + return self; +} + +- (void)close { + _audioSource->RemoveSink(_bridge); + delete _bridge; + _bridge = nil; + _audioSource = nil; +} + +void RTCAudioSinkCallback(void* object, + const void* audio_data, + int bits_per_sample, + int sample_rate, + size_t number_of_channels, + size_t number_of_frames) { + AudioBufferList audioBufferList; + AudioBuffer audioBuffer; + audioBuffer.mData = (void*)audio_data; + audioBuffer.mDataByteSize = bits_per_sample / 8 * number_of_channels * number_of_frames; + audioBuffer.mNumberChannels = number_of_channels; + audioBufferList.mNumberBuffers = 1; + audioBufferList.mBuffers[0] = audioBuffer; + AudioStreamBasicDescription audioDescription; + audioDescription.mBytesPerFrame = bits_per_sample / 8 * number_of_channels; + audioDescription.mBitsPerChannel = bits_per_sample; + audioDescription.mBytesPerPacket = bits_per_sample / 8 * number_of_channels; + audioDescription.mChannelsPerFrame = number_of_channels; + audioDescription.mFormatID = kAudioFormatLinearPCM; + audioDescription.mFormatFlags = + kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; + audioDescription.mFramesPerPacket = 1; + audioDescription.mReserved = 0; + audioDescription.mSampleRate = sample_rate; + CMAudioFormatDescriptionRef formatDesc; + CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &audioDescription, 0, nil, 0, nil, nil, + &formatDesc); + CMSampleBufferRef buffer; + CMSampleTimingInfo timing; + timing.decodeTimeStamp = kCMTimeInvalid; + timing.presentationTimeStamp = CMTimeMake(0, sample_rate); + timing.duration = CMTimeMake(1, sample_rate); + CMSampleBufferCreate(kCFAllocatorDefault, nil, false, nil, nil, formatDesc, + number_of_frames * number_of_channels, 1, &timing, 0, nil, &buffer); + CMSampleBufferSetDataBufferFromAudioBufferList(buffer, kCFAllocatorDefault, kCFAllocatorDefault, + 0, &audioBufferList); + @autoreleasepool { + FlutterRTCAudioSink* sink = (__bridge FlutterRTCAudioSink*)(object); + sink.format = formatDesc; + if (sink.bufferCallback != nil) { + sink.bufferCallback(buffer); + } else { + NSLog(@"Buffer callback is nil"); + } + } +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCDataChannel.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCDataChannel.m new file mode 100644 index 0000000000..314b2d2130 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCDataChannel.m @@ -0,0 +1,219 @@ +#import "include/stream_webrtc_flutter/FlutterRTCDataChannel.h" +#import +#import +#import "include/stream_webrtc_flutter/FlutterRTCPeerConnection.h" + +@implementation RTCDataChannel (Flutter) + +- (NSString*)peerConnectionId { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setPeerConnectionId:(NSString*)peerConnectionId { + objc_setAssociatedObject(self, @selector(peerConnectionId), peerConnectionId, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (FlutterEventSink)eventSink { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setEventSink:(FlutterEventSink)eventSink { + objc_setAssociatedObject(self, @selector(eventSink), eventSink, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSArray*)eventQueue { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setEventQueue:(NSArray*)eventQueue { + objc_setAssociatedObject(self, @selector(eventQueue), eventQueue, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSNumber*)flutterChannelId { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setFlutterChannelId:(NSNumber*)flutterChannelId { + objc_setAssociatedObject(self, @selector(flutterChannelId), flutterChannelId, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (FlutterEventChannel*)eventChannel { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setEventChannel:(FlutterEventChannel*)eventChannel { + objc_setAssociatedObject(self, @selector(eventChannel), eventChannel, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +#pragma mark - FlutterStreamHandler methods + +- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments { + self.eventSink = nil; + return nil; +} + +- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)sink { + self.eventSink = sink; + NSEnumerator* enumerator = [self.eventQueue objectEnumerator]; + id event; + while ((event = enumerator.nextObject) != nil) { + postEvent(sink, event); + }; + self.eventQueue = nil; + return nil; +} +@end + +@implementation FlutterWebRTCPlugin (RTCDataChannel) + +- (void)createDataChannel:(nonnull NSString*)peerConnectionId + label:(NSString*)label + config:(RTCDataChannelConfiguration*)config + messenger:(NSObject*)messenger + result:(nonnull FlutterResult)result { + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + RTCDataChannel* dataChannel = [peerConnection dataChannelForLabel:label configuration:config]; + + if (nil != dataChannel) { + dataChannel.peerConnectionId = peerConnectionId; + NSString* flutterId = [[NSUUID UUID] UUIDString]; + peerConnection.dataChannels[flutterId] = dataChannel; + dataChannel.flutterChannelId = flutterId; + dataChannel.delegate = self; + dataChannel.eventQueue = nil; + + FlutterEventChannel* eventChannel = [FlutterEventChannel + eventChannelWithName:[NSString stringWithFormat:@"FlutterWebRTC/dataChannelEvent%1$@%2$@", + peerConnectionId, flutterId] + binaryMessenger:messenger]; + + dataChannel.eventChannel = eventChannel; + [eventChannel setStreamHandler:dataChannel]; + + result(@{ + @"label" : label, + @"id" : [NSNumber numberWithInt:dataChannel.channelId], + @"flutterId" : flutterId + }); + } +} + +- (void)dataChannelClose:(nonnull NSString*)peerConnectionId + dataChannelId:(nonnull NSString*)dataChannelId { + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + NSMutableDictionary* dataChannels = peerConnection.dataChannels; + RTCDataChannel* dataChannel = dataChannels[dataChannelId]; + if (dataChannel) { + FlutterEventChannel* eventChannel = dataChannel.eventChannel; + [dataChannel close]; + [dataChannels removeObjectForKey:dataChannelId]; + [eventChannel setStreamHandler:nil]; + dataChannel.eventChannel = nil; + } +} + +- (void)dataChannelGetBufferedAmount:(nonnull NSString*)peerConnectionId + dataChannelId:(nonnull NSString*)dataChannelId + result:(nonnull FlutterResult)result { + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + RTCDataChannel* dataChannel = peerConnection.dataChannels[dataChannelId]; + if (dataChannel == NULL || dataChannel.readyState != RTCDataChannelStateOpen) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", @"dataChannelGetBufferedAmount"] + message:[NSString stringWithFormat:@"Error: dataChannel not found or not opened!"] + details:nil]); + } else { + result(@{@"bufferedAmount" : @(dataChannel.bufferedAmount)}); + } +} + +- (void)dataChannelSend:(nonnull NSString*)peerConnectionId + dataChannelId:(nonnull NSString*)dataChannelId + data:(id)data + type:(NSString*)type { + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + RTCDataChannel* dataChannel = peerConnection.dataChannels[dataChannelId]; + + NSData* bytes = [type isEqualToString:@"binary"] ? ((FlutterStandardTypedData*)data).data + : [data dataUsingEncoding:NSUTF8StringEncoding]; + + RTCDataBuffer* buffer = [[RTCDataBuffer alloc] initWithData:bytes + isBinary:[type isEqualToString:@"binary"]]; + [dataChannel sendData:buffer]; +} + +- (NSString*)stringForDataChannelState:(RTCDataChannelState)state { + switch (state) { + case RTCDataChannelStateConnecting: + return @"connecting"; + case RTCDataChannelStateOpen: + return @"open"; + case RTCDataChannelStateClosing: + return @"closing"; + case RTCDataChannelStateClosed: + return @"closed"; + } + return nil; +} + +- (void)sendEvent:(id)event withChannel:(RTCDataChannel*)channel { + if (channel.eventSink) { + postEvent(channel.eventSink, event); + } else { + if (!channel.eventQueue) { + channel.eventQueue = [NSMutableArray array]; + } + channel.eventQueue = [channel.eventQueue arrayByAddingObject:event]; + } +} + +#pragma mark - RTCDataChannelDelegate methods + +// Called when the data channel state has changed. +- (void)dataChannelDidChangeState:(RTCDataChannel*)channel { + [self sendEvent:@{ + @"event" : @"dataChannelStateChanged", + @"id" : [NSNumber numberWithInt:channel.channelId], + @"state" : [self stringForDataChannelState:channel.readyState] + } + withChannel:channel]; +} + +// Called when a data buffer was successfully received. +- (void)dataChannel:(RTCDataChannel*)channel didReceiveMessageWithBuffer:(RTCDataBuffer*)buffer { + NSString* type; + id data; + if (buffer.isBinary) { + type = @"binary"; + data = [FlutterStandardTypedData typedDataWithBytes:buffer.data]; + } else { + type = @"text"; + data = [[NSString alloc] initWithData:buffer.data encoding:NSUTF8StringEncoding]; + } + + [self sendEvent:@{ + @"event" : @"dataChannelReceiveMessage", + @"id" : [NSNumber numberWithInt:channel.channelId], + @"type" : type, + @"data" : (data ? data : [NSNull null]) + } + withChannel:channel]; +} + +- (void)dataChannel:(RTCDataChannel*)channel didChangeBufferedAmount:(uint64_t)amount { + [self sendEvent:@{ + @"event" : @"dataChannelBufferedAmountChange", + @"id" : [NSNumber numberWithInt:channel.channelId], + @"bufferedAmount" : [NSNumber numberWithLongLong:channel.bufferedAmount], + @"changedAmount" : [NSNumber numberWithLongLong:amount] + } + withChannel:channel]; +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCDesktopCapturer.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCDesktopCapturer.m new file mode 100644 index 0000000000..c0487cb63d --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCDesktopCapturer.m @@ -0,0 +1,374 @@ +#import + +#import "include/stream_webrtc_flutter/FlutterRTCDesktopCapturer.h" + +#import "include/stream_webrtc_flutter/LocalVideoTrack.h" +#import "include/stream_webrtc_flutter/NativePeerConnectionFactory.h" +#import "include/stream_webrtc_flutter/VideoProcessingAdapter.h" + +RTCDesktopMediaList* _screen = nil; +RTCDesktopMediaList* _window = nil; +NSArray* _captureSources; + +@implementation FlutterWebRTCPlugin (DesktopCapturer) + +- (void)getDisplayMedia:(NSDictionary*)constraints + factoryId:(NSString*)factoryId + result:(FlutterResult)result { + NSString* mediaStreamId = [[NSUUID UUID] UUIDString]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil || nf.factory == nil) { + result([FlutterError + errorWithCode:@"getDisplayMedia" + message:[NSString stringWithFormat:@"unknown factoryId %@", factoryId] + details:nil]); + return; + } + RTCMediaStream* mediaStream = [nf.factory mediaStreamWithStreamId:mediaStreamId]; + RTCVideoSource* videoSource = [nf.factory videoSourceForScreenCast:YES]; + NSString* trackUUID = [[NSUUID UUID] UUIDString]; + VideoProcessingAdapter* videoProcessingAdapter = + [[VideoProcessingAdapter alloc] initWithRTCVideoSource:videoSource]; + + /* example for constraints: + { + 'audio': false, + 'video": { + 'deviceId': {'exact': sourceId}, + 'mandatory': { + 'frameRate': 30.0 + }, + } + } + */ + NSString* sourceId = nil; + BOOL useDefaultScreen = NO; + NSInteger fps = 30; + id videoConstraints = constraints[@"video"]; + if ([videoConstraints isKindOfClass:[NSNumber class]] && [videoConstraints boolValue] == YES) { + useDefaultScreen = YES; + } else if ([videoConstraints isKindOfClass:[NSDictionary class]]) { + NSDictionary* deviceId = videoConstraints[@"deviceId"]; + if (deviceId != nil && [deviceId isKindOfClass:[NSDictionary class]]) { + if (deviceId[@"exact"] != nil) { + sourceId = deviceId[@"exact"]; + if (sourceId == nil) { + result(@{@"error" : @"No deviceId.exact found"}); + return; + } + } + } else { + // fall back to default screen if no deviceId is specified + useDefaultScreen = YES; + } + id mandatory = videoConstraints[@"mandatory"]; + if (mandatory != nil && [mandatory isKindOfClass:[NSDictionary class]]) { + id frameRate = mandatory[@"frameRate"]; + if (frameRate != nil && [frameRate isKindOfClass:[NSNumber class]]) { + fps = [frameRate integerValue]; + } + } + } + RTCDesktopCapturer* desktopCapturer; + RTCDesktopSource* source = nil; + + if (useDefaultScreen) { + desktopCapturer = [[RTCDesktopCapturer alloc] initWithDefaultScreen:self + captureDelegate:videoProcessingAdapter]; + } else { + source = [self getSourceById:sourceId]; + if (source == nil) { + result(@{@"error" : [NSString stringWithFormat:@"No source found for id: %@", sourceId]}); + return; + } + desktopCapturer = [[RTCDesktopCapturer alloc] initWithSource:source + delegate:self + captureDelegate:videoProcessingAdapter]; + } + [desktopCapturer startCaptureWithFPS:fps]; + NSLog(@"start desktop capture: sourceId: %@, type: %@, fps: %lu", sourceId, + source.sourceType == RTCDesktopSourceTypeScreen ? @"screen" : @"window", fps); + + self.videoCapturerStopHandlers[trackUUID] = ^(CompletionHandler handler) { + NSLog(@"stop desktop capture: sourceId: %@, type: %@, trackID %@", sourceId, + source.sourceType == RTCDesktopSourceTypeScreen ? @"screen" : @"window", trackUUID); + [desktopCapturer stopCapture]; + handler(); + }; + + RTCVideoTrack* videoTrack = [nf.factory videoTrackWithSource:videoSource + trackId:trackUUID]; + [mediaStream addVideoTrack:videoTrack]; + + LocalVideoTrack* localVideoTrack = [[LocalVideoTrack alloc] initWithTrack:videoTrack + videoProcessing:videoProcessingAdapter]; + + [self.localTracks setObject:localVideoTrack forKey:trackUUID]; + + if (nf.factoryId != nil) { + self.trackFactoryId[trackUUID] = nf.factoryId; + } + + NSMutableArray* audioTracks = [NSMutableArray array]; + NSMutableArray* videoTracks = [NSMutableArray array]; + + for (RTCVideoTrack* track in mediaStream.videoTracks) { + [videoTracks addObject:@{ + @"id" : track.trackId, + @"kind" : track.kind, + @"label" : track.trackId, + @"enabled" : @(track.isEnabled), + @"remote" : @(YES), + @"readyState" : @"live" + }]; + } + + self.localStreams[mediaStreamId] = mediaStream; + result( + @{@"streamId" : mediaStreamId, @"audioTracks" : audioTracks, @"videoTracks" : videoTracks}); +} + +- (void)getDesktopSources:(NSDictionary*)argsMap result:(FlutterResult)result { + NSLog(@"getDesktopSources"); + + NSArray* types = [argsMap objectForKey:@"types"]; + if (types == nil) { + result([FlutterError errorWithCode:@"ERROR" message:@"types is required" details:nil]); + return; + } + + if (![self buildDesktopSourcesListWithTypes:types forceReload:YES result:result]) { + NSLog(@"getDesktopSources failed."); + return; + } + + NSMutableArray* sources = [NSMutableArray array]; + NSEnumerator* enumerator = [_captureSources objectEnumerator]; + RTCDesktopSource* object; + while ((object = enumerator.nextObject) != nil) { + /*NSData *data = nil; + if([object thumbnail]) { + data = [[NSData alloc] init]; + NSImage *resizedImg = [self resizeImage:[object thumbnail] forSize:NSMakeSize(320, 180)]; + data = [resizedImg TIFFRepresentation]; + }*/ + [sources addObject:@{ + @"id" : object.sourceId, + @"name" : object.name, + @"thumbnailSize" : @{@"width" : @0, @"height" : @0}, + @"type" : object.sourceType == RTCDesktopSourceTypeScreen ? @"screen" : @"window", + //@"thumbnail": data, + }]; + } + result(@{@"sources" : sources}); +} + +- (void)getDesktopSourceThumbnail:(NSDictionary*)argsMap result:(FlutterResult)result { + NSLog(@"getDesktopSourceThumbnail"); + NSString* sourceId = argsMap[@"sourceId"]; + RTCDesktopSource* object = [self getSourceById:sourceId]; + if (object == nil) { + result(@{@"error" : @"No source found"}); + return; + } + NSImage* image = [object UpdateThumbnail]; + if (image != nil) { + NSImage* resizedImg = [self resizeImage:image forSize:NSMakeSize(320, 180)]; + NSData* data = [resizedImg TIFFRepresentation]; + result(data); + } else { + result(@{@"error" : @"No thumbnail found"}); + } +} + +- (void)updateDesktopSources:(NSDictionary*)argsMap result:(FlutterResult)result { + NSLog(@"updateDesktopSources"); + NSArray* types = [argsMap objectForKey:@"types"]; + if (types == nil) { + result([FlutterError errorWithCode:@"ERROR" message:@"types is required" details:nil]); + return; + } + if (![self buildDesktopSourcesListWithTypes:types forceReload:NO result:result]) { + NSLog(@"updateDesktopSources failed."); + return; + } + result(@{@"result" : @YES}); +} + +- (NSImage*)resizeImage:(NSImage*)sourceImage forSize:(CGSize)targetSize { + CGSize imageSize = sourceImage.size; + CGFloat width = imageSize.width; + CGFloat height = imageSize.height; + CGFloat targetWidth = targetSize.width; + CGFloat targetHeight = targetSize.height; + CGFloat scaleFactor = 0.0; + CGFloat scaledWidth = targetWidth; + CGFloat scaledHeight = targetHeight; + CGPoint thumbnailPoint = CGPointMake(0.0, 0.0); + + if (CGSizeEqualToSize(imageSize, targetSize) == NO) { + CGFloat widthFactor = targetWidth / width; + CGFloat heightFactor = targetHeight / height; + + // scale to fit the longer + scaleFactor = (widthFactor > heightFactor) ? widthFactor : heightFactor; + scaledWidth = ceil(width * scaleFactor); + scaledHeight = ceil(height * scaleFactor); + + // center the image + if (widthFactor > heightFactor) { + thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5; + } else if (widthFactor < heightFactor) { + thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5; + } + } + + NSImage* newImage = [[NSImage alloc] initWithSize:NSMakeSize(scaledWidth, scaledHeight)]; + CGRect thumbnailRect = {thumbnailPoint, {scaledWidth, scaledHeight}}; + NSRect imageRect = NSMakeRect(0.0, 0.0, width, height); + + [newImage lockFocus]; + [sourceImage drawInRect:thumbnailRect + fromRect:imageRect + operation:NSCompositingOperationCopy + fraction:1.0]; + [newImage unlockFocus]; + + return newImage; +} + +- (RTCDesktopSource*)getSourceById:(NSString*)sourceId { + NSEnumerator* enumerator = [_captureSources objectEnumerator]; + RTCDesktopSource* object; + while ((object = enumerator.nextObject) != nil) { + if ([sourceId isEqualToString:object.sourceId]) { + return object; + } + } + return nil; +} + +- (BOOL)buildDesktopSourcesListWithTypes:(NSArray*)types + forceReload:(BOOL)forceReload + result:(FlutterResult)result { + BOOL captureWindow = NO; + BOOL captureScreen = NO; + _captureSources = [NSMutableArray array]; + + NSEnumerator* typesEnumerator = [types objectEnumerator]; + NSString* type; + while ((type = typesEnumerator.nextObject) != nil) { + if ([type isEqualToString:@"screen"]) { + captureScreen = YES; + } else if ([type isEqualToString:@"window"]) { + captureWindow = YES; + } else { + result([FlutterError errorWithCode:@"ERROR" message:@"Invalid type" details:nil]); + return NO; + } + } + + if (!captureWindow && !captureScreen) { + result([FlutterError errorWithCode:@"ERROR" + message:@"At least one type is required" + details:nil]); + return NO; + } + + if (captureWindow) { + if (!_window) + _window = [[RTCDesktopMediaList alloc] initWithType:RTCDesktopSourceTypeWindow delegate:self]; + [_window UpdateSourceList:forceReload updateAllThumbnails:YES]; + NSArray* sources = [_window getSources]; + _captureSources = [_captureSources arrayByAddingObjectsFromArray:sources]; + } + if (captureScreen) { + if (!_screen) + _screen = [[RTCDesktopMediaList alloc] initWithType:RTCDesktopSourceTypeScreen delegate:self]; + [_screen UpdateSourceList:forceReload updateAllThumbnails:YES]; + NSArray* sources = [_screen getSources]; + _captureSources = [_captureSources arrayByAddingObjectsFromArray:sources]; + } + NSLog(@"captureSources: %lu", [_captureSources count]); + return YES; +} + +#pragma mark - RTCDesktopMediaListDelegate delegate + +#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" +- (void)didDesktopSourceAdded:(RTC_OBJC_TYPE(RTCDesktopSource) *)source { + // NSLog(@"didDesktopSourceAdded: %@, id %@", source.name, source.sourceId); + if (self.eventSink) { + NSImage* image = [source UpdateThumbnail]; + NSData* data = [[NSData alloc] init]; + if (image != nil) { + NSImage* resizedImg = [self resizeImage:image forSize:NSMakeSize(320, 180)]; + data = [resizedImg TIFFRepresentation]; + } + postEvent(self.eventSink, @{ + @"event" : @"desktopSourceAdded", + @"id" : source.sourceId, + @"name" : source.name, + @"thumbnailSize" : @{@"width" : @0, @"height" : @0}, + @"type" : source.sourceType == RTCDesktopSourceTypeScreen ? @"screen" : @"window", + @"thumbnail" : data + }); + } +} + +#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" +- (void)didDesktopSourceRemoved:(RTC_OBJC_TYPE(RTCDesktopSource) *)source { + // NSLog(@"didDesktopSourceRemoved: %@, id %@", source.name, source.sourceId); + if (self.eventSink) { + postEvent(self.eventSink, @{ + @"event" : @"desktopSourceRemoved", + @"id" : source.sourceId, + }); + } +} + +#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" +- (void)didDesktopSourceNameChanged:(RTC_OBJC_TYPE(RTCDesktopSource) *)source { + // NSLog(@"didDesktopSourceNameChanged: %@, id %@", source.name, source.sourceId); + if (self.eventSink) { + postEvent(self.eventSink, @{ + @"event" : @"desktopSourceNameChanged", + @"id" : source.sourceId, + @"name" : source.name, + }); + } +} + +#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" +- (void)didDesktopSourceThumbnailChanged:(RTC_OBJC_TYPE(RTCDesktopSource) *)source { + // NSLog(@"didDesktopSourceThumbnailChanged: %@, id %@", source.name, source.sourceId); + if (self.eventSink) { + NSImage* resizedImg = [self resizeImage:[source thumbnail] forSize:NSMakeSize(320, 180)]; + NSData* data = [resizedImg TIFFRepresentation]; + postEvent(self.eventSink, @{ + @"event" : @"desktopSourceThumbnailChanged", + @"id" : source.sourceId, + @"thumbnail" : data + }); + } +} + +#pragma mark - RTCDesktopCapturerDelegate delegate + +- (void)didSourceCaptureStart:(RTCDesktopCapturer*)capturer { + NSLog(@"didSourceCaptureStart"); +} + +- (void)didSourceCapturePaused:(RTCDesktopCapturer*)capturer { + NSLog(@"didSourceCapturePaused"); +} + +- (void)didSourceCaptureStop:(RTCDesktopCapturer*)capturer { + NSLog(@"didSourceCaptureStop"); +} + +- (void)didSourceCaptureError:(RTCDesktopCapturer*)capturer { + NSLog(@"didSourceCaptureError"); +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCFrameCapturer.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCFrameCapturer.m new file mode 100644 index 0000000000..e986d77df9 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCFrameCapturer.m @@ -0,0 +1,161 @@ +#import "include/stream_webrtc_flutter/FlutterRTCFrameCapturer.h" +#import + +@import CoreImage; +@import CoreVideo; + +@implementation FlutterRTCFrameCapturer { + RTCVideoTrack* _track; + NSString* _path; + FlutterResult _result; + bool _gotFrame; +} + +- (instancetype)initWithTrack:(RTCVideoTrack*)track + toPath:(NSString*)path + result:(FlutterResult)result { + self = [super init]; + if (self) { + _gotFrame = false; + _track = track; + _path = path; + _result = result; + [track addRenderer:self]; + } + return self; +} + +- (void)setSize:(CGSize)size { +} + +- (void)renderFrame:(nullable RTCVideoFrame*)frame { + if (_gotFrame || frame == nil) + return; + _gotFrame = true; + id buffer = frame.buffer; + CVPixelBufferRef pixelBufferRef; + bool shouldRelease; + if (![buffer isKindOfClass:[RTCCVPixelBuffer class]]) { + pixelBufferRef = [FlutterRTCFrameCapturer convertToCVPixelBuffer:frame]; + shouldRelease = true; + } else { + pixelBufferRef = ((RTCCVPixelBuffer*)buffer).pixelBuffer; + shouldRelease = false; + } + CIImage* ciImage = [CIImage imageWithCVPixelBuffer:pixelBufferRef]; + CGRect outputSize; + if (@available(iOS 11, macOS 10.13, *)) { + switch (frame.rotation) { + case RTCVideoRotation_90: + ciImage = [ciImage imageByApplyingCGOrientation:kCGImagePropertyOrientationRight]; + outputSize = CGRectMake(0, 0, frame.height, frame.width); + break; + case RTCVideoRotation_180: + ciImage = [ciImage imageByApplyingCGOrientation:kCGImagePropertyOrientationDown]; + outputSize = CGRectMake(0, 0, frame.width, frame.height); + break; + case RTCVideoRotation_270: + ciImage = [ciImage imageByApplyingCGOrientation:kCGImagePropertyOrientationLeft]; + outputSize = CGRectMake(0, 0, frame.height, frame.width); + break; + default: + outputSize = CGRectMake(0, 0, frame.width, frame.height); + break; + } + } else { + outputSize = CGRectMake(0, 0, frame.width, frame.height); + } + CIContext* tempContext = [CIContext contextWithOptions:nil]; + CGImageRef cgImage = [tempContext createCGImage:ciImage fromRect:outputSize]; + NSData* imageData; + NSBitmapImageRep* newRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage]; + [newRep setSize:NSSizeToCGSize(outputSize.size)]; + NSDictionary* quality = @{NSImageCompressionFactor : @1.0f}; + if ([[_path pathExtension] isEqualToString:@"jpg"]) { + imageData = [newRep representationUsingType:NSBitmapImageFileTypeJPEG properties:quality]; + } else { + imageData = [newRep representationUsingType:NSBitmapImageFileTypePNG properties:quality]; + } + CGImageRelease(cgImage); + if (shouldRelease) + CVPixelBufferRelease(pixelBufferRef); + if (imageData && [imageData writeToFile:_path atomically:NO]) { + NSLog(@"File writed successfully to %@", _path); + _result(nil); + } else { + NSLog(@"Failed to write to file"); + _result([FlutterError errorWithCode:@"CaptureFrameFailed" + message:@"Failed to write image data to file" + details:nil]); + } + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_track removeRenderer:self]; + self->_track = nil; + }); +} + ++ (CVPixelBufferRef)convertToCVPixelBuffer:(RTCVideoFrame*)frame { + id i420Buffer = [frame.buffer toI420]; + CVPixelBufferRef outputPixelBuffer; + size_t w = (size_t)roundf(i420Buffer.width); + size_t h = (size_t)roundf(i420Buffer.height); + NSDictionary* pixelAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}}; + CVPixelBufferCreate(kCFAllocatorDefault, w, h, kCVPixelFormatType_32BGRA, + (__bridge CFDictionaryRef)(pixelAttributes), &outputPixelBuffer); + CVPixelBufferLockBaseAddress(outputPixelBuffer, 0); + const OSType pixelFormat = CVPixelBufferGetPixelFormatType(outputPixelBuffer); + if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { + // NV12 + uint8_t* dstY = CVPixelBufferGetBaseAddressOfPlane(outputPixelBuffer, 0); + const size_t dstYStride = CVPixelBufferGetBytesPerRowOfPlane(outputPixelBuffer, 0); + uint8_t* dstUV = CVPixelBufferGetBaseAddressOfPlane(outputPixelBuffer, 1); + const size_t dstUVStride = CVPixelBufferGetBytesPerRowOfPlane(outputPixelBuffer, 1); + + [RTCYUVHelper I420ToNV12:i420Buffer.dataY + srcStrideY:i420Buffer.strideY + srcU:i420Buffer.dataU + srcStrideU:i420Buffer.strideU + srcV:i420Buffer.dataV + srcStrideV:i420Buffer.strideV + dstY:dstY + dstStrideY:(int)dstYStride + dstUV:dstUV + dstStrideUV:(int)dstUVStride + width:i420Buffer.width + height:i420Buffer.height]; + } else { + uint8_t* dst = CVPixelBufferGetBaseAddress(outputPixelBuffer); + const size_t bytesPerRow = CVPixelBufferGetBytesPerRow(outputPixelBuffer); + + if (pixelFormat == kCVPixelFormatType_32BGRA) { + // Corresponds to libyuv::FOURCC_ARGB + [RTCYUVHelper I420ToARGB:i420Buffer.dataY + srcStrideY:i420Buffer.strideY + srcU:i420Buffer.dataU + srcStrideU:i420Buffer.strideU + srcV:i420Buffer.dataV + srcStrideV:i420Buffer.strideV + dstARGB:dst + dstStrideARGB:(int)bytesPerRow + width:i420Buffer.width + height:i420Buffer.height]; + } else if (pixelFormat == kCVPixelFormatType_32ARGB) { + // Corresponds to libyuv::FOURCC_BGRA + [RTCYUVHelper I420ToBGRA:i420Buffer.dataY + srcStrideY:i420Buffer.strideY + srcU:i420Buffer.dataU + srcStrideU:i420Buffer.strideU + srcV:i420Buffer.dataV + srcStrideV:i420Buffer.strideV + dstBGRA:dst + dstStrideBGRA:(int)bytesPerRow + width:i420Buffer.width + height:i420Buffer.height]; + } + } + CVPixelBufferUnlockBaseAddress(outputPixelBuffer, 0); + return outputPixelBuffer; +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCMediaStream.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCMediaStream.m new file mode 100644 index 0000000000..a236df18c6 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCMediaStream.m @@ -0,0 +1,1037 @@ +#import "include/stream_webrtc_flutter/FlutterRTCMediaStream.h" +#import +#import "AVKit/AVKit.h" +#import "include/stream_webrtc_flutter/CameraUtils.h" +#import "include/stream_webrtc_flutter/FlutterRTCFrameCapturer.h" +#import "include/stream_webrtc_flutter/FlutterRTCPeerConnection.h" +#import "include/stream_webrtc_flutter/LocalAudioTrack.h" +#import "include/stream_webrtc_flutter/LocalVideoTrack.h" +#import "include/stream_webrtc_flutter/NativePeerConnectionFactory.h" +#import "include/stream_webrtc_flutter/StreamMacAudioDevices.h" +#import "include/stream_webrtc_flutter/VideoProcessingAdapter.h" + +@implementation RTCMediaStreamTrack (Flutter) + +- (id)settings { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setSettings:(id)settings { + objc_setAssociatedObject(self, @selector(settings), settings, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} +@end + +@implementation AVCaptureDevice (Flutter) + +- (NSString*)positionString { + switch (self.position) { + case AVCaptureDevicePositionUnspecified: + return @"unspecified"; + case AVCaptureDevicePositionBack: + return @"back"; + case AVCaptureDevicePositionFront: + return @"front"; + } + return nil; +} + +@end + +@implementation FlutterWebRTCPlugin (RTCMediaStream) + +/** + * {@link https://www.w3.org/TR/mediacapture-streams/#navigatorusermediaerrorcallback} + */ +typedef void (^NavigatorUserMediaErrorCallback)(NSString* errorType, NSString* errorMessage); + +/** + * {@link https://www.w3.org/TR/mediacapture-streams/#navigatorusermediasuccesscallback} + */ +typedef void (^NavigatorUserMediaSuccessCallback)(RTCMediaStream* mediaStream); + +- (NSDictionary*)defaultVideoConstraints { + return @{@"minWidth" : @"1280", @"minHeight" : @"720", @"minFrameRate" : @"30"}; +} + +- (NSDictionary*)defaultAudioConstraints { + return @{}; +} + +- (RTCMediaConstraints*)defaultMediaStreamConstraints { + RTCMediaConstraints* constraints = + [[RTCMediaConstraints alloc] initWithMandatoryConstraints:[self defaultVideoConstraints] + optionalConstraints:nil]; + return constraints; +} + +- (NSArray*)captureDevices { + if (@available(iOS 13.0, macOS 10.15, macCatalyst 14.0, tvOS 17.0, *)) { + NSArray* deviceTypes = @[ + AVCaptureDeviceTypeBuiltInWideAngleCamera, + ]; + + if (@available(macOS 13.0, *)) { + deviceTypes = [deviceTypes arrayByAddingObject:AVCaptureDeviceTypeDeskViewCamera]; + } + + if (@available(iOS 17.0, macOS 14.0, tvOS 17.0, *)) { + deviceTypes = [deviceTypes arrayByAddingObjectsFromArray:@[ + AVCaptureDeviceTypeContinuityCamera, + AVCaptureDeviceTypeExternal, + ]]; + } + + return [AVCaptureDeviceDiscoverySession + discoverySessionWithDeviceTypes:deviceTypes + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified] + .devices; + } + return @[]; +} + +/** + * Initializes a new {@link RTCAudioTrack} which satisfies specific constraints, + * adds it to a specific {@link RTCMediaStream}, and reports success to a + * specific callback. Implements the audio-specific counterpart of the + * {@code getUserMedia()} algorithm. + * + * @param constraints The {@code MediaStreamConstraints} which the new + * {@code RTCAudioTrack} instance is to satisfy. + * @param successCallback The {@link NavigatorUserMediaSuccessCallback} to which + * success is to be reported. + * @param errorCallback The {@link NavigatorUserMediaErrorCallback} to which + * failure is to be reported. + * @param mediaStream The {@link RTCMediaStream} which is being initialized as + * part of the execution of the {@code getUserMedia()} algorithm, to which a + * new {@code RTCAudioTrack} is to be added, and which is to be reported to + * {@code successCallback} upon success. + */ +- (void)getUserAudio:(NSDictionary*)constraints + successCallback:(NavigatorUserMediaSuccessCallback)successCallback + errorCallback:(NavigatorUserMediaErrorCallback)errorCallback + mediaStream:(RTCMediaStream*)mediaStream + factoryId:(NSString*)factoryId { + id audioConstraints = constraints[@"audio"]; + NSString* audioDeviceId = @""; + RTCMediaConstraints* rtcConstraints; + if ([audioConstraints isKindOfClass:[NSDictionary class]]) { + // constraints.audio.deviceId + NSString* deviceId = audioConstraints[@"deviceId"]; + + if (deviceId) { + audioDeviceId = deviceId; + } + + rtcConstraints = [self parseMediaConstraints:audioConstraints]; + // constraints.audio.optional.sourceId + id optionalConstraints = audioConstraints[@"optional"]; + if (optionalConstraints && [optionalConstraints isKindOfClass:[NSArray class]] && !deviceId) { + NSArray* options = optionalConstraints; + for (id item in options) { + if ([item isKindOfClass:[NSDictionary class]]) { + NSString* sourceId = ((NSDictionary*)item)[@"sourceId"]; + if (sourceId) { + audioDeviceId = sourceId; + } + } + } + } + } else { + rtcConstraints = [self parseMediaConstraints:[self defaultAudioConstraints]]; + } + + if (audioDeviceId != nil) { + [self selectAudioInput:audioDeviceId result:nil]; + } + + NSString* trackId = [[NSUUID UUID] UUIDString]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil || nf.factory == nil) { + errorCallback(@"OverconstrainedError", + [NSString stringWithFormat:@"unknown factoryId %@", factoryId]); + return; + } + RTCAudioSource* audioSource = [nf.factory audioSourceWithConstraints:rtcConstraints]; + RTCAudioTrack* audioTrack = [nf.factory audioTrackWithSource:audioSource trackId:trackId]; + LocalAudioTrack* localAudioTrack = [[LocalAudioTrack alloc] initWithTrack:audioTrack]; + + audioTrack.settings = @{ + @"deviceId" : audioDeviceId, + @"kind" : @"audioinput", + @"autoGainControl" : @YES, + @"echoCancellation" : @YES, + @"noiseSuppression" : @YES, + @"channelCount" : @1, + @"latency" : @0, + }; + + [mediaStream addAudioTrack:audioTrack]; + + [self.localTracks setObject:localAudioTrack forKey:trackId]; + + if (nf.factoryId != nil) { + self.trackFactoryId[trackId] = nf.factoryId; + [nf.ownedTrackIds addObject:trackId]; + } + + [self ensureAudioSession]; + + successCallback(mediaStream); +} + +// TODO: Use RCTConvert for constraints ... +- (void)getUserMedia:(NSDictionary*)constraints + factoryId:(NSString*)factoryId + result:(FlutterResult)result { + // Initialize RTCMediaStream with a unique label in order to allow multiple + // RTCMediaStream instances initialized by multiple getUserMedia calls to be + // added to 1 RTCPeerConnection instance. As suggested by + // https://www.w3.org/TR/mediacapture-streams/#mediastream to be a good + // practice, use a UUID (conforming to RFC4122). + NSString* mediaStreamId = [[NSUUID UUID] UUIDString]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil || nf.factory == nil) { + result([FlutterError + errorWithCode:@"getUserMedia" + message:[NSString stringWithFormat:@"unknown factoryId %@", factoryId] + details:nil]); + return; + } + RTCMediaStream* mediaStream = [nf.factory mediaStreamWithStreamId:mediaStreamId]; + + [self getUserMedia:constraints + factoryId:factoryId + successCallback:^(RTCMediaStream* mediaStream) { + NSString* mediaStreamId = mediaStream.streamId; + + NSMutableArray* audioTracks = [NSMutableArray array]; + NSMutableArray* videoTracks = [NSMutableArray array]; + + for (RTCAudioTrack* track in mediaStream.audioTracks) { + [audioTracks addObject:@{ + @"id" : track.trackId, + @"kind" : track.kind, + @"label" : track.trackId, + @"enabled" : @(track.isEnabled), + @"remote" : @(YES), + @"readyState" : @"live", + @"settings" : track.settings + }]; + } + + for (RTCVideoTrack* track in mediaStream.videoTracks) { + [videoTracks addObject:@{ + @"id" : track.trackId, + @"kind" : track.kind, + @"label" : track.trackId, + @"enabled" : @(track.isEnabled), + @"remote" : @(YES), + @"readyState" : @"live", + @"settings" : track.settings + }]; + } + + self.localStreams[mediaStreamId] = mediaStream; + [nf.ownedStreamIds addObject:mediaStreamId]; + result(@{ + @"streamId" : mediaStreamId, + @"audioTracks" : audioTracks, + @"videoTracks" : videoTracks + }); + } + errorCallback:^(NSString* errorType, NSString* errorMessage) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"Error %@", errorType] + message:errorMessage + details:nil]); + } + mediaStream:mediaStream]; +} + +/** + * Initializes a new {@link RTCAudioTrack} or a new {@link RTCVideoTrack} which + * satisfies specific constraints and adds it to a specific + * {@link RTCMediaStream} if the specified {@code mediaStream} contains no track + * of the respective media type and the specified {@code constraints} specify + * that a track of the respective media type is required; otherwise, reports + * success for the specified {@code mediaStream} to a specific + * {@link NavigatorUserMediaSuccessCallback}. In other words, implements a media + * type-specific iteration of or successfully concludes the + * {@code getUserMedia()} algorithm. The method will be recursively invoked to + * conclude the whole {@code getUserMedia()} algorithm either with (successful) + * satisfaction of the specified {@code constraints} or with failure. + * + * @param constraints The {@code MediaStreamConstraints} which specifies the + * requested media types and which the new {@code RTCAudioTrack} or + * {@code RTCVideoTrack} instance is to satisfy. + * @param successCallback The {@link NavigatorUserMediaSuccessCallback} to which + * success is to be reported. + * @param errorCallback The {@link NavigatorUserMediaErrorCallback} to which + * failure is to be reported. + * @param mediaStream The {@link RTCMediaStream} which is being initialized as + * part of the execution of the {@code getUserMedia()} algorithm. + */ +- (void)getUserMedia:(NSDictionary*)constraints + factoryId:(NSString*)factoryId + successCallback:(NavigatorUserMediaSuccessCallback)successCallback + errorCallback:(NavigatorUserMediaErrorCallback)errorCallback + mediaStream:(RTCMediaStream*)mediaStream { + // If mediaStream contains no audioTracks and the constraints request such a + // track, then run an iteration of the getUserMedia() algorithm to obtain + // local audio content. + if (mediaStream.audioTracks.count == 0) { + // constraints.audio + id audioConstraints = constraints[@"audio"]; + BOOL constraintsIsDictionary = [audioConstraints isKindOfClass:[NSDictionary class]]; + if (audioConstraints && (constraintsIsDictionary || [audioConstraints boolValue])) { + [self requestAccessForMediaType:AVMediaTypeAudio + constraints:constraints + successCallback:successCallback + errorCallback:errorCallback + mediaStream:mediaStream + factoryId:factoryId]; + return; + } + } + + // If mediaStream contains no videoTracks and the constraints request such a + // track, then run an iteration of the getUserMedia() algorithm to obtain + // local video content. + if (mediaStream.videoTracks.count == 0) { + // constraints.video + id videoConstraints = constraints[@"video"]; + if (videoConstraints) { + BOOL requestAccessForVideo = [videoConstraints isKindOfClass:[NSNumber class]] + ? [videoConstraints boolValue] + : [videoConstraints isKindOfClass:[NSDictionary class]]; + if (requestAccessForVideo) { + [self requestAccessForMediaType:AVMediaTypeVideo + constraints:constraints + successCallback:successCallback + errorCallback:errorCallback + mediaStream:mediaStream + factoryId:factoryId]; + return; + } + } + } + + // There are audioTracks and/or videoTracks in mediaStream as requested by + // constraints so the getUserMedia() is to conclude with success. + successCallback(mediaStream); +} + +- (int)getConstrainInt:(NSDictionary*)constraints forKey:(NSString*)key { + if (![constraints isKindOfClass:[NSDictionary class]]) { + return 0; + } + + id constraint = constraints[key]; + if ([constraint isKindOfClass:[NSNumber class]]) { + return [constraint intValue]; + } else if ([constraint isKindOfClass:[NSString class]]) { + int possibleValue = [constraint intValue]; + if (possibleValue != 0) { + return possibleValue; + } + } else if ([constraint isKindOfClass:[NSDictionary class]]) { + id idealConstraint = constraint[@"ideal"]; + if ([idealConstraint isKindOfClass:[NSString class]]) { + int possibleValue = [idealConstraint intValue]; + if (possibleValue != 0) { + return possibleValue; + } + } + } + + return 0; +} + +- (RTCMediaStreamTrack*)cloneTrack:(nonnull NSString*)trackId { + NSString* newTrackId = [[NSUUID UUID] UUIDString]; + + RTCMediaStreamTrack* originalTrack = [self trackForId:trackId peerConnectionId:nil]; + LocalVideoTrack* originalLocalTrack = self.localTracks[trackId]; + + // Resolve the same factory the original track was made with so the clone + // shares the underlying source's factory affinity. + NSString* sourceFactoryId = self.trackFactoryId[trackId]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:sourceFactoryId]; + if (nf == nil || nf.factory == nil) { + NSLog(@"[cloneTrack] no factory for trackId %@; refusing to clone", trackId); + return nil; + } + + if (originalTrack != nil && [originalTrack.kind isEqualToString:@"audio"]) { + RTCAudioTrack* originalAudioTrack = (RTCAudioTrack*)originalTrack; + RTCAudioSource* originalAudioSource = originalAudioTrack.source; + + RTCAudioTrack* audioTrack = [nf.factory audioTrackWithSource:originalAudioSource + trackId:newTrackId]; + LocalAudioTrack* localAudioTrack = [[LocalAudioTrack alloc] initWithTrack:audioTrack]; + + audioTrack.settings = originalAudioTrack.settings; + [self.localTracks setObject:localAudioTrack forKey:newTrackId]; + if (nf.factoryId != nil) { + self.trackFactoryId[newTrackId] = nf.factoryId; + [nf.ownedTrackIds addObject:newTrackId]; + } + + for (NSString* streamId in self.localStreams) { + RTCMediaStream* stream = [self.localStreams objectForKey:streamId]; + for (RTCAudioTrack* track in stream.audioTracks) { + if ([trackId isEqualToString:track.trackId]) { + [stream addAudioTrack:audioTrack]; + } + } + } + + return audioTrack; + } else if (originalTrack != nil && [originalTrack.kind isEqualToString:@"video"]) { + RTCVideoTrack* originalVideoTrack = (RTCVideoTrack*)originalTrack; + RTCVideoSource* videoSource = originalVideoTrack.source; + + RTCVideoTrack* videoTrack = [nf.factory videoTrackWithSource:videoSource trackId:newTrackId]; + LocalVideoTrack* localVideoTrack = + [[LocalVideoTrack alloc] initWithTrack:videoTrack + videoProcessing:originalLocalTrack.processing]; + + videoTrack.settings = originalVideoTrack.settings; + [self.localTracks setObject:localVideoTrack forKey:newTrackId]; + if (nf.factoryId != nil) { + self.trackFactoryId[newTrackId] = nf.factoryId; + [nf.ownedTrackIds addObject:newTrackId]; + } + + for (NSString* streamId in self.localStreams) { + RTCMediaStream* stream = [self.localStreams objectForKey:streamId]; + for (RTCVideoTrack* track in stream.videoTracks) { + if ([trackId isEqualToString:track.trackId]) { + [stream addVideoTrack:videoTrack]; + } + } + } + + return videoTrack; + } + + return originalTrack; +} + +/** + * Initializes a new {@link RTCVideoTrack} which satisfies specific constraints, + * adds it to a specific {@link RTCMediaStream}, and reports success to a + * specific callback. Implements the video-specific counterpart of the + * {@code getUserMedia()} algorithm. + * + * @param constraints The {@code MediaStreamConstraints} which the new + * {@code RTCVideoTrack} instance is to satisfy. + * @param successCallback The {@link NavigatorUserMediaSuccessCallback} to which + * success is to be reported. + * @param errorCallback The {@link NavigatorUserMediaErrorCallback} to which + * failure is to be reported. + * @param mediaStream The {@link RTCMediaStream} which is being initialized as + * part of the execution of the {@code getUserMedia()} algorithm, to which a + * new {@code RTCVideoTrack} is to be added, and which is to be reported to + * {@code successCallback} upon success. + */ +- (void)getUserVideo:(NSDictionary*)constraints + successCallback:(NavigatorUserMediaSuccessCallback)successCallback + errorCallback:(NavigatorUserMediaErrorCallback)errorCallback + mediaStream:(RTCMediaStream*)mediaStream + factoryId:(NSString*)factoryId { + id videoConstraints = constraints[@"video"]; + AVCaptureDevice* videoDevice; + NSString* videoDeviceId = nil; + NSString* facingMode = nil; + AVCaptureDevicePosition position = AVCaptureDevicePositionUnspecified; + NSArray* captureDevices = [self captureDevices]; + + if ([videoConstraints isKindOfClass:[NSDictionary class]]) { + // constraints.video.deviceId + NSString* deviceId = videoConstraints[@"deviceId"]; + + if (deviceId) { + for (AVCaptureDevice* device in captureDevices) { + if ([deviceId isEqualToString:device.uniqueID]) { + videoDevice = device; + videoDeviceId = deviceId; + } + } + } + + // constraints.video.optional + id optionalVideoConstraints = videoConstraints[@"optional"]; + if (optionalVideoConstraints && [optionalVideoConstraints isKindOfClass:[NSArray class]] && + !videoDevice) { + NSArray* options = optionalVideoConstraints; + for (id item in options) { + if ([item isKindOfClass:[NSDictionary class]]) { + NSString* sourceId = ((NSDictionary*)item)[@"sourceId"]; + if (sourceId) { + for (AVCaptureDevice* device in captureDevices) { + if ([sourceId isEqualToString:device.uniqueID]) { + videoDevice = device; + videoDeviceId = sourceId; + } + } + if (videoDevice) { + break; + } + } + } + } + } + + if (!videoDevice) { + // constraints.video.facingMode + // https://www.w3.org/TR/mediacapture-streams/#def-constraint-facingMode + facingMode = videoConstraints[@"facingMode"]; + if (facingMode && [facingMode isKindOfClass:[NSString class]]) { + if ([facingMode isEqualToString:@"environment"]) { + self._usingFrontCamera = NO; + position = AVCaptureDevicePositionBack; + } else if ([facingMode isEqualToString:@"user"]) { + self._usingFrontCamera = YES; + position = AVCaptureDevicePositionFront; + } else { + // If the specified facingMode value is not supported, fall back to + // the default video device. + self._usingFrontCamera = NO; + position = AVCaptureDevicePositionUnspecified; + } + videoDevice = [self findDeviceForPosition:position]; + } + } + } + + if ([videoConstraints isKindOfClass:[NSNumber class]]) { + videoConstraints = @{@"mandatory" : [self defaultVideoConstraints]}; + } + + NSInteger targetWidth = 0; + NSInteger targetHeight = 0; + NSInteger targetFps = 0; + + if (!videoDevice) { + videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + } + + if (videoDevice && position == AVCaptureDevicePositionUnspecified) { + position = videoDevice.position; + } + + int possibleWidth = [self getConstrainInt:videoConstraints forKey:@"width"]; + if (possibleWidth != 0) { + targetWidth = possibleWidth; + } + + int possibleHeight = [self getConstrainInt:videoConstraints forKey:@"height"]; + if (possibleHeight != 0) { + targetHeight = possibleHeight; + } + + int possibleFps = [self getConstrainInt:videoConstraints forKey:@"frameRate"]; + if (possibleFps != 0) { + targetFps = possibleFps; + } + + id mandatory = + [videoConstraints isKindOfClass:[NSDictionary class]] ? videoConstraints[@"mandatory"] : nil; + + // constraints.video.mandatory + if (mandatory && [mandatory isKindOfClass:[NSDictionary class]]) { + id widthConstraint = mandatory[@"minWidth"]; + if ([widthConstraint isKindOfClass:[NSString class]] || + [widthConstraint isKindOfClass:[NSNumber class]]) { + int possibleWidth = [widthConstraint intValue]; + if (possibleWidth != 0) { + targetWidth = possibleWidth; + } + } + id heightConstraint = mandatory[@"minHeight"]; + if ([heightConstraint isKindOfClass:[NSString class]] || + [heightConstraint isKindOfClass:[NSNumber class]]) { + int possibleHeight = [heightConstraint intValue]; + if (possibleHeight != 0) { + targetHeight = possibleHeight; + } + } + id fpsConstraint = mandatory[@"minFrameRate"]; + if ([fpsConstraint isKindOfClass:[NSString class]] || + [fpsConstraint isKindOfClass:[NSNumber class]]) { + int possibleFps = [fpsConstraint intValue]; + if (possibleFps != 0) { + targetFps = possibleFps; + } + } + } + + if (videoDevice) { + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil || nf.factory == nil) { + errorCallback(@"OverconstrainedError", + [NSString stringWithFormat:@"unknown factoryId %@", factoryId]); + return; + } + RTCVideoSource* videoSource = [nf.factory videoSource]; + if (self.videoCapturer) { + [self.videoCapturer stopCapture]; + } + + VideoProcessingAdapter* videoProcessingAdapter = + [[VideoProcessingAdapter alloc] initWithRTCVideoSource:videoSource]; + self.videoCapturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoProcessingAdapter]; + + AVCaptureDeviceFormat* selectedFormat = [self selectFormatForDevice:videoDevice + targetWidth:targetWidth + targetHeight:targetHeight]; + + CMVideoDimensions selectedDimension = + CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription); + NSInteger selectedWidth = (NSInteger)selectedDimension.width; + NSInteger selectedHeight = (NSInteger)selectedDimension.height; + NSInteger selectedFps = [self selectFpsForFormat:selectedFormat targetFps:targetFps]; + + NSLog(@"target format %ldx%ld, targetFps: %ld, selected format: %ldx%ld, selected fps %ld", + targetWidth, targetHeight, targetFps, selectedWidth, selectedHeight, selectedFps); + + if ([videoDevice lockForConfiguration:NULL]) { + @try { + videoDevice.activeVideoMaxFrameDuration = CMTimeMake(1, (int32_t)selectedFps); + videoDevice.activeVideoMinFrameDuration = CMTimeMake(1, (int32_t)selectedFps); + } @catch (NSException* exception) { + NSLog(@"Failed to set active frame rate!\n User info:%@", exception.userInfo); + } + [videoDevice unlockForConfiguration]; + } + + [self.videoCapturer startCaptureWithDevice:videoDevice + format:selectedFormat + fps:selectedFps + completionHandler:^(NSError* error) { + if (error) { + NSLog(@"Start capture error: %@", [error localizedDescription]); + } + }]; + + NSString* trackUUID = [[NSUUID UUID] UUIDString]; + RTCVideoTrack* videoTrack = [nf.factory videoTrackWithSource:videoSource trackId:trackUUID]; + LocalVideoTrack* localVideoTrack = + [[LocalVideoTrack alloc] initWithTrack:videoTrack videoProcessing:videoProcessingAdapter]; + + if (nf.factoryId != nil) { + self.trackFactoryId[trackUUID] = nf.factoryId; + [nf.ownedTrackIds addObject:trackUUID]; + } + + __weak RTCCameraVideoCapturer* capturer = self.videoCapturer; + + // Store camera state per track + NSMutableDictionary* captureState = [NSMutableDictionary new]; + captureState[@"usingFrontCamera"] = @(position == AVCaptureDevicePositionFront); + captureState[@"targetWidth"] = @(targetWidth); + captureState[@"targetHeight"] = @(targetHeight); + captureState[@"targetFps"] = @(selectedFps); + self.videoCaptureState[videoTrack.trackId] = captureState; + + // Update global state for backward compatibility + self._usingFrontCamera = (position == AVCaptureDevicePositionFront); + self._lastTargetFps = selectedFps; + self._lastTargetWidth = targetWidth; + self._lastTargetHeight = targetHeight; + + self.videoCapturerStopHandlers[videoTrack.trackId] = ^(CompletionHandler handler) { + NSLog(@"Stop video capturer, trackID %@", videoTrack.trackId); + [capturer stopCaptureWithCompletionHandler:handler]; + }; + + if (!videoDeviceId) { + videoDeviceId = videoDevice.uniqueID; + } + + if (!facingMode) { + facingMode = videoDevice.position == AVCaptureDevicePositionBack ? @"environment" + : videoDevice.position == AVCaptureDevicePositionFront ? @"user" + : @"unspecified"; + } + + videoTrack.settings = @{ + @"deviceId" : videoDeviceId, + @"kind" : @"videoinput", + @"width" : [NSNumber numberWithInteger:selectedWidth], + @"height" : [NSNumber numberWithInteger:selectedHeight], + @"frameRate" : [NSNumber numberWithInteger:selectedFps], + @"facingMode" : facingMode, + }; + + [mediaStream addVideoTrack:videoTrack]; + + [self.localTracks setObject:localVideoTrack forKey:trackUUID]; + + successCallback(mediaStream); + } else { + // According to step 6.2.3 of the getUserMedia() algorithm, if there is no + // source, fail with a new OverconstrainedError. + errorCallback(@"OverconstrainedError", /* errorMessage */ nil); + } +} + +- (void)mediaStreamRelease:(RTCMediaStream*)stream { + if (stream) { + for (RTCVideoTrack* track in stream.videoTracks) { + [self.localTracks removeObjectForKey:track.trackId]; + } + for (RTCAudioTrack* track in stream.audioTracks) { + [self.localTracks removeObjectForKey:track.trackId]; + } + [self.localStreams removeObjectForKey:stream.streamId]; + } +} + +/** + * Obtains local media content of a specific type. Requests access for the + * specified {@code mediaType} if necessary. In other words, implements a media + * type-specific iteration of the {@code getUserMedia()} algorithm. + * + * @param mediaType Either {@link AVMediaTypAudio} or {@link AVMediaTypeVideo} + * which specifies the type of the local media content to obtain. + * @param constraints The {@code MediaStreamConstraints} which are to be + * satisfied by the obtained local media content. + * @param successCallback The {@link NavigatorUserMediaSuccessCallback} to which + * success is to be reported. + * @param errorCallback The {@link NavigatorUserMediaErrorCallback} to which + * failure is to be reported. + * @param mediaStream The {@link RTCMediaStream} which is to collect the + * obtained local media content of the specified {@code mediaType}. + */ +- (void)requestAccessForMediaType:(NSString*)mediaType + constraints:(NSDictionary*)constraints + successCallback:(NavigatorUserMediaSuccessCallback)successCallback + errorCallback:(NavigatorUserMediaErrorCallback)errorCallback + mediaStream:(RTCMediaStream*)mediaStream + factoryId:(NSString*)factoryId { + // According to step 6.2.1 of the getUserMedia() algorithm, if there is no + // source, fail "with a new DOMException object whose name attribute has the + // value NotFoundError." + // XXX The following approach does not work for audio in Simulator. That is + // because audio capture is done using AVAudioSession which does not use + // AVCaptureDevice there. Anyway, Simulator will not (visually) request access + // for audio. + if (mediaType == AVMediaTypeVideo && [self captureDevices].count == 0) { + // Since successCallback and errorCallback are asynchronously invoked + // elsewhere, make sure that the invocation here is consistent. + dispatch_async(dispatch_get_main_queue(), ^{ + errorCallback(@"DOMException", @"NotFoundError"); + }); + return; + } + + if (@available(macOS 10.14, *)) { + [AVCaptureDevice requestAccessForMediaType:mediaType + completionHandler:^(BOOL granted) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (granted) { + NavigatorUserMediaSuccessCallback scb = + ^(RTCMediaStream* mediaStream) { + [self getUserMedia:constraints + factoryId:factoryId + successCallback:successCallback + errorCallback:errorCallback + mediaStream:mediaStream]; + }; + + if (mediaType == AVMediaTypeAudio) { + [self getUserAudio:constraints + successCallback:scb + errorCallback:errorCallback + mediaStream:mediaStream + factoryId:factoryId]; + } else if (mediaType == AVMediaTypeVideo) { + [self getUserVideo:constraints + successCallback:scb + errorCallback:errorCallback + mediaStream:mediaStream + factoryId:factoryId]; + } + } else { + // According to step 10 Permission Failure of the getUserMedia() + // algorithm, if the user has denied permission, fail "with a new + // DOMException object whose name attribute has the value + // NotAllowedError." + errorCallback(@"DOMException", @"NotAllowedError"); + } + }); + }]; + } else { + // Fallback on earlier versions + NavigatorUserMediaSuccessCallback scb = ^(RTCMediaStream* mediaStream) { + [self getUserMedia:constraints + factoryId:factoryId + successCallback:successCallback + errorCallback:errorCallback + mediaStream:mediaStream]; + }; + if (mediaType == AVMediaTypeAudio) { + [self getUserAudio:constraints + successCallback:scb + errorCallback:errorCallback + mediaStream:mediaStream + factoryId:factoryId]; + } else if (mediaType == AVMediaTypeVideo) { + [self getUserVideo:constraints + successCallback:scb + errorCallback:errorCallback + mediaStream:mediaStream + factoryId:factoryId]; + } + } +} + +- (void)createLocalMediaStream:(NSString*)factoryId result:(FlutterResult)result { + NSString* mediaStreamId = [[NSUUID UUID] UUIDString]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil || nf.factory == nil) { + result([FlutterError + errorWithCode:@"createLocalMediaStream" + message:[NSString stringWithFormat:@"unknown factoryId %@", factoryId] + details:nil]); + return; + } + RTCMediaStream* mediaStream = [nf.factory mediaStreamWithStreamId:mediaStreamId]; + + self.localStreams[mediaStreamId] = mediaStream; + [nf.ownedStreamIds addObject:mediaStreamId]; + result(@{@"streamId" : [mediaStream streamId]}); +} + +- (void)getSources:(FlutterResult)result { + NSMutableArray* sources = [NSMutableArray array]; + NSArray* videoDevices = [self captureDevices]; + for (AVCaptureDevice* device in videoDevices) { + [sources addObject:@{ + @"facing" : device.positionString, + @"deviceId" : device.uniqueID, + @"label" : device.localizedName, + @"kind" : @"videoinput", + }]; + } + for (NSDictionary* d in [StreamMacAudioDevices inputDevices]) { + [sources addObject:@{ + @"deviceId" : d[@"deviceId"], + @"label" : d[@"label"], + @"kind" : @"audioinput", + }]; + } + for (NSDictionary* d in [StreamMacAudioDevices outputDevices]) { + [sources addObject:@{ + @"deviceId" : d[@"deviceId"], + @"label" : d[@"label"], + @"kind" : @"audiooutput", + }]; + } + result(@{@"sources" : sources}); +} + +- (void)selectAudioInput:(NSString*)deviceId result:(FlutterResult)result { + if (![StreamMacAudioDevices deviceIdExists:deviceId asInput:YES]) { + if (result) + result([FlutterError errorWithCode:@"selectAudioInputFailed" + message:@"Error: deviceId not found!" + details:@{@"deviceId" : deviceId ?: [NSNull null]}]); + return; + } + + __block BOOL applied = NO; + @synchronized(self) { + for (NSString* fid in self.factories.allKeys) { + RTCAudioDeviceModule* adm = self.factories[fid].audioDeviceModule; + if (adm == nil) continue; + for (RTCIODevice* device in [adm inputDevices]) { + if ([deviceId isEqualToString:device.deviceId]) { + [adm setInputDevice:device]; + applied = YES; + break; + } + } + } + } + if (result) + result(applied + ? nil + : [FlutterError errorWithCode:@"selectAudioInputFailed" + message:@"No active per-call factory; " + @"join a call before selecting audio input." + details:@{@"deviceId" : deviceId}]); +} + +- (void)selectAudioOutput:(NSString*)deviceId result:(FlutterResult)result { + if (![StreamMacAudioDevices deviceIdExists:deviceId asInput:NO]) { + result([FlutterError errorWithCode:@"selectAudioOutputFailed" + message:@"Error: deviceId not found!" + details:@{@"deviceId" : deviceId ?: [NSNull null]}]); + return; + } + + __block BOOL applied = NO; + @synchronized(self) { + for (NSString* fid in self.factories.allKeys) { + RTCAudioDeviceModule* adm = self.factories[fid].audioDeviceModule; + if (adm == nil) continue; + for (RTCIODevice* device in [adm outputDevices]) { + if ([deviceId isEqualToString:device.deviceId]) { + [adm setOutputDevice:device]; + applied = YES; + break; + } + } + } + } + result(applied + ? nil + : [FlutterError errorWithCode:@"selectAudioOutputFailed" + message:@"No active per-call factory; " + @"join a call before selecting audio output." + details:@{@"deviceId" : deviceId}]); +} + +- (void)triggeriOSAudioRouteSelectionUI:(FlutterResult)result { + // macOS doesn't support iOS audio route selection UI + result([FlutterError errorWithCode:@"UnsupportedPlatformError" + message:@"triggeriOSAudioRouteSelectionUI is only supported on iOS" + details:nil]); +} + +- (void)mediaStreamTrackRelease:(RTCMediaStream*)mediaStream track:(RTCMediaStreamTrack*)track { + // what's different to mediaStreamTrackStop? only call mediaStream explicitly? + if (mediaStream && track) { + track.isEnabled = NO; + // FIXME this is called when track is removed from the MediaStream, + // but it doesn't mean it can not be added back using MediaStream.addTrack + // TODO: [self.localTracks removeObjectForKey:trackID]; + if ([track.kind isEqualToString:@"audio"]) { + [mediaStream removeAudioTrack:(RTCAudioTrack*)track]; + } else if ([track.kind isEqualToString:@"video"]) { + [mediaStream removeVideoTrack:(RTCVideoTrack*)track]; + } + } +} + +- (void)mediaStreamTrackHasTorch:(RTCMediaStreamTrack*)track result:(FlutterResult)result { + if (!self.videoCapturer) { + result(@NO); + return; + } + if (self.videoCapturer.captureSession.inputs.count == 0) { + result(@NO); + return; + } + + AVCaptureDeviceInput* deviceInput = [self.videoCapturer.captureSession.inputs objectAtIndex:0]; + AVCaptureDevice* device = deviceInput.device; + + result(@([device isTorchModeSupported:AVCaptureTorchModeOn])); +} + +- (void)mediaStreamTrackSetTorch:(RTCMediaStreamTrack*)track + torch:(BOOL)torch + result:(FlutterResult)result { + if (!self.videoCapturer) { + NSLog(@"Video capturer is null. Can't set torch"); + return; + } + if (self.videoCapturer.captureSession.inputs.count == 0) { + NSLog(@"Video capturer is missing an input. Can't set torch"); + return; + } + + AVCaptureDeviceInput* deviceInput = [self.videoCapturer.captureSession.inputs objectAtIndex:0]; + AVCaptureDevice* device = deviceInput.device; + + if (![device isTorchModeSupported:AVCaptureTorchModeOn]) { + NSLog(@"Current capture device does not support torch. Can't set torch"); + return; + } + + NSError* error; + if ([device lockForConfiguration:&error] == NO) { + NSLog(@"Failed to aquire configuration lock. %@", error.localizedDescription); + return; + } + + device.torchMode = torch ? AVCaptureTorchModeOn : AVCaptureTorchModeOff; + [device unlockForConfiguration]; + + result(nil); +} + +- (void)mediaStreamTrackSetZoom:(RTCMediaStreamTrack*)track + zoomLevel:(double)zoomLevel + result:(FlutterResult)result { + NSLog(@"Not supported on macOS. Can't set zoom"); +} + +- (void)mediaStreamTrackCaptureFrame:(RTCVideoTrack*)track + toPath:(NSString*)path + result:(FlutterResult)result { + self.frameCapturer = [[FlutterRTCFrameCapturer alloc] initWithTrack:track + toPath:path + result:result]; +} + +- (void)mediaStreamTrackStop:(RTCMediaStreamTrack*)track { + if (track) { + track.isEnabled = NO; + [self.localTracks removeObjectForKey:track.trackId]; + } +} + +- (AVCaptureDevice*)findDeviceForPosition:(AVCaptureDevicePosition)position { + if (position == AVCaptureDevicePositionUnspecified) { + return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + } + NSArray* captureDevices = [RTCCameraVideoCapturer captureDevices]; + for (AVCaptureDevice* device in captureDevices) { + if (device.position == position) { + return device; + } + } + if (captureDevices.count > 0) { + return captureDevices[0]; + } + return nil; +} + +- (AVCaptureDeviceFormat*)selectFormatForDevice:(AVCaptureDevice*)device + targetWidth:(NSInteger)targetWidth + targetHeight:(NSInteger)targetHeight { + NSArray* formats = + [RTCCameraVideoCapturer supportedFormatsForDevice:device]; + AVCaptureDeviceFormat* selectedFormat = nil; + long currentDiff = INT_MAX; + for (AVCaptureDeviceFormat* format in formats) { + CMVideoDimensions dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription); + FourCharCode pixelFormat = CMFormatDescriptionGetMediaSubType(format.formatDescription); + // NSLog(@"AVCaptureDeviceFormats,fps %d, dimension: %dx%d", + // format.videoSupportedFrameRateRanges, dimension.width, dimension.height); + long diff = labs(targetWidth - dimension.width) + labs(targetHeight - dimension.height); + if (diff < currentDiff) { + selectedFormat = format; + currentDiff = diff; + } else if (diff == currentDiff && + pixelFormat == [self.videoCapturer preferredOutputPixelFormat]) { + selectedFormat = format; + } + } + return selectedFormat; +} + +- (NSInteger)selectFpsForFormat:(AVCaptureDeviceFormat*)format targetFps:(NSInteger)targetFps { + Float64 maxSupportedFramerate = 0; + for (AVFrameRateRange* fpsRange in format.videoSupportedFrameRateRanges) { + maxSupportedFramerate = fmax(maxSupportedFramerate, fpsRange.maxFrameRate); + } + return fmin(maxSupportedFramerate, targetFps); +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCPeerConnection.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCPeerConnection.m new file mode 100644 index 0000000000..c6519d0a73 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCPeerConnection.m @@ -0,0 +1,929 @@ +#import "include/stream_webrtc_flutter/FlutterRTCPeerConnection.h" +#import +#import +#import "include/stream_webrtc_flutter/FlutterRTCDataChannel.h" +#import "include/stream_webrtc_flutter/FlutterWebRTCPlugin.h" +#import "include/stream_webrtc_flutter/NativePeerConnectionFactory.h" + +@implementation RTCPeerConnection (Flutter) + +@dynamic eventSink; + +- (NSString*)flutterId { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setFlutterId:(NSString*)flutterId { + objc_setAssociatedObject(self, @selector(flutterId), flutterId, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (FlutterEventSink)eventSink { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setEventSink:(FlutterEventSink)eventSink { + objc_setAssociatedObject(self, @selector(eventSink), eventSink, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (FlutterEventChannel*)eventChannel { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setEventChannel:(FlutterEventChannel*)eventChannel { + objc_setAssociatedObject(self, @selector(eventChannel), eventChannel, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSMutableDictionary*)dataChannels { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setDataChannels:(NSMutableDictionary*)dataChannels { + objc_setAssociatedObject(self, @selector(dataChannels), dataChannels, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSMutableDictionary*)remoteStreams { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setRemoteStreams:(NSMutableDictionary*)remoteStreams { + objc_setAssociatedObject(self, @selector(remoteStreams), remoteStreams, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSMutableDictionary*)remoteTracks { + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setRemoteTracks:(NSMutableDictionary*)remoteTracks { + objc_setAssociatedObject(self, @selector(remoteTracks), remoteTracks, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +#pragma mark - FlutterStreamHandler methods + +- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments { + self.eventSink = nil; + return nil; +} + +- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)sink { + self.eventSink = sink; + return nil; +} + +@end + +@implementation FlutterWebRTCPlugin (RTCPeerConnection) + +- (void)peerConnectionSetConfiguration:(RTCConfiguration*)configuration + peerConnection:(RTCPeerConnection*)peerConnection { + [peerConnection setConfiguration:configuration]; +} + +- (void)peerConnectionCreateOffer:(NSDictionary*)constraints + peerConnection:(RTCPeerConnection*)peerConnection + result:(FlutterResult)result { + [peerConnection + offerForConstraints:[self parseMediaConstraints:constraints] + completionHandler:^(RTCSessionDescription* sdp, NSError* error) { + if (error) { + result([FlutterError + errorWithCode:@"CreateOfferFailed" + message:[NSString stringWithFormat:@"Error %@", error.userInfo[@"error"]] + details:nil]); + } else { + NSString* type = [RTCSessionDescription stringForType:sdp.type]; + result(@{@"sdp" : sdp.sdp, @"type" : type}); + } + }]; +} + +- (void)peerConnectionCreateAnswer:(NSDictionary*)constraints + peerConnection:(RTCPeerConnection*)peerConnection + result:(FlutterResult)result { + [peerConnection + answerForConstraints:[self parseMediaConstraints:constraints] + completionHandler:^(RTCSessionDescription* sdp, NSError* error) { + if (error) { + result([FlutterError + errorWithCode:@"CreateAnswerFailed" + message:[NSString stringWithFormat:@"Error %@", error.userInfo[@"error"]] + details:nil]); + } else { + NSString* type = [RTCSessionDescription stringForType:sdp.type]; + result(@{@"sdp" : sdp.sdp, @"type" : type}); + } + }]; +} + +- (void)peerConnectionSetLocalDescription:(RTCSessionDescription*)sdp + peerConnection:(RTCPeerConnection*)peerConnection + result:(FlutterResult)result { + [peerConnection + setLocalDescription:sdp + completionHandler:^(NSError* error) { + if (error) { + result([FlutterError + errorWithCode:@"SetLocalDescriptionFailed" + message:[NSString stringWithFormat:@"Error %@", error.localizedDescription] + details:nil]); + } else { + result(nil); + } + }]; +} + +- (void)peerConnectionSetRemoteDescription:(RTCSessionDescription*)sdp + peerConnection:(RTCPeerConnection*)peerConnection + result:(FlutterResult)result { + [peerConnection + setRemoteDescription:sdp + completionHandler:^(NSError* error) { + if (error) { + result([FlutterError + errorWithCode:@"SetRemoteDescriptionFailed" + message:[NSString stringWithFormat:@"Error %@", error.localizedDescription] + details:nil]); + } else { + result(nil); + } + }]; +} + +- (void)peerConnectionAddICECandidate:(RTCIceCandidate*)candidate + peerConnection:(RTCPeerConnection*)peerConnection + result:(FlutterResult)result { + [peerConnection + addIceCandidate:candidate + completionHandler:^(NSError* _Nullable error) { + if (error) { + result([FlutterError + errorWithCode:@"AddIceCandidateFailed" + message:[NSString stringWithFormat:@"Error %@", error.localizedDescription] + details:nil]); + } else { + result(nil); + } + }]; +} + +- (void)peerConnectionClose:(RTCPeerConnection*)peerConnection { + [peerConnection close]; + + for (NSString* trackId in peerConnection.remoteTracks) { + [self.pausedTrackVolumes removeObjectForKey:trackId]; + [self.trackVolumeCache removeObjectForKey:trackId]; + } + + // Clean up peerConnection's streams and tracks + [peerConnection.remoteStreams removeAllObjects]; + [peerConnection.remoteTracks removeAllObjects]; + + // Clean up peerConnection's dataChannels. + NSMutableDictionary* dataChannels = peerConnection.dataChannels; + for (NSString* dataChannelId in dataChannels) { + dataChannels[dataChannelId].delegate = nil; + // There is no need to close the RTCDataChannel because it is owned by the + // RTCPeerConnection and the latter will close the former. + } + [dataChannels removeAllObjects]; +} + +- (void)peerConnectionGetStatsForTrackId:(nonnull NSString*)trackID + peerConnection:(nonnull RTCPeerConnection*)peerConnection + result:(nonnull FlutterResult)result { + RTCRtpSender* sender = nil; + RTCRtpReceiver* receiver = nil; + + for (RTCRtpSender* s in peerConnection.senders) { + if (s.track != nil && [s.track.trackId isEqualToString:trackID]) { + sender = s; + } + } + + for (RTCRtpReceiver* r in peerConnection.receivers) { + if (r.track != nil && [r.track.trackId isEqualToString:trackID]) { + receiver = r; + } + } + + if (sender != nil) { + [peerConnection statisticsForSender:sender + completionHandler:^(RTCStatisticsReport* statsReport) { + NSMutableArray* stats = [NSMutableArray array]; + for (id key in statsReport.statistics) { + RTCStatistics* report = [statsReport.statistics objectForKey:key]; + [stats addObject:@{ + @"id" : report.id, + @"type" : report.type, + @"timestamp" : @(report.timestamp_us), + @"values" : report.values + }]; + } + result(@{@"stats" : stats}); + }]; + } else if (receiver != nil) { + [peerConnection statisticsForReceiver:receiver + completionHandler:^(RTCStatisticsReport* statsReport) { + NSMutableArray* stats = [NSMutableArray array]; + for (id key in statsReport.statistics) { + RTCStatistics* report = [statsReport.statistics objectForKey:key]; + [stats addObject:@{ + @"id" : report.id, + @"type" : report.type, + @"timestamp" : @(report.timestamp_us), + @"values" : report.values + }]; + } + result(@{@"stats" : stats}); + }]; + } else { + result([FlutterError errorWithCode:@"GetStatsFailed" + message:[NSString stringWithFormat:@"Error %@", @""] + details:nil]); + } +} + +- (void)peerConnectionGetStats:(nonnull RTCPeerConnection*)peerConnection + result:(nonnull FlutterResult)result { + [peerConnection statisticsWithCompletionHandler:^(RTCStatisticsReport* statsReport) { + NSMutableArray* stats = [NSMutableArray array]; + for (id key in statsReport.statistics) { + RTCStatistics* report = [statsReport.statistics objectForKey:key]; + [stats addObject:@{ + @"id" : report.id, + @"type" : report.type, + @"timestamp" : @(report.timestamp_us), + @"values" : report.values + }]; + } + result(@{@"stats" : stats}); + }]; +} + +- (NSString*)stringForICEConnectionState:(RTCIceConnectionState)state { + switch (state) { + case RTCIceConnectionStateNew: + return @"new"; + case RTCIceConnectionStateChecking: + return @"checking"; + case RTCIceConnectionStateConnected: + return @"connected"; + case RTCIceConnectionStateCompleted: + return @"completed"; + case RTCIceConnectionStateFailed: + return @"failed"; + case RTCIceConnectionStateDisconnected: + return @"disconnected"; + case RTCIceConnectionStateClosed: + return @"closed"; + case RTCIceConnectionStateCount: + return @"count"; + } + return nil; +} + +- (NSString*)stringForICEGatheringState:(RTCIceGatheringState)state { + switch (state) { + case RTCIceGatheringStateNew: + return @"new"; + case RTCIceGatheringStateGathering: + return @"gathering"; + case RTCIceGatheringStateComplete: + return @"complete"; + } + return nil; +} + +- (NSString*)stringForSignalingState:(RTCSignalingState)state { + switch (state) { + case RTCSignalingStateStable: + return @"stable"; + case RTCSignalingStateHaveLocalOffer: + return @"have-local-offer"; + case RTCSignalingStateHaveLocalPrAnswer: + return @"have-local-pranswer"; + case RTCSignalingStateHaveRemoteOffer: + return @"have-remote-offer"; + case RTCSignalingStateHaveRemotePrAnswer: + return @"have-remote-pranswer"; + case RTCSignalingStateClosed: + return @"closed"; + } + return nil; +} + +- (NSString*)stringForPeerConnectionState:(RTCPeerConnectionState)state { + switch (state) { + case RTCPeerConnectionStateNew: + return @"new"; + case RTCPeerConnectionStateConnecting: + return @"connecting"; + case RTCPeerConnectionStateConnected: + return @"connected"; + case RTCPeerConnectionStateDisconnected: + return @"disconnected"; + case RTCPeerConnectionStateFailed: + return @"failed"; + case RTCPeerConnectionStateClosed: + return @"closed"; + } + return nil; +} + +/** + * Parses the constraint keys and values of a specific JavaScript object into + * a specific NSMutableDictionary in a format suitable for the + * initialization of a RTCMediaConstraints instance. + * + * @param src The JavaScript object which defines constraint keys and values and + * which is to be parsed into the specified dst. + * @param dst The NSMutableDictionary into which the constraint keys + * and values defined by src are to be written in a format suitable for + * the initialization of a RTCMediaConstraints instance. + */ +- (void)parseJavaScriptConstraints:(NSDictionary*)src + intoWebRTCConstraints:(NSMutableDictionary*)dst { + for (id srcKey in src) { + id srcValue = src[srcKey]; + NSString* dstValue; + + if ([srcValue isKindOfClass:[NSNumber class]]) { + dstValue = [srcValue boolValue] ? @"true" : @"false"; + } else { + dstValue = [srcValue description]; + } + dst[[srcKey description]] = dstValue; + } +} + +/** + * Parses a JavaScript object into a new RTCMediaConstraints instance. + * + * @param constraints The JavaScript object to parse into a new + * RTCMediaConstraints instance. + * @returns A new RTCMediaConstraints instance initialized with the + * mandatory and optional constraint keys and values specified by + * constraints. + */ +- (RTCMediaConstraints*)parseMediaConstraints:(NSDictionary*)constraints { + id mandatory = constraints[@"mandatory"]; + NSMutableDictionary* mandatory_ = [NSMutableDictionary new]; + + if ([mandatory isKindOfClass:[NSDictionary class]]) { + [self parseJavaScriptConstraints:(NSDictionary*)mandatory intoWebRTCConstraints:mandatory_]; + } + + id optional = constraints[@"optional"]; + NSMutableDictionary* optional_ = [NSMutableDictionary new]; + + if ([optional isKindOfClass:[NSArray class]]) { + for (id o in (NSArray*)optional) { + if ([o isKindOfClass:[NSDictionary class]]) { + [self parseJavaScriptConstraints:(NSDictionary*)o intoWebRTCConstraints:optional_]; + } + } + } + + return [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory_ + optionalConstraints:optional_]; +} + +#pragma mark - RTCPeerConnectionDelegate methods +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didChangeSignalingState:(RTCSignalingState)newState { + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, + @{@"event" : @"signalingState", @"state" : [self stringForSignalingState:newState]}); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection + mediaStream:(RTCMediaStream*)stream + didAddTrack:(RTCVideoTrack*)track { + peerConnection.remoteTracks[track.trackId] = track; + NSString* streamId = stream.streamId; + peerConnection.remoteStreams[streamId] = stream; + + if ([track isKindOfClass:[RTCAudioTrack class]]) { + RTCAudioTrack* audioTrack = (RTCAudioTrack*)track; + NSString* trackId = track.trackId; + if (self.trackVolumeCache[trackId] == nil) { + self.trackVolumeCache[trackId] = @(1.0); + } + if (self.isAudioPlayoutPaused) { + self.pausedTrackVolumes[trackId] = self.trackVolumeCache[trackId]; + audioTrack.source.volume = 0.0; + } + } + + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"onAddTrack", + @"streamId" : streamId, + @"trackId" : track.trackId, + @"track" : @{ + @"id" : track.trackId, + @"kind" : track.kind, + @"label" : track.trackId, + @"enabled" : @(track.isEnabled), + @"remote" : @(YES), + @"readyState" : @"live" + } + }); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection + mediaStream:(RTCMediaStream*)stream + didRemoveTrack:(RTCVideoTrack*)track { + [peerConnection.remoteTracks removeObjectForKey:track.trackId]; + [self.pausedTrackVolumes removeObjectForKey:track.trackId]; + [self.trackVolumeCache removeObjectForKey:track.trackId]; + NSString* streamId = stream.streamId; + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"onRemoveTrack", + @"streamId" : streamId, + @"trackId" : track.trackId, + @"track" : @{ + @"id" : track.trackId, + @"kind" : track.kind, + @"label" : track.trackId, + @"enabled" : @(track.isEnabled), + @"remote" : @(YES), + @"readyState" : @"live" + } + }); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection didAddStream:(RTCMediaStream*)stream { + NSMutableArray* audioTracks = [NSMutableArray array]; + NSMutableArray* videoTracks = [NSMutableArray array]; + + BOOL hasAudio = NO; + for (RTCAudioTrack* track in stream.audioTracks) { + peerConnection.remoteTracks[track.trackId] = track; + NSString* trackId = track.trackId; + if (self.trackVolumeCache[trackId] == nil) { + self.trackVolumeCache[trackId] = @(1.0); + } + if (self.isAudioPlayoutPaused) { + self.pausedTrackVolumes[trackId] = self.trackVolumeCache[trackId]; + track.source.volume = 0.0; + } + [audioTracks addObject:@{ + @"id" : trackId, + @"kind" : track.kind, + @"label" : trackId, + @"enabled" : @(track.isEnabled), + @"remote" : @(YES), + @"readyState" : @"live" + }]; + hasAudio = YES; + } + + for (RTCVideoTrack* track in stream.videoTracks) { + peerConnection.remoteTracks[track.trackId] = track; + [videoTracks addObject:@{ + @"id" : track.trackId, + @"kind" : track.kind, + @"label" : track.trackId, + @"enabled" : @(track.isEnabled), + @"remote" : @(YES), + @"readyState" : @"live" + }]; + } + + NSString* streamId = stream.streamId; + peerConnection.remoteStreams[streamId] = stream; + + if (hasAudio) { + [self ensureAudioSession]; + } + + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"onAddStream", + @"streamId" : streamId, + @"audioTracks" : audioTracks, + @"videoTracks" : videoTracks, + }); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection didRemoveStream:(RTCMediaStream*)stream { + NSArray* keysArray = [peerConnection.remoteStreams allKeysForObject:stream]; + // We assume there can be only one object for 1 key + if (keysArray.count > 1) { + NSLog(@"didRemoveStream - more than one stream entry found for stream instance with id: %@", + stream.streamId); + } + NSString* streamId = stream.streamId; + + for (RTCVideoTrack* track in stream.videoTracks) { + [peerConnection.remoteTracks removeObjectForKey:track.trackId]; + } + for (RTCAudioTrack* track in stream.audioTracks) { + [peerConnection.remoteTracks removeObjectForKey:track.trackId]; + [self.pausedTrackVolumes removeObjectForKey:track.trackId]; + [self.trackVolumeCache removeObjectForKey:track.trackId]; + } + + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"onRemoveStream", + @"streamId" : streamId, + }); + } +} + +- (void)peerConnectionShouldNegotiate:(RTCPeerConnection*)peerConnection { + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"onRenegotiationNeeded", + }); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didChangeIceConnectionState:(RTCIceConnectionState)newState { + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"iceConnectionState", + @"state" : [self stringForICEConnectionState:newState] + }); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didChangeIceGatheringState:(RTCIceGatheringState)newState { + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent( + eventSink, + @{@"event" : @"iceGatheringState", @"state" : [self stringForICEGatheringState:newState]}); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didGenerateIceCandidate:(RTCIceCandidate*)candidate { + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"onCandidate", + @"candidate" : @{ + @"candidate" : candidate.sdp, + @"sdpMLineIndex" : @(candidate.sdpMLineIndex), + @"sdpMid" : candidate.sdpMid + } + }); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didOpenDataChannel:(RTCDataChannel*)dataChannel { + if (-1 == dataChannel.channelId) { + return; + } + + NSString* flutterChannelId = [[NSUUID UUID] UUIDString]; + NSNumber* dataChannelId = [NSNumber numberWithInteger:dataChannel.channelId]; + dataChannel.peerConnectionId = peerConnection.flutterId; + dataChannel.delegate = self; + peerConnection.dataChannels[flutterChannelId] = dataChannel; + + FlutterEventChannel* eventChannel = [FlutterEventChannel + eventChannelWithName:[NSString stringWithFormat:@"FlutterWebRTC/dataChannelEvent%1$@%2$@", + peerConnection.flutterId, flutterChannelId] + binaryMessenger:self.messenger]; + + dataChannel.eventChannel = eventChannel; + dataChannel.flutterChannelId = flutterChannelId; + dataChannel.eventQueue = nil; + + dispatch_async(dispatch_get_main_queue(), ^{ + // setStreamHandler on main thread + [eventChannel setStreamHandler:dataChannel]; + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"didOpenDataChannel", + @"id" : dataChannelId, + @"label" : dataChannel.label, + @"flutterId" : flutterChannelId + }); + } + }); +} + +/** Called any time the PeerConnectionState changes. */ +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didChangeConnectionState:(RTCPeerConnectionState)newState { + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"peerConnectionState", + @"state" : [self stringForPeerConnectionState:newState] + }); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didStartReceivingOnTransceiver:(RTCRtpTransceiver*)transceiver { +} + +/** Called when a receiver and its track are created. */ +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didAddReceiver:(RTCRtpReceiver*)rtpReceiver + streams:(NSArray*)mediaStreams { + // For unified-plan + NSMutableArray* streams = [NSMutableArray array]; + for (RTCMediaStream* stream in mediaStreams) { + [streams addObject:[self mediaStreamToMap:stream ownerTag:peerConnection.flutterId]]; + } + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + NSMutableDictionary* event = [NSMutableDictionary dictionary]; + [event addEntriesFromDictionary:@{ + @"event" : @"onTrack", + @"track" : [self mediaTrackToMap:rtpReceiver.track], + @"receiver" : [self receiverToMap:rtpReceiver], + @"streams" : streams, + }]; + + if (peerConnection.configuration.sdpSemantics == RTCSdpSemanticsUnifiedPlan) { + for (RTCRtpTransceiver* transceiver in peerConnection.transceivers) { + if (transceiver.receiver != nil && + [transceiver.receiver.receiverId isEqualToString:rtpReceiver.receiverId]) { + [event setValue:[self transceiverToMap:transceiver] forKey:@"transceiver"]; + } + } + } + + peerConnection.remoteTracks[rtpReceiver.track.trackId] = rtpReceiver.track; + if (mediaStreams.count > 0) { + peerConnection.remoteStreams[mediaStreams[0].streamId] = mediaStreams[0]; + } + + if ([rtpReceiver.track.kind isEqualToString:@"audio"]) { + [self ensureAudioSession]; + NSString* trackId = rtpReceiver.track.trackId; + if (self.trackVolumeCache[trackId] == nil) { + self.trackVolumeCache[trackId] = @(1.0); + } + if (self.isAudioPlayoutPaused && + [rtpReceiver.track isKindOfClass:[RTCAudioTrack class]]) { + RTCAudioTrack* audioTrack = (RTCAudioTrack*)rtpReceiver.track; + self.pausedTrackVolumes[trackId] = self.trackVolumeCache[trackId]; + audioTrack.source.volume = 0.0; + } + } + postEvent(eventSink, event); + } +} + +/** Called when the receiver and its track are removed. */ +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didRemoveReceiver:(RTCRtpReceiver*)rtpReceiver { + if (rtpReceiver.track != nil) { + [self.pausedTrackVolumes removeObjectForKey:rtpReceiver.track.trackId]; + [self.trackVolumeCache removeObjectForKey:rtpReceiver.track.trackId]; + } +} + +/** Called when the selected ICE candidate pair is changed. */ +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didChangeLocalCandidate:(RTCIceCandidate*)local + remoteCandidate:(RTCIceCandidate*)remote + lastReceivedMs:(int)lastDataReceivedMs + changeReason:(NSString*)reason { + FlutterEventSink eventSink = peerConnection.eventSink; + if (eventSink) { + postEvent(eventSink, @{ + @"event" : @"onSelectedCandidatePairChanged", + @"local" : @{ + @"candidate" : local.sdp, + @"sdpMLineIndex" : @(local.sdpMLineIndex), + @"sdpMid" : local.sdpMid + }, + @"remote" : @{ + @"candidate" : remote.sdp, + @"sdpMLineIndex" : @(remote.sdpMLineIndex), + @"sdpMid" : remote.sdpMid + }, + @"reason" : reason, + @"lastDataReceivedMs" : @(lastDataReceivedMs) + }); + } +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didRemoveIceCandidates:(NSArray*)candidates { +} + +NSString* mediaTypeFromString(NSString* kind) { + NSString* mediaType = kRTCMediaStreamTrackKindAudio; + if ([kind isEqualToString:@"audio"]) { + mediaType = kRTCMediaStreamTrackKindAudio; + } else if ([kind isEqualToString:@"video"]) { + mediaType = kRTCMediaStreamTrackKindVideo; + } + return mediaType; +} + +NSString* parametersToString(NSDictionary* parameters) { + NSMutableArray* kvs = [NSMutableArray array]; + for (NSString* key in parameters) { + if (key.length > 0) { + [kvs addObject:[NSString stringWithFormat:@"%@=%@", key, parameters[key]]]; + } else { + [kvs addObject:parameters[key]]; + } + } + return [kvs componentsJoinedByString:@";"]; +} + +NSDictionary* stringToParameters(NSString* str) { + NSMutableDictionary* parameters = [NSMutableDictionary dictionary]; + NSArray* kvs = [str componentsSeparatedByString:@";"]; + for (NSString* kv in kvs) { + NSArray* kvArr = [kv componentsSeparatedByString:@"="]; + if (kvArr.count == 2) { + parameters[kvArr[0]] = kvArr[1]; + } else if (kvArr.count == 1) { + parameters[@""] = kvArr[0]; + } + } + return parameters; +} + +- (void)peerConnectionGetRtpReceiverCapabilities:(nonnull NSDictionary*)argsMap + result:(nonnull FlutterResult)result { + NSString* factoryId = argsMap[@"factoryId"]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (factoryId == nil || factoryId.length == 0 || nf == nil || nf.factory == nil) { + result([FlutterError errorWithCode:@"MISSING_FACTORY_ID" + message:@"getRtpReceiverCapabilities requires a valid factoryId" + details:@{@"factoryId" : factoryId ?: [NSNull null]}]); + return; + } + NSString* kind = argsMap[@"kind"]; + RTCRtpCapabilities* caps = [nf.factory rtpReceiverCapabilitiesForKind:mediaTypeFromString(kind)]; + NSMutableArray* codecsMap = [NSMutableArray array]; + for (RTCRtpCodecCapability* c in caps.codecs) { + if ([kind isEqualToString:@"audio"]) { + [codecsMap addObject:@{ + @"channels" : c.numChannels, + @"clockRate" : c.clockRate, + @"mimeType" : c.mimeType, + @"sdpFmtpLine" : parametersToString(c.parameters), + }]; + } else if ([kind isEqualToString:@"video"]) { + [codecsMap addObject:@{ + @"clockRate" : c.clockRate, + @"mimeType" : c.mimeType, + @"sdpFmtpLine" : parametersToString(c.parameters), + }]; + } + } + result(@{ + @"codecs" : codecsMap, + @"headerExtensions" : @[], + @"fecMechanisms" : @[], + }); +} + +- (void)peerConnectionGetRtpSenderCapabilities:(nonnull NSDictionary*)argsMap + result:(nonnull FlutterResult)result { + NSString* factoryId = argsMap[@"factoryId"]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (factoryId == nil || factoryId.length == 0 || nf == nil || nf.factory == nil) { + result([FlutterError errorWithCode:@"MISSING_FACTORY_ID" + message:@"getRtpSenderCapabilities requires a valid factoryId" + details:@{@"factoryId" : factoryId ?: [NSNull null]}]); + return; + } + NSString* kind = argsMap[@"kind"]; + RTCRtpCapabilities* caps = [nf.factory rtpSenderCapabilitiesForKind:mediaTypeFromString(kind)]; + NSMutableArray* codecsMap = [NSMutableArray array]; + for (RTCRtpCodecCapability* c in caps.codecs) { + if ([kind isEqualToString:@"audio"]) { + [codecsMap addObject:@{ + @"channels" : c.numChannels, + @"clockRate" : c.clockRate, + @"mimeType" : c.mimeType, + @"sdpFmtpLine" : parametersToString(c.parameters), + }]; + } else if ([kind isEqualToString:@"video"]) { + [codecsMap addObject:@{ + @"clockRate" : c.clockRate, + @"mimeType" : c.mimeType, + @"sdpFmtpLine" : parametersToString(c.parameters), + }]; + } + } + result(@{ + @"codecs" : codecsMap, + @"headerExtensions" : @[], + @"fecMechanisms" : @[], + }); +} + +- (RTC_OBJC_TYPE(RTCRtpCodecCapability) *)findCodecCapability:(NSString*)kind + codec:(NSString*)codec + parameters:(NSDictionary*) + parameters + factory:(RTCPeerConnectionFactory*)factory { + if (factory == nil) return nil; + RTCRtpCapabilities* caps = [factory + rtpSenderCapabilitiesForKind:[kind isEqualToString:@"video"] ? kRTCMediaStreamTrackKindVideo + : kRTCMediaStreamTrackKindAudio]; + for (RTCRtpCodecCapability* capCodec in caps.codecs) { + if ([capCodec.name isEqualToString:codec] && [capCodec.kind isEqualToString:kind]) { + BOOL matched = YES; + for (NSString* key in capCodec.parameters) { + NSString* value = [capCodec.parameters objectForKey:key]; + NSString* value2 = [parameters objectForKey:key]; + if (![value isEqualToString:value2]) { + matched = NO; + } + } + if (matched) { + return capCodec; + } + } + } + return nil; +} + +- (void)transceiverSetCodecPreferences:(nonnull NSDictionary*)argsMap + result:(nonnull FlutterResult)result { + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:@"transceiverSetCodecPreferencesFailed" + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + // Resolve the per-call factory that built this peer connection so the + // capability lookup runs against the correct codec set. + NSString* factoryIdForPc = self.pcFactoryId[peerConnectionId]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryIdForPc]; + RTCPeerConnectionFactory* factory = nf.factory; + NSString* transceiverId = argsMap[@"transceiverId"]; + RTCRtpTransceiver* transcevier = [self getRtpTransceiverById:peerConnection Id:transceiverId]; + if (transcevier == nil) { + result([FlutterError errorWithCode:@"transceiverSetCodecPreferencesFailed" + message:[NSString stringWithFormat:@"Error: transcevier not found!"] + details:nil]); + return; + } + id codecs = argsMap[@"codecs"]; + NSMutableArray* codecCaps = [NSMutableArray array]; + for (id c in codecs) { + NSArray* kindAndName = [c[@"mimeType"] componentsSeparatedByString:@"/"]; + NSString* kind = [kindAndName[0] lowercaseString]; + NSString* name = kindAndName[1]; + NSLog(@"codec %@/%@", kind, name); + NSDictionary* parameters = nil; + if (c[@"sdpFmtpLine"] != nil && ![((NSString*)c[@"sdpFmtpLine"]) isEqualToString:@""]) { + parameters = stringToParameters((NSString*)c[@"sdpFmtpLine"]); + } + RTCRtpCodecCapability* codec = [self findCodecCapability:kind + codec:name + parameters:parameters + factory:factory]; + if (codec != nil) { + [codecCaps addObject:codec]; + } + } + NSError* error = nil; + [transcevier setCodecPreferences:codecCaps error:&error]; + if (error) { + result([FlutterError errorWithCode:@"SetCodecPreferencesError" + message:error.localizedDescription + details:nil]); + } else { + result(nil); + } +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCVideoRenderer.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCVideoRenderer.m new file mode 100644 index 0000000000..41cbdbcc4b --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterRTCVideoRenderer.m @@ -0,0 +1,296 @@ +#import "include/stream_webrtc_flutter/FlutterRTCVideoRenderer.h" + +#import +#import +#import +#import +#import + +#import + +#import +#import "include/stream_webrtc_flutter/FlutterWebRTCPlugin.h" + +@implementation FlutterRTCVideoRenderer { + CGSize _frameSize; + CGSize _renderSize; + CVPixelBufferRef _pixelBufferRef; + RTCVideoRotation _rotation; + FlutterEventChannel* _eventChannel; + bool _isFirstFrameRendered; + bool _frameAvailable; + os_unfair_lock _lock; +} + +@synthesize textureId = _textureId; +@synthesize registry = _registry; +@synthesize eventSink = _eventSink; +@synthesize videoTrack = _videoTrack; + +- (instancetype)initWithTextureRegistry:(id)registry + messenger:(NSObject*)messenger { + self = [super init]; + if (self) { + _lock = OS_UNFAIR_LOCK_INIT; + _isFirstFrameRendered = false; + _frameAvailable = false; + _frameSize = CGSizeZero; + _renderSize = CGSizeZero; + _rotation = -1; + _registry = registry; + _pixelBufferRef = nil; + _eventSink = nil; + _rotation = -1; + _textureId = [registry registerTexture:self]; + /*Create Event Channel.*/ + _eventChannel = [FlutterEventChannel + eventChannelWithName:[NSString stringWithFormat:@"FlutterWebRTC/Texture%lld", _textureId] + binaryMessenger:messenger]; + [_eventChannel setStreamHandler:self]; + } + return self; +} + +- (CVPixelBufferRef)copyPixelBuffer { + CVPixelBufferRef buffer = nil; + os_unfair_lock_lock(&_lock); + if (_pixelBufferRef != nil && _frameAvailable) { + buffer = CVBufferRetain(_pixelBufferRef); + _frameAvailable = false; + } + os_unfair_lock_unlock(&_lock); + return buffer; +} + +- (void)dispose { + os_unfair_lock_lock(&_lock); + [_registry unregisterTexture:_textureId]; + _textureId = -1; + if (_pixelBufferRef) { + CVBufferRelease(_pixelBufferRef); + _pixelBufferRef = nil; + } + _frameAvailable = false; + os_unfair_lock_unlock(&_lock); +} + +- (void)setVideoTrack:(RTCVideoTrack*)videoTrack { + RTCVideoTrack* oldValue = self.videoTrack; + if (oldValue != videoTrack) { + os_unfair_lock_lock(&_lock); + _videoTrack = videoTrack; + os_unfair_lock_unlock(&_lock); + _isFirstFrameRendered = false; + if (oldValue) { + [oldValue removeRenderer:self]; + } + _frameSize = CGSizeZero; + _renderSize = CGSizeZero; + _rotation = -1; + if (videoTrack) { + [videoTrack addRenderer:self]; + } + } +} + +- (id)correctRotation:(const id)src + withRotation:(RTCVideoRotation)rotation { + int rotated_width = src.width; + int rotated_height = src.height; + + if (rotation == RTCVideoRotation_90 || rotation == RTCVideoRotation_270) { + int temp = rotated_width; + rotated_width = rotated_height; + rotated_height = temp; + } + + id buffer = [[RTCI420Buffer alloc] initWithWidth:rotated_width + height:rotated_height]; + + [RTCYUVHelper I420Rotate:src.dataY + srcStrideY:src.strideY + srcU:src.dataU + srcStrideU:src.strideU + srcV:src.dataV + srcStrideV:src.strideV + dstY:(uint8_t*)buffer.dataY + dstStrideY:buffer.strideY + dstU:(uint8_t*)buffer.dataU + dstStrideU:buffer.strideU + dstV:(uint8_t*)buffer.dataV + dstStrideV:buffer.strideV + width:src.width + height:src.height + mode:rotation]; + + return buffer; +} + +- (void)copyI420ToCVPixelBuffer:(CVPixelBufferRef)outputPixelBuffer + withFrame:(RTCVideoFrame*)frame { + id i420Buffer = [self correctRotation:[frame.buffer toI420] + withRotation:frame.rotation]; + CVPixelBufferLockBaseAddress(outputPixelBuffer, 0); + + const OSType pixelFormat = CVPixelBufferGetPixelFormatType(outputPixelBuffer); + if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { + // NV12 + uint8_t* dstY = CVPixelBufferGetBaseAddressOfPlane(outputPixelBuffer, 0); + const size_t dstYStride = CVPixelBufferGetBytesPerRowOfPlane(outputPixelBuffer, 0); + uint8_t* dstUV = CVPixelBufferGetBaseAddressOfPlane(outputPixelBuffer, 1); + const size_t dstUVStride = CVPixelBufferGetBytesPerRowOfPlane(outputPixelBuffer, 1); + + [RTCYUVHelper I420ToNV12:i420Buffer.dataY + srcStrideY:i420Buffer.strideY + srcU:i420Buffer.dataU + srcStrideU:i420Buffer.strideU + srcV:i420Buffer.dataV + srcStrideV:i420Buffer.strideV + dstY:dstY + dstStrideY:(int)dstYStride + dstUV:dstUV + dstStrideUV:(int)dstUVStride + width:i420Buffer.width + height:i420Buffer.height]; + + } else { + uint8_t* dst = CVPixelBufferGetBaseAddress(outputPixelBuffer); + const size_t bytesPerRow = CVPixelBufferGetBytesPerRow(outputPixelBuffer); + + if (pixelFormat == kCVPixelFormatType_32BGRA) { + // Corresponds to libyuv::FOURCC_ARGB + + [RTCYUVHelper I420ToARGB:i420Buffer.dataY + srcStrideY:i420Buffer.strideY + srcU:i420Buffer.dataU + srcStrideU:i420Buffer.strideU + srcV:i420Buffer.dataV + srcStrideV:i420Buffer.strideV + dstARGB:dst + dstStrideARGB:(int)bytesPerRow + width:i420Buffer.width + height:i420Buffer.height]; + + } else if (pixelFormat == kCVPixelFormatType_32ARGB) { + // Corresponds to libyuv::FOURCC_BGRA + [RTCYUVHelper I420ToBGRA:i420Buffer.dataY + srcStrideY:i420Buffer.strideY + srcU:i420Buffer.dataU + srcStrideU:i420Buffer.strideU + srcV:i420Buffer.dataV + srcStrideV:i420Buffer.strideV + dstBGRA:dst + dstStrideBGRA:(int)bytesPerRow + width:i420Buffer.width + height:i420Buffer.height]; + } + } + + CVPixelBufferUnlockBaseAddress(outputPixelBuffer, 0); +} + +#pragma mark - RTCVideoRenderer methods +- (void)renderFrame:(RTCVideoFrame*)frame { + os_unfair_lock_lock(&_lock); + if (_videoTrack == nil) { + os_unfair_lock_unlock(&_lock); + return; + } + if (!_frameAvailable && _pixelBufferRef) { + [self copyI420ToCVPixelBuffer:_pixelBufferRef withFrame:frame]; + if (_textureId != -1) { + [_registry textureFrameAvailable:_textureId]; + } + _frameAvailable = true; + } + os_unfair_lock_unlock(&_lock); + + __weak FlutterRTCVideoRenderer* weakSelf = self; + if (_renderSize.width != frame.width || _renderSize.height != frame.height) { + dispatch_async(dispatch_get_main_queue(), ^{ + FlutterRTCVideoRenderer* strongSelf = weakSelf; + if (strongSelf.eventSink) { + strongSelf.eventSink(@{ + @"event" : @"didTextureChangeVideoSize", + @"id" : @(strongSelf.textureId), + @"width" : @(frame.width), + @"height" : @(frame.height), + }); + } + }); + _renderSize = CGSizeMake(frame.width, frame.height); + } + + if (frame.rotation != _rotation) { + dispatch_async(dispatch_get_main_queue(), ^{ + FlutterRTCVideoRenderer* strongSelf = weakSelf; + if (strongSelf.eventSink) { + strongSelf.eventSink(@{ + @"event" : @"didTextureChangeRotation", + @"id" : @(strongSelf.textureId), + @"rotation" : @(frame.rotation), + }); + } + }); + + _rotation = frame.rotation; + } + + // Notify the Flutter new pixelBufferRef to be ready. + dispatch_async(dispatch_get_main_queue(), ^{ + FlutterRTCVideoRenderer* strongSelf = weakSelf; + if (!strongSelf->_isFirstFrameRendered) { + if (strongSelf.eventSink) { + strongSelf.eventSink(@{@"event" : @"didFirstFrameRendered"}); + strongSelf->_isFirstFrameRendered = true; + } + } + }); +} + +/** + * Sets the size of the video frame to render. + * + * @param size The size of the video frame to render. + */ +- (void)setSize:(CGSize)size { + os_unfair_lock_lock(&_lock); + if (size.width != _frameSize.width || size.height != _frameSize.height) { + if (_pixelBufferRef) { + CVBufferRelease(_pixelBufferRef); + } + NSDictionary* pixelAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}}; + CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, kCVPixelFormatType_32BGRA, + (__bridge CFDictionaryRef)(pixelAttributes), &_pixelBufferRef); + _frameAvailable = false; + _frameSize = size; + } + os_unfair_lock_unlock(&_lock); +} + +#pragma mark - FlutterStreamHandler methods + +- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments { + _eventSink = nil; + return nil; +} + +- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)sink { + _eventSink = sink; + return nil; +} +@end + +@implementation FlutterWebRTCPlugin (FlutterVideoRendererManager) + +- (FlutterRTCVideoRenderer*)createWithTextureRegistry:(id)registry + messenger:(NSObject*)messenger { + return [[FlutterRTCVideoRenderer alloc] initWithTextureRegistry:registry messenger:messenger]; +} + +- (void)rendererSetSrcObject:(FlutterRTCVideoRenderer*)renderer stream:(RTCVideoTrack*)videoTrack { + renderer.videoTrack = videoTrack; +} +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m new file mode 100644 index 0000000000..c5c06ede47 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/FlutterWebRTCPlugin.m @@ -0,0 +1,2900 @@ +#import "include/stream_webrtc_flutter/FlutterWebRTCPlugin.h" +#import "include/stream_webrtc_flutter/CameraUtils.h" + +#import "include/stream_webrtc_flutter/AudioManager.h" +#import "include/stream_webrtc_flutter/FlutterDataPacketCryptor.h" +#import "include/stream_webrtc_flutter/FlutterRTCDataChannel.h" +#import "include/stream_webrtc_flutter/FlutterRTCDesktopCapturer.h" +#import "include/stream_webrtc_flutter/FlutterRTCMediaStream.h" +#import "include/stream_webrtc_flutter/FlutterRTCPeerConnection.h" +#import "include/stream_webrtc_flutter/FlutterRTCVideoRenderer.h" +#import "include/stream_webrtc_flutter/ProcessorProvider.h" +#import "include/stream_webrtc_flutter/VideoEffectProcessor.h" +#import "include/stream_webrtc_flutter/VideoFrameProcessor.h" + +#import +#import + +#import +#import +#import +#import + +#import "include/stream_webrtc_flutter/LocalAudioTrack.h" +#import "include/stream_webrtc_flutter/LocalTrack.h" +#import "include/stream_webrtc_flutter/LocalVideoTrack.h" +#import "include/stream_webrtc_flutter/NativePeerConnectionFactory.h" +#import "include/stream_webrtc_flutter/VideoFactoriesPrivate.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wprotocol" + +NSArray* motifyH264ProfileLevelId( + NSArray* codecs) { + NSMutableArray* newCodecs = [[NSMutableArray alloc] init]; + NSInteger count = codecs.count; + for (NSInteger i = 0; i < count; i++) { + RTC_OBJC_TYPE(RTCVideoCodecInfo)* info = [codecs objectAtIndex:i]; + if ([info.name isEqualToString:kRTCVideoCodecH264Name]) { + NSString* hexString = info.parameters[@"profile-level-id"]; + RTCH264ProfileLevelId* profileLevelId = + [[RTCH264ProfileLevelId alloc] initWithHexString:hexString]; + if (profileLevelId.level < RTCH264Level5_1) { + RTCH264ProfileLevelId* newProfileLevelId = + [[RTCH264ProfileLevelId alloc] initWithProfile:profileLevelId.profile + level:RTCH264Level5_1]; + // NSLog(@"profile-level-id: %@ => %@", hexString, [newProfileLevelId hexString]); + NSMutableDictionary* parametersCopy = [[NSMutableDictionary alloc] init]; + [parametersCopy addEntriesFromDictionary:info.parameters]; + [parametersCopy setObject:[newProfileLevelId hexString] forKey:@"profile-level-id"]; + [newCodecs insertObject:[[RTCVideoCodecInfo alloc] initWithName:kRTCVideoCodecH264Name + parameters:parametersCopy] + atIndex:i]; + } else { + [newCodecs insertObject:info atIndex:i]; + } + } else { + [newCodecs insertObject:info atIndex:i]; + } + } + return newCodecs; +} + +@implementation VideoEncoderFactory +- (NSArray*)supportedCodecs { + NSArray* codecs = [super supportedCodecs]; + return motifyH264ProfileLevelId(codecs); +} +@end + +@implementation VideoDecoderFactory +- (NSArray*)supportedCodecs { + NSArray* codecs = [super supportedCodecs]; + return motifyH264ProfileLevelId(codecs); +} +@end + +@implementation VideoEncoderFactorySimulcast +- (NSArray*)supportedCodecs { + NSArray* codecs = [super supportedCodecs]; + return motifyH264ProfileLevelId(codecs); +} +@end + +void postEvent(FlutterEventSink _Nullable sink, id _Nullable event) { + if (sink == nil) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + sink(event); + }); +} + +/** + * Immutable snapshot of the arguments passed to the most-recent + * [WebRTC.initialize] call. Used as build defaults for the implicit + * factory, since the implicit factory is built lazily on first use + * rather than eagerly during initialize. + */ +@interface RTCInitializeSnapshot : NSObject +@property(nonatomic, assign) BOOL bypassVoiceProcessing; +@property(nonatomic, copy) NSArray* networkIgnoreMask; +@property(nonatomic, copy, nullable) NSDictionary* appleAudioConfiguration; +@end + +@implementation RTCInitializeSnapshot +@end + +@implementation FlutterWebRTCPlugin { +#pragma clang diagnostic pop + FlutterMethodChannel* _methodChannel; + FlutterEventSink _eventSink; + FlutterEventChannel* _eventChannel; + id _registry; + id _messenger; + id _textures; + BOOL _speakerOn; + BOOL _speakerOnButPreferBluetooth; + AudioManager* _audioManager; + BOOL _stereoPlayoutPreferred; + + RTC_OBJC_TYPE(RTCCallbackLogger) * loggerCallback; + + /** Snapshot of the most-recent [WebRTC.initialize] options. */ + RTCInitializeSnapshot* _initializeSnapshot; +} + +static FlutterWebRTCPlugin* sharedSingleton; + ++ (FlutterWebRTCPlugin*)sharedSingleton { + @synchronized(self) { + return sharedSingleton; + } +} + +@synthesize messenger = _messenger; +@synthesize eventSink = _eventSink; +@synthesize audioManager = _audioManager; + ++ (void)registerWithRegistrar:(NSObject*)registrar { + FlutterMethodChannel* channel = + [FlutterMethodChannel methodChannelWithName:@"FlutterWebRTC.Method" + binaryMessenger:[registrar messenger]]; + FlutterWebRTCPlugin* instance = + [[FlutterWebRTCPlugin alloc] initWithChannel:channel + registrar:registrar + messenger:[registrar messenger] + withTextures:[registrar textures]]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)initWithChannel:(FlutterMethodChannel*)channel + registrar:(NSObject*)registrar + messenger:(NSObject*)messenger + withTextures:(NSObject*)textures { + + self = [super init]; + sharedSingleton = self; + + FlutterEventChannel* eventChannel = + [FlutterEventChannel eventChannelWithName:@"FlutterWebRTC.Event" binaryMessenger:messenger]; + [eventChannel setStreamHandler:self]; + + if (self) { + _methodChannel = channel; + _registry = registrar; + _textures = textures; + _messenger = messenger; + _speakerOn = NO; + _speakerOnButPreferBluetooth = NO; + _eventChannel = eventChannel; + _audioManager = AudioManager.sharedInstance; + + } + + NSDictionary* fieldTrials = @{kRTCFieldTrialUseNWPathMonitor : kRTCFieldTrialEnabledValue}; + RTCInitFieldTrialDictionary(fieldTrials); + + self.peerConnections = [NSMutableDictionary new]; + self.localStreams = [NSMutableDictionary new]; + self.localTracks = [NSMutableDictionary new]; + self.renders = [NSMutableDictionary new]; + self.videoCapturerStopHandlers = [NSMutableDictionary new]; + self.videoCaptureState = [NSMutableDictionary new]; + self.trackVolumeCache = [NSMutableDictionary new]; + self.pausedTrackVolumes = [NSMutableDictionary new]; + _factories = [NSMutableDictionary new]; + _pcFactoryId = [NSMutableDictionary new]; + _trackFactoryId = [NSMutableDictionary new]; + self.isAudioPlayoutPaused = NO; + + return self; +} + +- (void)detachFromEngineForRegistrar:(NSObject*)registrar { + for (RTCPeerConnection* peerConnection in _peerConnections.allValues) { + for (RTCDataChannel* dataChannel in peerConnection.dataChannels) { + dataChannel.eventSink = nil; + } + peerConnection.eventSink = nil; + } + _eventSink = nil; +} + +#pragma mark - FlutterStreamHandler methods + +#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" +- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments { + _eventSink = nil; + return nil; +} + +#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" +- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)sink { + _eventSink = sink; + return nil; +} + +- (void)didSessionRouteChange:(NSNotification*)notification { +} + +- (void)handleInterruption:(NSNotification*)notification { +} + +- (void)initLoggerCallback:(RTCLoggingSeverity)severity { + if (loggerCallback == nil) { + loggerCallback = [RTC_OBJC_TYPE(RTCCallbackLogger) new]; + [loggerCallback start:^(NSString* logMessage) { + postEvent(self.eventSink, @{@"event" : @"onLogData", @"data" : logMessage}); + }]; + } + + loggerCallback.severity = severity; +} + +- (RTCLoggingSeverity)str2LogSeverity:(NSString*)str { + if ([@"verbose" isEqualToString:str]) { + return RTCLoggingSeverityVerbose; + } else if ([@"info" isEqualToString:str]) { + return RTCLoggingSeverityInfo; + } else if ([@"warning" isEqualToString:str]) { + return RTCLoggingSeverityWarning; + } else if ([@"error" isEqualToString:str]) { + return RTCLoggingSeverityError; + } else if ([@"none" isEqualToString:str]) { + return RTCLoggingSeverityNone; + } + + return RTCLoggingSeverityNone; +} + +- (void)initialize:(NSArray*)networkIgnoreMask + bypassVoiceProcessing:(BOOL)bypassVoiceProcessing + appleAudioConfig:(NSDictionary*)appleAudioConfig + severity:(RTCLoggingSeverity)severity { + // RTCSetMinDebugLogLevel(severity); + [self initLoggerCallback:severity]; + + RTCInitializeSnapshot* snapshot = [RTCInitializeSnapshot new]; + snapshot.bypassVoiceProcessing = bypassVoiceProcessing; + snapshot.networkIgnoreMask = networkIgnoreMask ?: @[]; + snapshot.appleAudioConfiguration = appleAudioConfig; + @synchronized(self) { + _initializeSnapshot = snapshot; + } +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + if ([@"initialize" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSDictionary* options = argsMap[@"options"]; + BOOL enableBypassVoiceProcessing = NO; + if (options[@"bypassVoiceProcessing"] != nil) { + enableBypassVoiceProcessing = ((NSNumber*)options[@"bypassVoiceProcessing"]).boolValue; + } + NSArray* networkIgnoreMask = [NSArray new]; + if (options[@"networkIgnoreMask"] != nil) { + networkIgnoreMask = ((NSArray*)options[@"networkIgnoreMask"]); + } + RTCLoggingSeverity severity = RTCLoggingSeverityNone; + if (options[@"logSeverity"] != nil) { + NSString* severityStr = ((NSString*)options[@"logSeverity"]); + severity = [self str2LogSeverity:severityStr]; + } + NSDictionary* appleAudioConfig = nil; + if ([options[@"appleAudioConfiguration"] isKindOfClass:[NSDictionary class]]) { + appleAudioConfig = options[@"appleAudioConfiguration"]; + } + + [self initialize:networkIgnoreMask + bypassVoiceProcessing:enableBypassVoiceProcessing + appleAudioConfig:appleAudioConfig + severity:severity]; + result(@""); + } else if ([@"createPeerConnectionFactory" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSDictionary* createOptions = argsMap[@"options"] ?: @{}; + BOOL bypass = NO; + if (createOptions[@"bypassVoiceProcessing"] != nil) { + bypass = ((NSNumber*)createOptions[@"bypassVoiceProcessing"]).boolValue; + } + NSArray* mask = createOptions[@"networkIgnoreMask"] ?: @[]; + NSDictionary* audioConfig = nil; + if ([createOptions[@"appleAudioConfiguration"] isKindOfClass:[NSDictionary class]]) { + audioConfig = createOptions[@"appleAudioConfiguration"]; + } + NSString* factoryId = [[NSUUID UUID] UUIDString]; + NativePeerConnectionFactory* nf = + [[NativePeerConnectionFactory alloc] initWithFactoryId:factoryId + bypassVoiceProcessing:bypass + networkIgnoreMask:mask + audioProcessingModule:_audioManager.audioProcessingModule + appleAudioConfiguration:audioConfig + admObserver:self]; + @synchronized(self) { + _factories[factoryId] = nf; + } + NSLog(@"[createPeerConnectionFactory] built id: %@", factoryId); + result(@{@"factoryId" : factoryId}); + } else if ([@"disposePeerConnectionFactory" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* factoryId = argsMap[@"factoryId"]; + if (factoryId == nil || factoryId.length == 0) { + result([FlutterError errorWithCode:@"disposePeerConnectionFactory" + message:@"factoryId argument is required" + details:nil]); + return; + } + NativePeerConnectionFactory* nf; + @synchronized(self) { + nf = _factories[factoryId]; + [_factories removeObjectForKey:factoryId]; + } + if (nf == nil) { + NSLog(@"[disposePeerConnectionFactory] unknown factoryId: %@", factoryId); + result(nil); + return; + } + NSLog(@"[disposePeerConnectionFactory] disposing id: %@, ownedPcs: %lu, " + "ownedTracks: %lu, ownedStreams: %lu", + factoryId, (unsigned long)nf.ownedPcIds.count, (unsigned long)nf.ownedTrackIds.count, + (unsigned long)nf.ownedStreamIds.count); + + // 1. Defensively drain any PCs the SDK forgot. + NSArray* pcIdsSnapshot = [nf.ownedPcIds.allObjects copy]; + for (NSString* pcId in pcIdsSnapshot) { + RTCPeerConnection* peerConnection = self.peerConnections[pcId]; + if (peerConnection != nil) { + [peerConnection close]; + [self.peerConnections removeObjectForKey:pcId]; + } + [self.pcFactoryId removeObjectForKey:pcId]; + } + [nf.ownedPcIds removeAllObjects]; + + // 2. Evict every track wrapper this factory created. + NSArray* trackIdsSnapshot = [nf.ownedTrackIds.allObjects copy]; + for (NSString* trackId in trackIdsSnapshot) { + id lt = self.localTracks[trackId]; + if (lt != nil) { + @try { + lt.track.isEnabled = NO; + } @catch (NSException* e) { + // Native peer may already be gone; ignore. + } + } + CapturerStopHandler stopHandler = self.videoCapturerStopHandlers[trackId]; + if (stopHandler) { + @try { + stopHandler(^{ + NSLog(@"[disposePeerConnectionFactory] capturer stopped, trackId = %@", trackId); + }); + } @catch (NSException* e) { + NSLog(@"[disposePeerConnectionFactory] capturer stop failed: %@", e); + } + [self.videoCapturerStopHandlers removeObjectForKey:trackId]; + [self.videoCaptureState removeObjectForKey:trackId]; + } + [self.localTracks removeObjectForKey:trackId]; + [self.trackFactoryId removeObjectForKey:trackId]; + } + [nf.ownedTrackIds removeAllObjects]; + + // 3. Evict every stream wrapper this factory created. + NSArray* streamIdsSnapshot = [nf.ownedStreamIds.allObjects copy]; + for (NSString* streamId in streamIdsSnapshot) { + [self.localStreams removeObjectForKey:streamId]; + } + [nf.ownedStreamIds removeAllObjects]; + + // 4. Tear down the native factory itself. + @try { + [nf dispose]; + } @catch (NSException* e) { + NSLog(@"[disposePeerConnectionFactory] dispose failed: %@", e); + } + + result(nil); + } else if ([@"setVideoEffects" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + NSArray* names = argsMap[@"names"]; + + [self mediaStreamTrackSetVideoEffects:trackId names:names]; + result(nil); + } else if ([@"handleCallInterruptionCallbacks" isEqualToString:call.method]) { + result(@""); + } else if ([@"createPeerConnection" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSDictionary* configuration = argsMap[@"configuration"]; + NSDictionary* constraints = argsMap[@"constraints"]; + NSString* factoryIdArg = argsMap[@"factoryId"]; + + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryIdArg]; + if (nf == nil) { + result([FlutterError + errorWithCode:@"createPeerConnection" + message:[NSString stringWithFormat:@"unknown factoryId %@", factoryIdArg] + details:nil]); + return; + } + + RTCPeerConnection* peerConnection = + [nf.factory peerConnectionWithConfiguration:[self RTCConfiguration:configuration] + constraints:[self parseMediaConstraints:constraints] + delegate:self]; + + peerConnection.remoteStreams = [NSMutableDictionary new]; + peerConnection.remoteTracks = [NSMutableDictionary new]; + peerConnection.dataChannels = [NSMutableDictionary new]; + + NSString* peerConnectionId = [[NSUUID UUID] UUIDString]; + peerConnection.flutterId = peerConnectionId; + + /*Create Event Channel.*/ + peerConnection.eventChannel = [FlutterEventChannel + eventChannelWithName:[NSString stringWithFormat:@"FlutterWebRTC/peerConnectionEvent%@", + peerConnectionId] + binaryMessenger:_messenger]; + [peerConnection.eventChannel setStreamHandler:peerConnection]; + + self.peerConnections[peerConnectionId] = peerConnection; + + self.pcFactoryId[peerConnectionId] = factoryIdArg; + [nf.ownedPcIds addObject:peerConnectionId]; + result(@{@"peerConnectionId" : peerConnectionId}); + } else if ([@"getUserMedia" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSDictionary* constraints = argsMap[@"constraints"]; + NSString* factoryId = argsMap[@"factoryId"]; + [self getUserMedia:constraints factoryId:factoryId result:result]; + } else if ([@"getDisplayMedia" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSDictionary* constraints = argsMap[@"constraints"]; + NSString* factoryId = argsMap[@"factoryId"]; + [self getDisplayMedia:constraints factoryId:factoryId result:result]; + } else if ([@"createLocalMediaStream" isEqualToString:call.method]) { + NSString* factoryId = call.arguments[@"factoryId"]; + [self createLocalMediaStream:factoryId result:result]; + } else if ([@"getSources" isEqualToString:call.method]) { + [self getSources:result]; + } else if ([@"selectAudioInput" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* deviceId = argsMap[@"deviceId"]; + [self selectAudioInput:deviceId result:result]; + } else if ([@"selectAudioOutput" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* deviceId = argsMap[@"deviceId"]; + [self selectAudioOutput:deviceId result:result]; + } else if ([@"triggeriOSAudioRouteSelectionUI" isEqualToString:call.method]) { + [self triggeriOSAudioRouteSelectionUI:result]; + } else if ([@"mediaStreamGetTracks" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* streamId = argsMap[@"streamId"]; + [self mediaStreamGetTracks:streamId result:result]; + } else if ([@"createOffer" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSDictionary* constraints = argsMap[@"constraints"]; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + [self peerConnectionCreateOffer:constraints peerConnection:peerConnection result:result]; + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"createAnswer" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSDictionary* constraints = argsMap[@"constraints"]; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + [self peerConnectionCreateAnswer:constraints peerConnection:peerConnection result:result]; + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"addStream" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + + NSString* streamId = ((NSString*)argsMap[@"streamId"]); + RTCMediaStream* stream = self.localStreams[streamId]; + + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + + if (peerConnection && stream) { + [peerConnection addStream:stream]; + result(@""); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString + stringWithFormat:@"Error: peerConnection or mediaStream not found!"] + details:nil]); + } + } else if ([@"removeStream" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + + NSString* streamId = ((NSString*)argsMap[@"streamId"]); + RTCMediaStream* stream = self.localStreams[streamId]; + + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + + if (peerConnection && stream) { + [peerConnection removeStream:stream]; + result(nil); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString + stringWithFormat:@"Error: peerConnection or mediaStream not found!"] + details:nil]); + } + } else if ([@"captureFrame" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* path = argsMap[@"path"]; + NSString* trackId = argsMap[@"trackId"]; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + + RTCMediaStreamTrack* track = [self trackForId:trackId peerConnectionId:peerConnectionId]; + if (track != nil && [track isKindOfClass:[RTCVideoTrack class]]) { + RTCVideoTrack* videoTrack = (RTCVideoTrack*)track; + [self mediaStreamTrackCaptureFrame:videoTrack toPath:path result:result]; + } else { + if (track == nil) { + result([FlutterError errorWithCode:@"Track is nil" message:nil details:nil]); + } else { + result([FlutterError errorWithCode:[@"Track is class of " + stringByAppendingString:[[track class] description]] + message:nil + details:nil]); + } + } + } else if ([@"setLocalDescription" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + NSDictionary* descriptionMap = argsMap[@"description"]; + NSString* sdp = descriptionMap[@"sdp"]; + RTCSdpType sdpType = [RTCSessionDescription typeForString:descriptionMap[@"type"]]; + RTCSessionDescription* description = [[RTCSessionDescription alloc] initWithType:sdpType + sdp:sdp]; + if (peerConnection) { + [self peerConnectionSetLocalDescription:description + peerConnection:peerConnection + result:result]; + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"setRemoteDescription" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + NSDictionary* descriptionMap = argsMap[@"description"]; + NSString* sdp = descriptionMap[@"sdp"]; + RTCSdpType sdpType = [RTCSessionDescription typeForString:descriptionMap[@"type"]]; + RTCSessionDescription* description = [[RTCSessionDescription alloc] initWithType:sdpType + sdp:sdp]; + + if (peerConnection) { + [self peerConnectionSetRemoteDescription:description + peerConnection:peerConnection + result:result]; + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"sendDtmf" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* tone = argsMap[@"tone"]; + int duration = ((NSNumber*)argsMap[@"duration"]).intValue; + int interToneGap = ((NSNumber*)argsMap[@"gap"]).intValue; + + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + RTCRtpSender* audioSender = nil; + for (RTCRtpSender* rtpSender in peerConnection.senders) { + if ([[[rtpSender track] kind] isEqualToString:@"audio"]) { + audioSender = rtpSender; + } + } + if (audioSender) { + NSOperationQueue* queue = [[NSOperationQueue alloc] init]; + [queue addOperationWithBlock:^{ + double durationMs = duration / 1000.0; + double interToneGapMs = interToneGap / 1000.0; + [audioSender.dtmfSender insertDtmf:(NSString*)tone + duration:(NSTimeInterval)durationMs + interToneGap:(NSTimeInterval)interToneGapMs]; + NSLog(@"DTMF Tone played "); + }]; + } + + result(@{@"result" : @"success"}); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"addCandidate" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSDictionary* candMap = argsMap[@"candidate"]; + NSString* sdp = candMap[@"candidate"]; + id sdpMLineIndexValue = candMap[@"sdpMLineIndex"]; + int sdpMLineIndex = 0; + if (![sdpMLineIndexValue isKindOfClass:[NSNull class]]) { + sdpMLineIndex = ((NSNumber*)candMap[@"sdpMLineIndex"]).intValue; + } + NSString* sdpMid = candMap[@"sdpMid"]; + + RTCIceCandidate* candidate = [[RTCIceCandidate alloc] initWithSdp:sdp + sdpMLineIndex:sdpMLineIndex + sdpMid:sdpMid]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + + if (peerConnection) { + [self peerConnectionAddICECandidate:candidate peerConnection:peerConnection result:result]; + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"getStats" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + id trackId = argsMap[@"trackId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + if (trackId != nil && trackId != [NSNull null]) { + return [self peerConnectionGetStatsForTrackId:trackId + peerConnection:peerConnection + result:result]; + } else { + return [self peerConnectionGetStats:peerConnection result:result]; + } + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"createDataChannel" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* label = argsMap[@"label"]; + NSDictionary* dataChannelDict = (NSDictionary*)argsMap[@"dataChannelDict"]; + [self createDataChannel:peerConnectionId + label:label + config:[self RTCDataChannelConfiguration:dataChannelDict] + messenger:_messenger + result:result]; + } else if ([@"dataChannelSend" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* dataChannelId = argsMap[@"dataChannelId"]; + NSString* type = argsMap[@"type"]; + id data = argsMap[@"data"]; + + [self dataChannelSend:peerConnectionId dataChannelId:dataChannelId data:data type:type]; + result(nil); + } else if ([@"dataChannelGetBufferedAmount" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* dataChannelId = argsMap[@"dataChannelId"]; + + [self dataChannelGetBufferedAmount:peerConnectionId dataChannelId:dataChannelId result:result]; + } else if ([@"dataChannelClose" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* dataChannelId = argsMap[@"dataChannelId"]; + [self dataChannelClose:peerConnectionId dataChannelId:dataChannelId]; + result(nil); + } else if ([@"streamDispose" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* streamId = argsMap[@"streamId"]; + RTCMediaStream* stream = self.localStreams[streamId]; + NativePeerConnectionFactory* ownerNf = [self resolveFactoryForStreamId:streamId]; + BOOL shouldCallResult = YES; + if (stream) { + for (RTCVideoTrack* track in stream.videoTracks) { + [_localTracks removeObjectForKey:track.trackId]; + [self.trackFactoryId removeObjectForKey:track.trackId]; + if (ownerNf != nil) { + [ownerNf.ownedTrackIds removeObject:track.trackId]; + } + RTCVideoTrack* videoTrack = (RTCVideoTrack*)track; + FlutterRTCVideoRenderer* renderer = [self findRendererByTrackId:videoTrack.trackId]; + if (renderer != nil) { + renderer.videoTrack = nil; + } + CapturerStopHandler stopHandler = self.videoCapturerStopHandlers[videoTrack.trackId]; + if (stopHandler) { + shouldCallResult = NO; + stopHandler(^{ + NSLog(@"video capturer stopped, trackID = %@", videoTrack.trackId); + self.videoCapturer = nil; + result(nil); + }); + [self.videoCapturerStopHandlers removeObjectForKey:videoTrack.trackId]; + [self.videoCaptureState removeObjectForKey:videoTrack.trackId]; + } + } + for (RTCAudioTrack* track in stream.audioTracks) { + [_localTracks removeObjectForKey:track.trackId]; + [self.trackFactoryId removeObjectForKey:track.trackId]; + if (ownerNf != nil) { + [ownerNf.ownedTrackIds removeObject:track.trackId]; + } + } + [self.localStreams removeObjectForKey:streamId]; + [self deactiveRtcAudioSession]; + } + if (ownerNf != nil) { + [ownerNf.ownedStreamIds removeObject:streamId]; + } + if (shouldCallResult) { + // do not call if will be called in stopCapturer above. + result(nil); + } + } else if ([@"mediaStreamTrackSetEnable" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + NSNumber* enabled = argsMap[@"enabled"]; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + + RTCMediaStreamTrack* track = [self trackForId:trackId peerConnectionId:peerConnectionId]; + if (track != nil) { + @try { + track.isEnabled = enabled.boolValue; + } @catch (NSException* e) { + NSLog(@"mediaStreamTrackSetEnable: track %@ stale: %@", trackId, e); + } + } + result(nil); + } else if ([@"mediaStreamAddTrack" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* streamId = argsMap[@"streamId"]; + NSString* trackId = argsMap[@"trackId"]; + + RTCMediaStream* stream = self.localStreams[streamId]; + if (stream) { + RTCMediaStreamTrack* track = [self trackForId:trackId peerConnectionId:nil]; + if (track != nil) { + if ([track isKindOfClass:[RTCAudioTrack class]]) { + RTCAudioTrack* audioTrack = (RTCAudioTrack*)track; + [stream addAudioTrack:audioTrack]; + } else if ([track isKindOfClass:[RTCVideoTrack class]]) { + RTCVideoTrack* videoTrack = (RTCVideoTrack*)track; + [stream addVideoTrack:videoTrack]; + } + } else { + result([FlutterError errorWithCode:@"mediaStreamAddTrack: Track is nil" + message:nil + details:nil]); + } + } else { + result([FlutterError errorWithCode:@"mediaStreamAddTrack: Stream is nil" + message:nil + details:nil]); + } + result(nil); + } else if ([@"mediaStreamRemoveTrack" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* streamId = argsMap[@"streamId"]; + NSString* trackId = argsMap[@"trackId"]; + RTCMediaStream* stream = self.localStreams[streamId]; + if (stream) { + id track = self.localTracks[trackId]; + if (track != nil) { + if ([track isKindOfClass:[LocalAudioTrack class]]) { + RTCAudioTrack* audioTrack = ((LocalAudioTrack*)track).audioTrack; + [stream removeAudioTrack:audioTrack]; + } else if ([track isKindOfClass:[LocalVideoTrack class]]) { + RTCVideoTrack* videoTrack = ((LocalVideoTrack*)track).videoTrack; + [stream removeVideoTrack:videoTrack]; + } + } else { + result([FlutterError errorWithCode:@"mediaStreamRemoveTrack: Track is nil" + message:nil + details:nil]); + } + } else { + result([FlutterError errorWithCode:@"mediaStreamRemoveTrack: Stream is nil" + message:nil + details:nil]); + } + result(nil); + } else if ([@"trackDispose" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + BOOL audioTrack = NO; + for (NSString* streamId in self.localStreams) { + RTCMediaStream* stream = [self.localStreams objectForKey:streamId]; + for (RTCAudioTrack* track in stream.audioTracks) { + if ([trackId isEqualToString:track.trackId]) { + [stream removeAudioTrack:track]; + audioTrack = YES; + } + } + for (RTCVideoTrack* track in stream.videoTracks) { + if ([trackId isEqualToString:track.trackId]) { + [stream removeVideoTrack:track]; + CapturerStopHandler stopHandler = self.videoCapturerStopHandlers[track.trackId]; + if (stopHandler) { + stopHandler(^{ + NSLog(@"video capturer stopped, trackID = %@", track.trackId); + }); + [self.videoCapturerStopHandlers removeObjectForKey:track.trackId]; + [self.videoCaptureState removeObjectForKey:track.trackId]; + } + } + } + } + NSString* ownerFactoryId = self.trackFactoryId[trackId]; + [_localTracks removeObjectForKey:trackId]; + [self.trackFactoryId removeObjectForKey:trackId]; + if (ownerFactoryId != nil) { + NativePeerConnectionFactory* ownerNf = [self resolveFactoryForId:ownerFactoryId]; + if (ownerNf != nil) { + [ownerNf.ownedTrackIds removeObject:trackId]; + } + } + if (audioTrack) { + [self ensureAudioSession]; + } + FlutterRTCVideoRenderer* renderer = [self findRendererByTrackId:trackId]; + if (renderer != nil) { + renderer.videoTrack = nil; + } + result(nil); + } else if ([@"restartIce" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (!peerConnection) { + result([FlutterError errorWithCode:@"restartIce: peerConnection is nil" + message:nil + details:nil]); + } else { + [peerConnection restartIce]; + result(nil); + } + } else if ([@"peerConnectionClose" isEqualToString:call.method] || + [@"peerConnectionDispose" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + BOOL isDispose = [@"peerConnectionDispose" isEqualToString:call.method]; + + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + [peerConnection close]; + [self.peerConnections removeObjectForKey:peerConnectionId]; + + for (NSString* trackId in peerConnection.remoteTracks) { + [self.pausedTrackVolumes removeObjectForKey:trackId]; + [self.trackVolumeCache removeObjectForKey:trackId]; + } + + // Clean up peerConnection's streams and tracks + [peerConnection.remoteStreams removeAllObjects]; + [peerConnection.remoteTracks removeAllObjects]; + + // Clean up peerConnection's dataChannels. + NSMutableDictionary* dataChannels = peerConnection.dataChannels; + for (NSString* dataChannelId in dataChannels) { + dataChannels[dataChannelId].delegate = nil; + // There is no need to close the RTCDataChannel because it is owned by the + // RTCPeerConnection and the latter will close the former. + } + [dataChannels removeAllObjects]; + } + + NSString* factoryIdForPc = self.pcFactoryId[peerConnectionId]; + if (factoryIdForPc != nil) { + [self.pcFactoryId removeObjectForKey:peerConnectionId]; + NativePeerConnectionFactory* nf; + @synchronized(self) { + nf = self.factories[factoryIdForPc]; + } + if (nf != nil) { + [nf.ownedPcIds removeObject:peerConnectionId]; + } + } + [self deactiveRtcAudioSession]; + result(nil); + } else if ([@"createVideoRenderer" isEqualToString:call.method]) { + FlutterRTCVideoRenderer* render = [self createWithTextureRegistry:_textures + messenger:_messenger]; + self.renders[@(render.textureId)] = render; + result(@{@"textureId" : @(render.textureId)}); + } else if ([@"videoRendererDispose" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSNumber* textureId = argsMap[@"textureId"]; + FlutterRTCVideoRenderer* render = self.renders[textureId]; + if (render != nil) { + render.videoTrack = nil; + [render dispose]; + [self.renders removeObjectForKey:textureId]; + } + result(nil); + } else if ([@"videoRendererSetSrcObject" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSNumber* textureId = argsMap[@"textureId"]; + FlutterRTCVideoRenderer* render = self.renders[textureId]; + NSString* streamId = argsMap[@"streamId"]; + NSString* ownerTag = argsMap[@"ownerTag"]; + NSString* trackId = argsMap[@"trackId"]; + if (!render) { + result([FlutterError errorWithCode:@"videoRendererSetSrcObject: render is nil" + message:nil + details:nil]); + return; + } + RTCMediaStream* stream = nil; + RTCVideoTrack* videoTrack = nil; + if ([ownerTag isEqualToString:@"local"]) { + stream = _localStreams[streamId]; + } + if (!stream) { + stream = [self streamForId:streamId peerConnectionId:ownerTag]; + } + if (stream) { + NSArray* videoTracks = stream ? stream.videoTracks : nil; + videoTrack = videoTracks && videoTracks.count ? videoTracks[0] : nil; + for (RTCVideoTrack* track in videoTracks) { + if ([track.trackId isEqualToString:trackId]) { + videoTrack = track; + } + } + if (!videoTrack) { + NSLog(@"Not found video track for RTCMediaStream: %@", streamId); + } + } + [self rendererSetSrcObject:render stream:videoTrack]; + result(nil); + } else if ([@"enableIOSMultitaskingCameraAccess" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + BOOL enable = [argsMap[@"enable"] boolValue]; + + [self enableMultitaskingCameraAccess:enable result:result]; + } else if ([@"mediaStreamTrackHasTorch" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + id track = self.localTracks[trackId]; + if (track != nil && [track isKindOfClass:[LocalVideoTrack class]]) { + RTCVideoTrack* videoTrack = ((LocalVideoTrack*)track).videoTrack; + [self mediaStreamTrackHasTorch:videoTrack result:result]; + } else { + if (track == nil) { + result([FlutterError errorWithCode:@"Track is nil" message:nil details:nil]); + } else { + result([FlutterError errorWithCode:[@"Track is class of " + stringByAppendingString:[[track class] description]] + message:nil + details:nil]); + } + } + } else if ([@"mediaStreamTrackSetTorch" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + BOOL torch = [argsMap[@"torch"] boolValue]; + id track = self.localTracks[trackId]; + if (track != nil && [track isKindOfClass:[LocalVideoTrack class]]) { + RTCVideoTrack* videoTrack = ((LocalVideoTrack*)track).videoTrack; + [self mediaStreamTrackSetTorch:videoTrack torch:torch result:result]; + } else { + if (track == nil) { + result([FlutterError errorWithCode:@"Track is nil" message:nil details:nil]); + } else { + result([FlutterError errorWithCode:[@"Track is class of " + stringByAppendingString:[[track class] description]] + message:nil + details:nil]); + } + } + } else if ([@"mediaStreamTrackSetZoom" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + double zoomLevel = [argsMap[@"zoomLevel"] doubleValue]; + id track = self.localTracks[trackId]; + if (track != nil && [track isKindOfClass:[LocalVideoTrack class]]) { + RTCVideoTrack* videoTrack = ((LocalVideoTrack*)track).videoTrack; + [self mediaStreamTrackSetZoom:videoTrack zoomLevel:zoomLevel result:result]; + } else { + if (track == nil) { + result([FlutterError errorWithCode:@"Track is nil" message:nil details:nil]); + } else { + result([FlutterError errorWithCode:[@"Track is class of " + stringByAppendingString:[[track class] description]] + message:nil + details:nil]); + } + } + } else if ([@"mediaStreamTrackSetFocusMode" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + NSString* focusMode = argsMap[@"focusMode"]; + id track = self.localTracks[trackId]; + if (track != nil && focusMode != nil && [track isKindOfClass:[LocalVideoTrack class]]) { + RTCVideoTrack* videoTrack = (RTCVideoTrack*)track.track; + [self mediaStreamTrackSetFocusMode:videoTrack focusMode:focusMode result:result]; + } else { + if (track == nil) { + result([FlutterError errorWithCode:@"Track is nil" message:nil details:nil]); + } else { + result([FlutterError errorWithCode:[@"Track is class of " + stringByAppendingString:[[track class] description]] + message:nil + details:nil]); + } + } + } else if ([@"mediaStreamTrackSetFocusPoint" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + NSDictionary* focusPoint = argsMap[@"focusPoint"]; + id track = self.localTracks[trackId]; + if (track != nil && focusPoint != nil && [track isKindOfClass:[LocalVideoTrack class]]) { + RTCVideoTrack* videoTrack = (RTCVideoTrack*)track.track; + [self mediaStreamTrackSetFocusPoint:videoTrack focusPoint:focusPoint result:result]; + } else { + if (track == nil) { + result([FlutterError errorWithCode:@"Track is nil" message:nil details:nil]); + } else { + result([FlutterError errorWithCode:[@"Track is class of " + stringByAppendingString:[[track class] description]] + message:nil + details:nil]); + } + } + } else if ([@"mediaStreamTrackSetExposureMode" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + NSString* exposureMode = argsMap[@"exposureMode"]; + id track = self.localTracks[trackId]; + if (track != nil && exposureMode != nil && [track isKindOfClass:[LocalVideoTrack class]]) { + RTCVideoTrack* videoTrack = (RTCVideoTrack*)track.track; + [self mediaStreamTrackSetExposureMode:videoTrack exposureMode:exposureMode result:result]; + } else { + if (track == nil) { + result([FlutterError errorWithCode:@"Track is nil" message:nil details:nil]); + } else { + result([FlutterError errorWithCode:[@"Track is class of " + stringByAppendingString:[[track class] description]] + message:nil + details:nil]); + } + } + } else if ([@"mediaStreamTrackSetExposurePoint" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + NSDictionary* exposurePoint = argsMap[@"exposurePoint"]; + id track = self.localTracks[trackId]; + if (track != nil && exposurePoint != nil && [track isKindOfClass:[LocalVideoTrack class]]) { + RTCVideoTrack* videoTrack = (RTCVideoTrack*)track.track; + [self mediaStreamTrackSetExposurePoint:videoTrack exposurePoint:exposurePoint result:result]; + } else { + if (track == nil) { + result([FlutterError errorWithCode:@"Track is nil" message:nil details:nil]); + } else { + result([FlutterError errorWithCode:[@"Track is class of " + stringByAppendingString:[[track class] description]] + message:nil + details:nil]); + } + } + } else if ([@"mediaStreamTrackSwitchCamera" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + id track = self.localTracks[trackId]; + if (track != nil && [track isKindOfClass:[LocalVideoTrack class]]) { + RTCVideoTrack* videoTrack = (RTCVideoTrack*)track.track; + [self mediaStreamTrackSwitchCamera:videoTrack result:result]; + } else { + if (track == nil) { + result([FlutterError errorWithCode:@"Track is nil" message:nil details:nil]); + } else { + result([FlutterError errorWithCode:[@"Track is class of " + stringByAppendingString:[[track class] description]] + message:nil + details:nil]); + } + } + } else if ([@"setVolume" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + NSNumber* volume = argsMap[@"volume"]; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + + RTCMediaStreamTrack* track = [self trackForId:trackId peerConnectionId:peerConnectionId]; + if (track != nil && [track isKindOfClass:[RTCAudioTrack class]]) { + RTCAudioTrack* audioTrack = (RTCAudioTrack*)track; + RTCAudioSource* audioSource = audioTrack.source; + self.trackVolumeCache[trackId] = volume; + if (self.pausedTrackVolumes[trackId] != nil) { + self.pausedTrackVolumes[trackId] = volume; + audioSource.volume = 0.0; + } else { + audioSource.volume = [volume doubleValue]; + } + } + result(nil); + } else if ([@"trackClone" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + RTCMediaStreamTrack* track = [self cloneTrack:trackId]; + + if (track == nil) { + result([FlutterError errorWithCode:@"TRACK_CLONE_FAILED" + message:@"Cannot clone track: source factory not found" + details:@{@"trackId" : trackId ?: [NSNull null]}]); + } else { + result([self mediaTrackToMap:track]); + } + } else if ([@"setMicrophoneMute" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* trackId = argsMap[@"trackId"]; + NSNumber* mute = argsMap[@"mute"]; + id track = self.localTracks[trackId]; + if (track != nil && [track isKindOfClass:[LocalAudioTrack class]]) { + RTCAudioTrack* audioTrack = ((LocalAudioTrack*)track).audioTrack; + audioTrack.isEnabled = !mute.boolValue; + } + result(nil); + } else if ([@"getLocalDescription" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + RTCSessionDescription* sdp = peerConnection.localDescription; + if (nil == sdp) { + result(nil); + } else { + NSString* type = [RTCSessionDescription stringForType:sdp.type]; + result(@{@"sdp" : sdp.sdp, @"type" : type}); + } + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"getRemoteDescription" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + RTCSessionDescription* sdp = peerConnection.remoteDescription; + if (nil == sdp) { + result(nil); + } else { + NSString* type = [RTCSessionDescription stringForType:sdp.type]; + result(@{@"sdp" : sdp.sdp, @"type" : type}); + } + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"setConfiguration" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSDictionary* configuration = argsMap[@"configuration"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + [self peerConnectionSetConfiguration:[self RTCConfiguration:configuration] + peerConnection:peerConnection]; + result(nil); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"addTrack" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* trackId = argsMap[@"trackId"]; + NSArray* streamIds = argsMap[@"streamIds"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + + RTCMediaStreamTrack* track = [self trackForId:trackId peerConnectionId:nil]; + if (track == nil) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: track not found!"] + details:nil]); + return; + } + RTCRtpSender* sender = [peerConnection addTrack:track streamIds:streamIds]; + if (sender == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection.addTrack failed!"] + details:nil]); + return; + } + + result([self rtpSenderToMap:sender]); + } else if ([@"removeTrack" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* senderId = argsMap[@"senderId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + RTCRtpSender* sender = [self getRtpSenderById:peerConnection Id:senderId]; + if (sender == nil) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: sender not found!"] + details:nil]); + return; + } + result(@{@"result" : @([peerConnection removeTrack:sender])}); + } else if ([@"addTransceiver" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSDictionary* transceiverInit = argsMap[@"transceiverInit"]; + NSString* trackId = argsMap[@"trackId"]; + NSString* mediaType = argsMap[@"mediaType"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + RTCRtpTransceiver* transceiver = nil; + BOOL hasAudio = NO; + if (trackId != nil) { + RTCMediaStreamTrack* track = [self trackForId:trackId peerConnectionId:nil]; + if (transceiverInit != nil) { + RTCRtpTransceiverInit* init = [self mapToTransceiverInit:transceiverInit]; + transceiver = [peerConnection addTransceiverWithTrack:track init:init]; + } else { + transceiver = [peerConnection addTransceiverWithTrack:track]; + } + if ([track.kind isEqualToString:@"audio"]) { + hasAudio = YES; + } + } else if (mediaType != nil) { + RTCRtpMediaType rtpMediaType = [self stringToRtpMediaType:mediaType]; + if (transceiverInit != nil) { + RTCRtpTransceiverInit* init = [self mapToTransceiverInit:transceiverInit]; + transceiver = [peerConnection addTransceiverOfType:(rtpMediaType) init:init]; + } else { + transceiver = [peerConnection addTransceiverOfType:rtpMediaType]; + } + if (rtpMediaType == RTCRtpMediaTypeAudio) { + hasAudio = YES; + } + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: Incomplete parameters!"] + details:nil]); + return; + } + + if (transceiver == nil) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: can't addTransceiver!"] + details:nil]); + return; + } + + result([self transceiverToMap:transceiver]); + } else if ([@"rtpTransceiverSetDirection" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* direction = argsMap[@"direction"]; + NSString* transceiverId = argsMap[@"transceiverId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + RTCRtpTransceiver* transcevier = [self getRtpTransceiverById:peerConnection Id:transceiverId]; + if (transcevier == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: transcevier not found!"] + details:nil]); + return; + } + [transcevier setDirection:[self stringToTransceiverDirection:direction] error:nil]; + result(nil); + } else if ([@"rtpTransceiverGetCurrentDirection" isEqualToString:call.method] || + [@"rtpTransceiverGetDirection" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* transceiverId = argsMap[@"transceiverId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + RTCRtpTransceiver* transcevier = [self getRtpTransceiverById:peerConnection Id:transceiverId]; + if (transcevier == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: transcevier not found!"] + details:nil]); + return; + } + + if ([@"rtpTransceiverGetDirection" isEqualToString:call.method]) { + result(@{@"result" : [self transceiverDirectionString:transcevier.direction]}); + } else if ([@"rtpTransceiverGetCurrentDirection" isEqualToString:call.method]) { + RTCRtpTransceiverDirection directionOut = transcevier.direction; + if ([transcevier currentDirection:&directionOut]) { + result(@{@"result" : [self transceiverDirectionString:directionOut]}); + } else { + result(nil); + } + } + } else if ([@"rtpTransceiverStop" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* transceiverId = argsMap[@"transceiverId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + RTCRtpTransceiver* transcevier = [self getRtpTransceiverById:peerConnection Id:transceiverId]; + if (transcevier == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: transcevier not found!"] + details:nil]); + return; + } + [transcevier stopInternal]; + result(nil); + } else if ([@"rtpSenderSetParameters" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* senderId = argsMap[@"rtpSenderId"]; + NSDictionary* parameters = argsMap[@"parameters"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + RTCRtpSender* sender = [self getRtpSenderById:peerConnection Id:senderId]; + if (sender == nil) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: sender not found!"] + details:nil]); + return; + } + [sender setParameters:[self updateRtpParameters:sender.parameters with:parameters]]; + + result(@{@"result" : @(YES)}); + } else if ([@"rtpSenderReplaceTrack" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* senderId = argsMap[@"rtpSenderId"]; + NSString* trackId = argsMap[@"trackId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + RTCRtpSender* sender = [self getRtpSenderById:peerConnection Id:senderId]; + if (sender == nil) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: sender not found!"] + details:nil]); + return; + } + RTCMediaStreamTrack* track = nil; + if ([trackId length] > 0) { + track = [self trackForId:trackId peerConnectionId:nil]; + if (track == nil) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: track not found!"] + details:nil]); + return; + } + } + [sender setTrack:track]; + result(nil); + } else if ([@"rtpSenderSetTrack" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* senderId = argsMap[@"rtpSenderId"]; + NSString* trackId = argsMap[@"trackId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + RTCRtpSender* sender = [self getRtpSenderById:peerConnection Id:senderId]; + if (sender == nil) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: sender not found!"] + details:nil]); + return; + } + RTCMediaStreamTrack* track = nil; + if ([trackId length] > 0) { + track = [self trackForId:trackId peerConnectionId:nil]; + if (track == nil) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: track not found!"] + details:nil]); + return; + } + } + [sender setTrack:track]; + result(nil); + } else if ([@"rtpSenderSetStreams" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + NSString* senderId = argsMap[@"rtpSenderId"]; + NSArray* streamIds = argsMap[@"streamIds"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + RTCRtpSender* sender = [self getRtpSenderById:peerConnection Id:senderId]; + if (sender == nil) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: sender not found!"] + details:nil]); + return; + } + [sender setStreamIds:streamIds]; + result(nil); + } else if ([@"getSenders" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + + NSMutableArray* senders = [NSMutableArray array]; + for (RTCRtpSender* sender in peerConnection.senders) { + [senders addObject:[self rtpSenderToMap:sender]]; + } + + result(@{@"senders" : senders}); + } else if ([@"getReceivers" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + + NSMutableArray* receivers = [NSMutableArray array]; + for (RTCRtpReceiver* receiver in peerConnection.receivers) { + [receivers addObject:[self receiverToMap:receiver]]; + } + + result(@{@"receivers" : receivers}); + } else if ([@"getTransceivers" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection == nil) { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + return; + } + + NSMutableArray* transceivers = [NSMutableArray array]; + for (RTCRtpTransceiver* transceiver in peerConnection.transceivers) { + [transceivers addObject:[self transceiverToMap:transceiver]]; + } + + result(@{@"transceivers" : transceivers}); + } else if ([@"getDesktopSources" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + [self getDesktopSources:argsMap result:result]; + } else if ([@"updateDesktopSources" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + [self updateDesktopSources:argsMap result:result]; + } else if ([@"getDesktopSourceThumbnail" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + [self getDesktopSourceThumbnail:argsMap result:result]; + } else if ([@"setCodecPreferences" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + [self transceiverSetCodecPreferences:argsMap result:result]; + } else if ([@"getRtpReceiverCapabilities" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + [self peerConnectionGetRtpReceiverCapabilities:argsMap result:result]; + } else if ([@"getRtpSenderCapabilities" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + [self peerConnectionGetRtpSenderCapabilities:argsMap result:result]; + } else if ([@"getSignalingState" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + result(@{@"state" : [self stringForSignalingState:peerConnection.signalingState]}); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"getIceGatheringState" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + result(@{@"state" : [self stringForICEGatheringState:peerConnection.iceGatheringState]}); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"getIceConnectionState" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + result(@{@"state" : [self stringForICEConnectionState:peerConnection.iceConnectionState]}); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"getConnectionState" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* peerConnectionId = argsMap[@"peerConnectionId"]; + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + if (peerConnection) { + result(@{@"state" : [self stringForPeerConnectionState:peerConnection.connectionState]}); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@Failed", call.method] + message:[NSString stringWithFormat:@"Error: peerConnection not found!"] + details:nil]); + } + } else if ([@"setLogSeverity" isEqualToString:call.method]) { + NSDictionary* argsMap = call.arguments; + NSString* severityStr = argsMap[@"severity"]; + RTCLoggingSeverity severity = [self str2LogSeverity:severityStr]; + [self initLoggerCallback:severity]; + } else if ([@"resumeAudioPlayout" isEqualToString:call.method]) { + self.isAudioPlayoutPaused = NO; + + if (self.pausedTrackVolumes.count == 0) { + result(nil); + return; + } + + NSDictionary* volumesToRestore = + [NSDictionary dictionaryWithDictionary:self.pausedTrackVolumes]; + [self.pausedTrackVolumes removeAllObjects]; + + for (NSString* trackId in volumesToRestore) { + double targetVolume = [volumesToRestore[trackId] doubleValue]; + RTCMediaStreamTrack* track = [self trackForId:trackId peerConnectionId:nil]; + if (track != nil && [track isKindOfClass:[RTCAudioTrack class]]) { + RTCAudioTrack* audioTrack = (RTCAudioTrack*)track; + audioTrack.source.volume = targetVolume; + self.trackVolumeCache[trackId] = @(targetVolume); + } + } + + result(nil); + } else if ([@"pauseAudioPlayout" isEqualToString:call.method]) { + self.isAudioPlayoutPaused = YES; + + for (NSString* peerConnectionId in _peerConnections) { + RTCPeerConnection* peerConnection = _peerConnections[peerConnectionId]; + for (NSString* trackId in peerConnection.remoteTracks) { + RTCMediaStreamTrack* track = peerConnection.remoteTracks[trackId]; + if ([track isKindOfClass:[RTCAudioTrack class]]) { + RTCAudioTrack* audioTrack = (RTCAudioTrack*)track; + if (self.pausedTrackVolumes[trackId] == nil) { + NSNumber* previousVolume = self.trackVolumeCache[trackId]; + if (previousVolume == nil) { + previousVolume = @(1.0); + } + self.pausedTrackVolumes[trackId] = previousVolume; + } + audioTrack.source.volume = 0.0; + } + } + } + + result(nil); + } else if ([@"startLocalRecording" isEqualToString:call.method]) { + NSString* factoryId = call.arguments[@"factoryId"]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil) { + result([FlutterError + errorWithCode:@"startLocalRecording" + message:[NSString stringWithFormat:@"unknown factoryId %@", factoryId] + details:nil]); + return; + } + RTCAudioDeviceModule* adm = nf.audioDeviceModule; + // Run on background queue + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + NSInteger admResult = [adm initAndStartRecording]; + + // Return to main queue + dispatch_async(dispatch_get_main_queue(), ^{ + if (admResult == 0) { + result(nil); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@ failed", call.method] + message:[NSString stringWithFormat:@"Error: adm api failed with code: %ld", + (long)admResult] + details:nil]); + } + }); + }); + } else if ([@"stopLocalRecording" isEqualToString:call.method]) { + NSString* factoryId = call.arguments[@"factoryId"]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil) { + result([FlutterError + errorWithCode:@"stopLocalRecording" + message:[NSString stringWithFormat:@"unknown factoryId %@", factoryId] + details:nil]); + return; + } + RTCAudioDeviceModule* adm = nf.audioDeviceModule; + // Run on background queue + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + NSInteger admResult = [adm stopRecording]; + + // Return to main queue + dispatch_async(dispatch_get_main_queue(), ^{ + if (admResult == 0) { + result(nil); + } else { + result([FlutterError + errorWithCode:[NSString stringWithFormat:@"%@ failed", call.method] + message:[NSString stringWithFormat:@"Error: adm api failed with code: %ld", + (long)admResult] + details:nil]); + } + }); + }); + } else if ([@"suspendAudioPeerConnectionFactory" isEqualToString:call.method]) { + NSString* factoryId = call.arguments[@"factoryId"]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil) { + result([FlutterError + errorWithCode:@"suspendAudioPeerConnectionFactory" + message:[NSString stringWithFormat:@"unknown factoryId %@", factoryId] + details:nil]); + return; + } + RTCAudioDeviceModule* adm = nf.audioDeviceModule; + BOOL wasPlaying = adm.isPlaying; + BOOL wasRecording = adm.isRecording; + nf.wasPlayingBeforeSuspend = wasPlaying; + nf.wasRecordingBeforeSuspend = wasRecording; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + if (wasRecording) + [adm stopRecording]; + if (wasPlaying) + [adm stopPlayout]; + dispatch_async(dispatch_get_main_queue(), ^{ + result(nil); + }); + }); + } else if ([@"resumeAudioPeerConnectionFactory" isEqualToString:call.method]) { + NSString* factoryId = call.arguments[@"factoryId"]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil) { + result([FlutterError + errorWithCode:@"resumeAudioPeerConnectionFactory" + message:[NSString stringWithFormat:@"unknown factoryId %@", factoryId] + details:nil]); + return; + } + RTCAudioDeviceModule* adm = nf.audioDeviceModule; + BOOL restorePlaying = nf.wasPlayingBeforeSuspend; + BOOL restoreRecording = nf.wasRecordingBeforeSuspend; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + if (restorePlaying) { + [adm initPlayout]; + [adm startPlayout]; + } + if (restoreRecording) { + [adm initRecording]; + [adm startRecording]; + } + dispatch_async(dispatch_get_main_queue(), ^{ + nf.wasPlayingBeforeSuspend = NO; + nf.wasRecordingBeforeSuspend = NO; + result(nil); + }); + }); + } else if ([@"isVoiceProcessingEnabled" isEqualToString:call.method]) { + NSString* factoryId = call.arguments[@"factoryId"]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil) { + result(@(NO)); + return; + } + result(@(nf.audioDeviceModule.isVoiceProcessingEnabled)); + } else if ([@"isVoiceProcessingBypassed" isEqualToString:call.method]) { + NSString* factoryId = call.arguments[@"factoryId"]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil) { + result(@(NO)); + return; + } + result(@(nf.audioDeviceModule.isVoiceProcessingBypassed)); + } else if ([@"setIsVoiceProcessingBypassed" isEqualToString:call.method]) { + NSString* factoryId = call.arguments[@"factoryId"]; + NativePeerConnectionFactory* nf = [self resolveFactoryForId:factoryId]; + if (nf == nil) { + result([FlutterError + errorWithCode:@"setIsVoiceProcessingBypassed" + message:[NSString stringWithFormat:@"unknown factoryId %@", factoryId] + details:nil]); + return; + } + NSNumber* value = call.arguments[@"value"]; + nf.audioDeviceModule.voiceProcessingBypassed = value.boolValue; + result(nil); + } else if ([@"setStereoPlayoutPreferred" isEqualToString:call.method]) { + NSNumber* value = call.arguments[@"preferred"]; + BOOL preferred = value.boolValue; + _stereoPlayoutPreferred = preferred; + NSArray* snapshot; + @synchronized(self) { + snapshot = [self.factories.allValues copy]; + } + if (snapshot.count == 0) { + // Nothing to apply yet; will take effect when implicit/per-call + // factory is built. Track the preference via _stereoPlayoutPreferred. + result(nil); + return; + } + for (NativePeerConnectionFactory* nf in snapshot) { + RTCAudioDeviceModule* adm = nf.audioDeviceModule; + if (adm == nil) + continue; + adm.prefersStereoPlayout = preferred; + adm.voiceProcessingBypassed = preferred; + [adm setMuteMode:preferred ? RTCAudioEngineMuteModeInputMixer + : RTCAudioEngineMuteModeVoiceProcessing]; + } + result(nil); + } else if ([@"isStereoPlayoutEnabled" isEqualToString:call.method]) { + // Sample any active factory's ADM. Stereo preference is process-wide + // so the value is consistent across factories; use the implicit one + // (or the first explicit factory) as the source of truth. + NativePeerConnectionFactory* nf; + @synchronized(self) { + nf = self.factories.allValues.firstObject; + } + if (nf == nil || nf.audioDeviceModule == nil) { + result(@(NO)); + return; + } + result(@(nf.audioDeviceModule.isStereoPlayoutEnabled)); + } else if ([@"refreshStereoPlayoutState" isEqualToString:call.method]) { + // Broadcast across every factory's ADM (stereo preference is + // process-wide). + NSArray* snapshot; + @synchronized(self) { + snapshot = [self.factories.allValues copy]; + } + for (NativePeerConnectionFactory* nf in snapshot) { + RTCAudioDeviceModule* adm = nf.audioDeviceModule; + if (adm != nil) { + [adm refreshStereoPlayoutState]; + } + } + result(nil); + } else { + // Frame cryptor was deactivated alongside the iOS ambient-factory removal — + // it routed factory creation through the ambient ADM and Stream SDK does + // not use it. Reviving requires wiring a per-PC factoryId through every + // FlutterRTCFrameCryptor entry point. Until then frame-cryptor calls bubble + // through data-packet cryptor (which does not recognize them) and + // ultimately receive FlutterMethodNotImplemented. + + // if ([self handleFrameCryptorMethodCall:call result:result]) { + // return; + // } else { + // [self handleDataPacketCryptorMethodCall:call result:result]; + // } + + [self handleDataPacketCryptorMethodCall:call result:result]; + } +} + +- (void)dealloc { + [_localTracks removeAllObjects]; + _localTracks = nil; + [_localStreams removeAllObjects]; + _localStreams = nil; + + for (NSString* peerConnectionId in _peerConnections) { + RTCPeerConnection* peerConnection = _peerConnections[peerConnectionId]; + peerConnection.delegate = nil; + [peerConnection close]; + } + [_peerConnections removeAllObjects]; + + // Drain every registered factory (explicit per-call factories the SDK + // forgot to dispose, plus the implicit factory if still around). The + // PCs above were already closed; NativePeerConnectionFactory.dispose now just + // releases the underlying RTCPeerConnectionFactory + ADM. + @synchronized(self) { + for (NSString* fid in _factories.allKeys) { + NativePeerConnectionFactory* nf = _factories[fid]; + @try { + [nf dispose]; + } @catch (NSException* e) { + NSLog(@"[dealloc] native factory dispose failed: %@", e); + } + } + [_factories removeAllObjects]; + } + [_pcFactoryId removeAllObjects]; + [_trackFactoryId removeAllObjects]; +} + +- (BOOL)hasLocalAudioTrack { + for (id key in _localTracks.allKeys) { + id track = [_localTracks objectForKey:key]; + if (track != nil && [track isKindOfClass:[LocalAudioTrack class]]) { + return YES; + } + } + return NO; +} + +#pragma mark - Per-call factory resolution + +- (NativePeerConnectionFactory*)resolveFactoryForId:(NSString*)factoryId { + if (factoryId == nil || factoryId.length == 0) { + return nil; + } + @synchronized(self) { + return _factories[factoryId]; + } +} + +- (NativePeerConnectionFactory*)resolveFactoryForStreamId:(NSString*)streamId { + if (streamId == nil || streamId.length == 0) + return nil; + @synchronized(self) { + for (NativePeerConnectionFactory* nf in _factories.allValues) { + if ([nf.ownedStreamIds containsObject:streamId]) + return nf; + } + } + return nil; +} + +- (void)ensureAudioSession { +} + +- (void)deactiveRtcAudioSession { +} + +- (void)mediaStreamTrackSetVideoEffects:(nonnull NSString*)trackId + names:(nonnull NSArray*)names { + RTCMediaStreamTrack* track = [self trackForId:trackId peerConnectionId:nil]; + + if (track) { + NSLog(@"mediaStreamTrackSetVideoEffects: track found"); + + RTCVideoTrack* videoTrack = (RTCVideoTrack*)track; + RTCVideoSource* videoSource = videoTrack.source; + + NSMutableArray* processors = [[NSMutableArray alloc] init]; + for (NSString* name in names) { + NSObject* processor = [ProcessorProvider getProcessor:name]; + if (processor != nil) { + [processors addObject:processor]; + } + } + + self.videoEffectProcessor = [[VideoEffectProcessor alloc] initWithProcessors:processors + videoSource:videoSource]; + + self.videoCapturer.delegate = self.videoEffectProcessor; + } else { + NSLog(@"mediaStreamTrackSetVideoEffects: track not found"); + } +} + +- (void)enableMultitaskingCameraAccess:(BOOL)enable result:(FlutterResult)result { + NSLog(@"enableMultitaskingCameraAccess: Multitasking camera access is not available on macOS."); + result(@NO); +} + +- (void)mediaStreamGetTracks:(NSString*)streamId result:(FlutterResult)result { + RTCMediaStream* stream = [self streamForId:streamId peerConnectionId:@""]; + if (stream) { + NativePeerConnectionFactory* streamFactory = [self resolveFactoryForStreamId:streamId]; + NSMutableArray* audioTracks = [NSMutableArray array]; + NSMutableArray* videoTracks = [NSMutableArray array]; + + for (RTCMediaStreamTrack* track in stream.audioTracks) { + NSString* trackId = track.trackId; + [self.localTracks setObject:[[LocalAudioTrack alloc] initWithTrack:(RTCAudioTrack*)track] + forKey:trackId]; + if (streamFactory != nil) { + [streamFactory.ownedTrackIds addObject:trackId]; + if (streamFactory.factoryId != nil) { + self.trackFactoryId[trackId] = streamFactory.factoryId; + } + } + [audioTracks addObject:@{ + @"enabled" : @(track.isEnabled), + @"id" : trackId, + @"kind" : track.kind, + @"label" : trackId, + @"readyState" : @"live", + @"remote" : @(NO) + }]; + } + + for (RTCMediaStreamTrack* track in stream.videoTracks) { + NSString* trackId = track.trackId; + [_localTracks setObject:[[LocalVideoTrack alloc] initWithTrack:(RTCVideoTrack*)track] + forKey:trackId]; + if (streamFactory != nil) { + [streamFactory.ownedTrackIds addObject:trackId]; + if (streamFactory.factoryId != nil) { + self.trackFactoryId[trackId] = streamFactory.factoryId; + } + } + [videoTracks addObject:@{ + @"enabled" : @(track.isEnabled), + @"id" : trackId, + @"kind" : track.kind, + @"label" : trackId, + @"readyState" : @"live", + @"remote" : @(NO) + }]; + } + + result(@{@"audioTracks" : audioTracks, @"videoTracks" : videoTracks}); + } else { + result(nil); + } +} + +- (RTCMediaStream*)streamForId:(NSString*)streamId peerConnectionId:(NSString*)peerConnectionId { + RTCMediaStream* stream = nil; + if (peerConnectionId.length > 0) { + RTCPeerConnection* peerConnection = [_peerConnections objectForKey:peerConnectionId]; + stream = peerConnection.remoteStreams[streamId]; + } else { + for (RTCPeerConnection* peerConnection in _peerConnections.allValues) { + stream = peerConnection.remoteStreams[streamId]; + if (stream) { + break; + } + } + } + if (!stream) { + stream = _localStreams[streamId]; + } + return stream; +} + +- (RTCMediaStreamTrack* _Nullable)remoteTrackForId:(NSString* _Nonnull)trackId { + RTCMediaStreamTrack* mediaStreamTrack = nil; + for (NSString* currentId in _peerConnections.allKeys) { + RTCPeerConnection* peerConnection = _peerConnections[currentId]; + mediaStreamTrack = peerConnection.remoteTracks[trackId]; + if (!mediaStreamTrack) { + for (RTCRtpTransceiver* transceiver in peerConnection.transceivers) { + if (transceiver.receiver.track != nil && + [transceiver.receiver.track.trackId isEqual:trackId]) { + mediaStreamTrack = transceiver.receiver.track; + break; + } + } + } + if (mediaStreamTrack) { + break; + } + } + + return mediaStreamTrack; +} + +- (NSString*)audioTrackIdForVideoTrackId:(NSString*)videoTrackId { + NSString* audioTrackId = nil; + + // Iterate through all peerConnections + for (NSString* peerConnectionId in self.peerConnections) { + RTCPeerConnection* peerConnection = self.peerConnections[peerConnectionId]; + + // Iterate through the receivers to find the video track + for (RTCRtpReceiver* receiver in peerConnection.receivers) { + RTCMediaStreamTrack* track = [receiver valueForKey:@"track"]; + if ([track.kind isEqualToString:@"video"] && [track.trackId isEqualToString:videoTrackId]) { + // Found the video track, now look for the audio track in the same peerConnection + for (RTCRtpReceiver* audioReceiver in peerConnection.receivers) { + RTCMediaStreamTrack* audioTrack = [audioReceiver valueForKey:@"track"]; + if ([audioTrack.kind isEqualToString:@"audio"]) { + audioTrackId = audioTrack.trackId; + break; + } + } + break; + } + } + + // If the audioTrackId is found, break out of the loop + if (audioTrackId != nil) { + break; + } + } + + return audioTrackId; +} + +- (RTCMediaStreamTrack*)trackForId:(NSString*)trackId peerConnectionId:(NSString*)peerConnectionId { + id track = _localTracks[trackId]; + RTCMediaStreamTrack* mediaStreamTrack = nil; + if (!track) { + for (NSString* currentId in _peerConnections.allKeys) { + if (peerConnectionId && [currentId isEqualToString:peerConnectionId] == false) { + continue; + } + RTCPeerConnection* peerConnection = _peerConnections[currentId]; + mediaStreamTrack = peerConnection.remoteTracks[trackId]; + if (!mediaStreamTrack) { + for (RTCRtpTransceiver* transceiver in peerConnection.transceivers) { + if (transceiver.receiver.track != nil && + [transceiver.receiver.track.trackId isEqual:trackId]) { + mediaStreamTrack = transceiver.receiver.track; + break; + } + } + } + if (mediaStreamTrack) { + break; + } + } + } else { + mediaStreamTrack = [track track]; + } + return mediaStreamTrack; +} + +- (RTCIceServer*)RTCIceServer:(id)json { + if (!json) { + NSLog(@"a valid iceServer value"); + return nil; + } + + if (![json isKindOfClass:[NSDictionary class]]) { + NSLog(@"must be an object"); + return nil; + } + + NSArray* urls; + if ([json[@"url"] isKindOfClass:[NSString class]]) { + // TODO: 'url' is non-standard + urls = @[ json[@"url"] ]; + } else if ([json[@"urls"] isKindOfClass:[NSString class]]) { + urls = @[ json[@"urls"] ]; + } else { + urls = (NSArray*)json[@"urls"]; + } + + if (json[@"username"] != nil || json[@"credential"] != nil) { + return [[RTCIceServer alloc] initWithURLStrings:urls + username:json[@"username"] + credential:json[@"credential"]]; + } + + return [[RTCIceServer alloc] initWithURLStrings:urls]; +} + +- (nonnull RTCConfiguration*)RTCConfiguration:(id)json { + RTCConfiguration* config = [[RTCConfiguration alloc] init]; + + if (!json) { + return config; + } + + if (![json isKindOfClass:[NSDictionary class]]) { + NSLog(@"must be an object"); + return config; + } + + if (json[@"audioJitterBufferMaxPackets"] != nil && + [json[@"audioJitterBufferMaxPackets"] isKindOfClass:[NSNumber class]]) { + config.audioJitterBufferMaxPackets = [json[@"audioJitterBufferMaxPackets"] intValue]; + } + + if (json[@"bundlePolicy"] != nil && [json[@"bundlePolicy"] isKindOfClass:[NSString class]]) { + NSString* bundlePolicy = json[@"bundlePolicy"]; + if ([bundlePolicy isEqualToString:@"balanced"]) { + config.bundlePolicy = RTCBundlePolicyBalanced; + } else if ([bundlePolicy isEqualToString:@"max-compat"]) { + config.bundlePolicy = RTCBundlePolicyMaxCompat; + } else if ([bundlePolicy isEqualToString:@"max-bundle"]) { + config.bundlePolicy = RTCBundlePolicyMaxBundle; + } + } + + if (json[@"iceBackupCandidatePairPingInterval"] != nil && + [json[@"iceBackupCandidatePairPingInterval"] isKindOfClass:[NSNumber class]]) { + config.iceBackupCandidatePairPingInterval = + [json[@"iceBackupCandidatePairPingInterval"] intValue]; + } + + if (json[@"iceConnectionReceivingTimeout"] != nil && + [json[@"iceConnectionReceivingTimeout"] isKindOfClass:[NSNumber class]]) { + config.iceConnectionReceivingTimeout = [json[@"iceConnectionReceivingTimeout"] intValue]; + } + + if (json[@"iceServers"] != nil && [json[@"iceServers"] isKindOfClass:[NSArray class]]) { + NSMutableArray* iceServers = [NSMutableArray new]; + for (id server in json[@"iceServers"]) { + RTCIceServer* convert = [self RTCIceServer:server]; + if (convert != nil) { + [iceServers addObject:convert]; + } + } + config.iceServers = iceServers; + } + + if (json[@"iceTransportPolicy"] != nil && + [json[@"iceTransportPolicy"] isKindOfClass:[NSString class]]) { + NSString* iceTransportPolicy = json[@"iceTransportPolicy"]; + if ([iceTransportPolicy isEqualToString:@"all"]) { + config.iceTransportPolicy = RTCIceTransportPolicyAll; + } else if ([iceTransportPolicy isEqualToString:@"none"]) { + config.iceTransportPolicy = RTCIceTransportPolicyNone; + } else if ([iceTransportPolicy isEqualToString:@"nohost"]) { + config.iceTransportPolicy = RTCIceTransportPolicyNoHost; + } else if ([iceTransportPolicy isEqualToString:@"relay"]) { + config.iceTransportPolicy = RTCIceTransportPolicyRelay; + } + } + + if (json[@"rtcpMuxPolicy"] != nil && [json[@"rtcpMuxPolicy"] isKindOfClass:[NSString class]]) { + NSString* rtcpMuxPolicy = json[@"rtcpMuxPolicy"]; + if ([rtcpMuxPolicy isEqualToString:@"negotiate"]) { + config.rtcpMuxPolicy = RTCRtcpMuxPolicyNegotiate; + } else if ([rtcpMuxPolicy isEqualToString:@"require"]) { + config.rtcpMuxPolicy = RTCRtcpMuxPolicyRequire; + } + } + + if (json[@"sdpSemantics"] != nil && [json[@"sdpSemantics"] isKindOfClass:[NSString class]]) { + NSString* sdpSemantics = json[@"sdpSemantics"]; + if ([sdpSemantics isEqualToString:@"plan-b"]) { + config.sdpSemantics = RTCSdpSemanticsPlanB; + } else if ([sdpSemantics isEqualToString:@"unified-plan"]) { + config.sdpSemantics = RTCSdpSemanticsUnifiedPlan; + } + } + + if (json[@"maxIPv6Networks"] != nil && + [json[@"maxIPv6Networks"] isKindOfClass:[NSNumber class]]) { + NSNumber* maxIPv6Networks = json[@"maxIPv6Networks"]; + config.maxIPv6Networks = [maxIPv6Networks intValue]; + } + + // === below is private api in webrtc === + if (json[@"tcpCandidatePolicy"] != nil && + [json[@"tcpCandidatePolicy"] isKindOfClass:[NSString class]]) { + NSString* tcpCandidatePolicy = json[@"tcpCandidatePolicy"]; + if ([tcpCandidatePolicy isEqualToString:@"enabled"]) { + config.tcpCandidatePolicy = RTCTcpCandidatePolicyEnabled; + } else if ([tcpCandidatePolicy isEqualToString:@"disabled"]) { + config.tcpCandidatePolicy = RTCTcpCandidatePolicyDisabled; + } + } + + // candidateNetworkPolicy (private api) + if (json[@"candidateNetworkPolicy"] != nil && + [json[@"candidateNetworkPolicy"] isKindOfClass:[NSString class]]) { + NSString* candidateNetworkPolicy = json[@"candidateNetworkPolicy"]; + if ([candidateNetworkPolicy isEqualToString:@"all"]) { + config.candidateNetworkPolicy = RTCCandidateNetworkPolicyAll; + } else if ([candidateNetworkPolicy isEqualToString:@"low_cost"]) { + config.candidateNetworkPolicy = RTCCandidateNetworkPolicyLowCost; + } + } + + // KeyType (private api) + if (json[@"keyType"] != nil && [json[@"keyType"] isKindOfClass:[NSString class]]) { + NSString* keyType = json[@"keyType"]; + if ([keyType isEqualToString:@"RSA"]) { + config.keyType = RTCEncryptionKeyTypeRSA; + } else if ([keyType isEqualToString:@"ECDSA"]) { + config.keyType = RTCEncryptionKeyTypeECDSA; + } + } + + // continualGatheringPolicy (private api) + if (json[@"continualGatheringPolicy"] != nil && + [json[@"continualGatheringPolicy"] isKindOfClass:[NSString class]]) { + NSString* continualGatheringPolicy = json[@"continualGatheringPolicy"]; + if ([continualGatheringPolicy isEqualToString:@"gather_once"]) { + config.continualGatheringPolicy = RTCContinualGatheringPolicyGatherOnce; + } else if ([continualGatheringPolicy isEqualToString:@"gather_continually"]) { + config.continualGatheringPolicy = RTCContinualGatheringPolicyGatherContinually; + } + } + + // audioJitterBufferMaxPackets (private api) + if (json[@"audioJitterBufferMaxPackets"] != nil && + [json[@"audioJitterBufferMaxPackets"] isKindOfClass:[NSNumber class]]) { + NSNumber* audioJitterBufferMaxPackets = json[@"audioJitterBufferMaxPackets"]; + config.audioJitterBufferMaxPackets = [audioJitterBufferMaxPackets intValue]; + } + + // iceConnectionReceivingTimeout (private api) + if (json[@"iceConnectionReceivingTimeout"] != nil && + [json[@"iceConnectionReceivingTimeout"] isKindOfClass:[NSNumber class]]) { + NSNumber* iceConnectionReceivingTimeout = json[@"iceConnectionReceivingTimeout"]; + config.iceConnectionReceivingTimeout = [iceConnectionReceivingTimeout intValue]; + } + + // iceBackupCandidatePairPingInterval (private api) + if (json[@"iceBackupCandidatePairPingInterval"] != nil && + [json[@"iceBackupCandidatePairPingInterval"] isKindOfClass:[NSNumber class]]) { + NSNumber* iceBackupCandidatePairPingInterval = json[@"iceConnectionReceivingTimeout"]; + config.iceBackupCandidatePairPingInterval = [iceBackupCandidatePairPingInterval intValue]; + } + + // audioJitterBufferFastAccelerate (private api) + if (json[@"audioJitterBufferFastAccelerate"] != nil && + [json[@"audioJitterBufferFastAccelerate"] isKindOfClass:[NSNumber class]]) { + NSNumber* audioJitterBufferFastAccelerate = json[@"audioJitterBufferFastAccelerate"]; + config.audioJitterBufferFastAccelerate = [audioJitterBufferFastAccelerate boolValue]; + } + + // pruneTurnPorts (private api) + if (json[@"pruneTurnPorts"] != nil && [json[@"pruneTurnPorts"] isKindOfClass:[NSNumber class]]) { + NSNumber* pruneTurnPorts = json[@"pruneTurnPorts"]; + config.shouldPruneTurnPorts = [pruneTurnPorts boolValue]; + } + + // presumeWritableWhenFullyRelayed (private api) + if (json[@"presumeWritableWhenFullyRelayed"] != nil && + [json[@"presumeWritableWhenFullyRelayed"] isKindOfClass:[NSNumber class]]) { + NSNumber* presumeWritableWhenFullyRelayed = json[@"presumeWritableWhenFullyRelayed"]; + config.shouldPresumeWritableWhenFullyRelayed = [presumeWritableWhenFullyRelayed boolValue]; + } + + // cryptoOptions (private api) + if (json[@"cryptoOptions"] != nil && + [json[@"cryptoOptions"] isKindOfClass:[NSDictionary class]]) { + id options = json[@"cryptoOptions"]; + BOOL srtpEnableGcmCryptoSuites = NO; + BOOL sframeRequireFrameEncryption = NO; + BOOL srtpEnableEncryptedRtpHeaderExtensions = NO; + BOOL srtpEnableAes128Sha1_32CryptoCipher = NO; + + if (options[@"enableGcmCryptoSuites"] != nil && + [options[@"enableGcmCryptoSuites"] isKindOfClass:[NSNumber class]]) { + NSNumber* value = options[@"enableGcmCryptoSuites"]; + srtpEnableGcmCryptoSuites = [value boolValue]; + } + + if (options[@"requireFrameEncryption"] != nil && + [options[@"requireFrameEncryption"] isKindOfClass:[NSNumber class]]) { + NSNumber* value = options[@"requireFrameEncryption"]; + sframeRequireFrameEncryption = [value boolValue]; + } + + if (options[@"enableEncryptedRtpHeaderExtensions"] != nil && + [options[@"enableEncryptedRtpHeaderExtensions"] isKindOfClass:[NSNumber class]]) { + NSNumber* value = options[@"enableEncryptedRtpHeaderExtensions"]; + srtpEnableEncryptedRtpHeaderExtensions = [value boolValue]; + } + + if (options[@"enableAes128Sha1_32CryptoCipher"] != nil && + [options[@"enableAes128Sha1_32CryptoCipher"] isKindOfClass:[NSNumber class]]) { + NSNumber* value = options[@"enableAes128Sha1_32CryptoCipher"]; + srtpEnableAes128Sha1_32CryptoCipher = [value boolValue]; + } + + config.cryptoOptions = [[RTCCryptoOptions alloc] + initWithSrtpEnableGcmCryptoSuites:srtpEnableGcmCryptoSuites + srtpEnableAes128Sha1_32CryptoCipher:srtpEnableAes128Sha1_32CryptoCipher + srtpEnableEncryptedRtpHeaderExtensions:srtpEnableEncryptedRtpHeaderExtensions + sframeRequireFrameEncryption:(BOOL)sframeRequireFrameEncryption]; + } + + return config; +} + +- (RTCDataChannelConfiguration*)RTCDataChannelConfiguration:(id)json { + if (!json) { + return nil; + } + if ([json isKindOfClass:[NSDictionary class]]) { + RTCDataChannelConfiguration* init = [RTCDataChannelConfiguration new]; + + if (json[@"id"]) { + [init setChannelId:(int)[json[@"id"] integerValue]]; + } + if (json[@"ordered"]) { + init.isOrdered = [json[@"ordered"] boolValue]; + } + if (json[@"maxRetransmits"]) { + init.maxRetransmits = [json[@"maxRetransmits"] intValue]; + } + if (json[@"negotiated"]) { + init.isNegotiated = [json[@"negotiated"] boolValue]; + } + if (json[@"protocol"]) { + init.protocol = json[@"protocol"]; + } + return init; + } + return nil; +} + +- (CGRect)parseRect:(NSDictionary*)rect { + return CGRectMake( + [[rect valueForKey:@"left"] doubleValue], [[rect valueForKey:@"top"] doubleValue], + [[rect valueForKey:@"width"] doubleValue], [[rect valueForKey:@"height"] doubleValue]); +} + +- (NSDictionary*)dtmfSenderToMap:(id)dtmf Id:(NSString*)Id { + return @{ + @"dtmfSenderId" : Id, + @"interToneGap" : @(dtmf.interToneGap / 1000.0), + @"duration" : @(dtmf.duration / 1000.0), + }; +} + +- (NSDictionary*)rtpParametersToMap:(RTCRtpParameters*)parameters { + NSDictionary* rtcp = @{ + @"cname" : parameters.rtcp.cname, + @"reducedSize" : @(parameters.rtcp.isReducedSize), + }; + + NSMutableArray* headerExtensions = [NSMutableArray array]; + for (RTCRtpHeaderExtension* headerExtension in parameters.headerExtensions) { + [headerExtensions addObject:@{ + @"uri" : headerExtension.uri, + @"encrypted" : @(headerExtension.encrypted), + @"id" : @(headerExtension.id), + }]; + } + + NSMutableArray* encodings = [NSMutableArray array]; + for (RTCRtpEncodingParameters* encoding in parameters.encodings) { + // non-nil values + NSMutableDictionary* obj = [@{@"active" : @(encoding.isActive)} mutableCopy]; + // optional values + if (encoding.rid != nil) + [obj setObject:encoding.rid forKey:@"rid"]; + if (encoding.minBitrateBps != nil) + [obj setObject:encoding.minBitrateBps forKey:@"minBitrate"]; + if (encoding.maxBitrateBps != nil) + [obj setObject:encoding.maxBitrateBps forKey:@"maxBitrate"]; + if (encoding.maxFramerate != nil) + [obj setObject:encoding.maxFramerate forKey:@"maxFramerate"]; + if (encoding.numTemporalLayers != nil) + [obj setObject:encoding.numTemporalLayers forKey:@"numTemporalLayers"]; + if (encoding.scaleResolutionDownBy != nil) + [obj setObject:encoding.scaleResolutionDownBy forKey:@"scaleResolutionDownBy"]; + if (encoding.ssrc != nil) + [obj setObject:encoding.ssrc forKey:@"ssrc"]; + + [encodings addObject:obj]; + } + + NSMutableArray* codecs = [NSMutableArray array]; + for (RTCRtpCodecParameters* codec in parameters.codecs) { + [codecs addObject:@{ + @"name" : codec.name, + @"payloadType" : @(codec.payloadType), + @"clockRate" : codec.clockRate, + @"numChannels" : codec.numChannels ? codec.numChannels : @(1), + @"parameters" : codec.parameters, + @"kind" : codec.kind + }]; + } + + NSString* degradationPreference = @"balanced"; + if (parameters.degradationPreference != nil) { + if ([parameters.degradationPreference intValue] == RTCDegradationPreferenceMaintainFramerate) { + degradationPreference = @"maintain-framerate"; + } else if ([parameters.degradationPreference intValue] == + RTCDegradationPreferenceMaintainResolution) { + degradationPreference = @"maintain-resolution"; + } else if ([parameters.degradationPreference intValue] == RTCDegradationPreferenceBalanced) { + degradationPreference = @"balanced"; + } else if ([parameters.degradationPreference intValue] == RTCDegradationPreferenceDisabled) { + degradationPreference = @"disabled"; + } + } + + return @{ + @"transactionId" : parameters.transactionId, + @"rtcp" : rtcp, + @"headerExtensions" : headerExtensions, + @"encodings" : encodings, + @"codecs" : codecs, + @"degradationPreference" : degradationPreference, + }; +} + +- (NSString*)streamTrackStateToString:(RTCMediaStreamTrackState)state { + switch (state) { + case RTCMediaStreamTrackStateLive: + return @"live"; + case RTCMediaStreamTrackStateEnded: + return @"ended"; + default: + break; + } + return @""; +} + +- (NSDictionary*)mediaStreamToMap:(RTCMediaStream*)stream ownerTag:(NSString*)ownerTag { + NSMutableArray* audioTracks = [NSMutableArray array]; + NSMutableArray* videoTracks = [NSMutableArray array]; + + for (RTCMediaStreamTrack* track in stream.audioTracks) { + [audioTracks addObject:[self mediaTrackToMap:track]]; + } + + for (RTCMediaStreamTrack* track in stream.videoTracks) { + [videoTracks addObject:[self mediaTrackToMap:track]]; + } + + return @{ + @"streamId" : stream.streamId, + @"ownerTag" : ownerTag, + @"audioTracks" : audioTracks, + @"videoTracks" : videoTracks, + + }; +} + +- (NSDictionary*)mediaTrackToMap:(RTCMediaStreamTrack*)track { + if (track == nil) + return @{}; + NSDictionary* params = @{ + @"enabled" : @(track.isEnabled), + @"id" : track.trackId, + @"kind" : track.kind, + @"label" : track.trackId, + @"readyState" : [self streamTrackStateToString:track.readyState], + @"remote" : @(YES) + }; + return params; +} + +- (NSDictionary*)rtpSenderToMap:(RTCRtpSender*)sender { + NSDictionary* params = @{ + @"senderId" : sender.senderId, + @"ownsTrack" : @(YES), + @"rtpParameters" : [self rtpParametersToMap:sender.parameters], + @"track" : [self mediaTrackToMap:sender.track], + @"dtmfSender" : [self dtmfSenderToMap:sender.dtmfSender Id:sender.senderId] + }; + return params; +} + +- (NSDictionary*)receiverToMap:(RTCRtpReceiver*)receiver { + NSDictionary* params = @{ + @"receiverId" : receiver.receiverId, + @"rtpParameters" : [self rtpParametersToMap:receiver.parameters], + @"track" : [self mediaTrackToMap:receiver.track], + }; + return params; +} + +- (RTCRtpTransceiver*)getRtpTransceiverById:(RTCPeerConnection*)peerConnection Id:(NSString*)Id { + for (RTCRtpTransceiver* transceiver in peerConnection.transceivers) { + NSString* mid = transceiver.mid ? transceiver.mid : @""; + if ([mid isEqualToString:Id]) { + return transceiver; + } + } + return nil; +} + +- (RTCRtpSender*)getRtpSenderById:(RTCPeerConnection*)peerConnection Id:(NSString*)Id { + for (RTCRtpSender* sender in peerConnection.senders) { + if ([sender.senderId isEqualToString:Id]) { + return sender; + } + } + return nil; +} + +- (RTCRtpReceiver*)getRtpReceiverById:(RTCPeerConnection*)peerConnection Id:(NSString*)Id { + for (RTCRtpReceiver* receiver in peerConnection.receivers) { + if ([receiver.receiverId isEqualToString:Id]) { + return receiver; + } + } + return nil; +} + +- (RTCRtpEncodingParameters*)mapToEncoding:(NSDictionary*)map { + RTCRtpEncodingParameters* encoding = [[RTCRtpEncodingParameters alloc] init]; + encoding.isActive = YES; + encoding.scaleResolutionDownBy = [NSNumber numberWithDouble:1.0]; + encoding.numTemporalLayers = [NSNumber numberWithInt:1]; + [encoding setRid:map[@"rid"]]; + + if (map[@"active"] != nil) { + [encoding setIsActive:((NSNumber*)map[@"active"]).boolValue]; + } + + if (map[@"minBitrate"] != nil) { + [encoding setMinBitrateBps:(NSNumber*)map[@"minBitrate"]]; + } + + if (map[@"maxBitrate"] != nil) { + [encoding setMaxBitrateBps:(NSNumber*)map[@"maxBitrate"]]; + } + + if (map[@"maxFramerate"] != nil) { + [encoding setMaxFramerate:(NSNumber*)map[@"maxFramerate"]]; + } + + if (map[@"numTemporalLayers"] != nil) { + [encoding setNumTemporalLayers:(NSNumber*)map[@"numTemporalLayers"]]; + } + + if (map[@"scaleResolutionDownBy"] != nil) { + [encoding setScaleResolutionDownBy:(NSNumber*)map[@"scaleResolutionDownBy"]]; + } + + if (map[@"scalabilityMode"] != nil) { + [encoding setScalabilityMode:(NSString*)map[@"scalabilityMode"]]; + } + + return encoding; +} + +- (RTCRtpTransceiverInit*)mapToTransceiverInit:(NSDictionary*)map { + NSArray* streamIds = map[@"streamIds"]; + NSArray* encodingsParams = map[@"sendEncodings"]; + NSString* direction = map[@"direction"]; + + RTCRtpTransceiverInit* init = [RTCRtpTransceiverInit alloc]; + + if (direction != nil) { + init.direction = [self stringToTransceiverDirection:direction]; + } + + if (streamIds != nil) { + init.streamIds = streamIds; + } + + if (encodingsParams != nil) { + NSMutableArray* sendEncodings = [[NSMutableArray alloc] init]; + for (NSDictionary* map in encodingsParams) { + [sendEncodings addObject:[self mapToEncoding:map]]; + } + [init setSendEncodings:sendEncodings]; + } + return init; +} + +- (RTCRtpMediaType)stringToRtpMediaType:(NSString*)type { + if ([type isEqualToString:@"audio"]) { + return RTCRtpMediaTypeAudio; + } else if ([type isEqualToString:@"video"]) { + return RTCRtpMediaTypeVideo; + } else if ([type isEqualToString:@"data"]) { + return RTCRtpMediaTypeData; + } + return RTCRtpMediaTypeAudio; +} + +- (RTCRtpTransceiverDirection)stringToTransceiverDirection:(NSString*)type { + if ([type isEqualToString:@"sendrecv"]) { + return RTCRtpTransceiverDirectionSendRecv; + } else if ([type isEqualToString:@"sendonly"]) { + return RTCRtpTransceiverDirectionSendOnly; + } else if ([type isEqualToString:@"recvonly"]) { + return RTCRtpTransceiverDirectionRecvOnly; + } else if ([type isEqualToString:@"inactive"]) { + return RTCRtpTransceiverDirectionInactive; + } + return RTCRtpTransceiverDirectionInactive; +} + +- (RTCRtpParameters*)updateRtpParameters:(RTCRtpParameters*)parameters + with:(NSDictionary*)newParameters { + // current encodings + NSArray* currentEncodings = parameters.encodings; + // new encodings + NSArray* newEncodings = [newParameters objectForKey:@"encodings"]; + + NSString* degradationPreference = [newParameters objectForKey:@"degradationPreference"]; + + if (degradationPreference != nil) { + if ([degradationPreference isEqualToString:@"maintain-framerate"]) { + parameters.degradationPreference = + [NSNumber numberWithInt:RTCDegradationPreferenceMaintainFramerate]; + } else if ([degradationPreference isEqualToString:@"maintain-resolution"]) { + parameters.degradationPreference = + [NSNumber numberWithInt:RTCDegradationPreferenceMaintainResolution]; + } else if ([degradationPreference isEqualToString:@"balanced"]) { + parameters.degradationPreference = [NSNumber numberWithInt:RTCDegradationPreferenceBalanced]; + } else if ([degradationPreference isEqualToString:@"disabled"]) { + parameters.degradationPreference = [NSNumber numberWithInt:RTCDegradationPreferenceDisabled]; + } + } + + for (int i = 0; i < [newEncodings count]; i++) { + RTCRtpEncodingParameters* currentParams = nil; + NSDictionary* newParams = [newEncodings objectAtIndex:i]; + NSString* rid = [newParams objectForKey:@"rid"]; + + // update by matching RID + if ([rid isKindOfClass:[NSString class]] && [rid length] != 0) { + // try to find current encoding with same rid + NSUInteger result = + [currentEncodings indexOfObjectPassingTest:^BOOL(RTCRtpEncodingParameters* _Nonnull obj, + NSUInteger idx, BOOL* _Nonnull stop) { + // stop if found object with matching rid + return (*stop = ([rid isEqualToString:obj.rid])); + }]; + + if (result != NSNotFound) { + currentParams = [currentEncodings objectAtIndex:result]; + } + } + + // fall back to update by index + if (currentParams == nil && i < [currentEncodings count]) { + currentParams = [currentEncodings objectAtIndex:i]; + } + + if (currentParams != nil) { + // update values + NSNumber* active = [newParams objectForKey:@"active"]; + if (active != nil) + currentParams.isActive = [active boolValue]; + NSNumber* maxBitrate = [newParams objectForKey:@"maxBitrate"]; + if (maxBitrate != nil) + currentParams.maxBitrateBps = maxBitrate; + NSNumber* minBitrate = [newParams objectForKey:@"minBitrate"]; + if (minBitrate != nil) + currentParams.minBitrateBps = minBitrate; + NSNumber* maxFramerate = [newParams objectForKey:@"maxFramerate"]; + if (maxFramerate != nil) + currentParams.maxFramerate = maxFramerate; + NSNumber* numTemporalLayers = [newParams objectForKey:@"numTemporalLayers"]; + if (numTemporalLayers != nil) + currentParams.numTemporalLayers = numTemporalLayers; + NSNumber* scaleResolutionDownBy = [newParams objectForKey:@"scaleResolutionDownBy"]; + if (scaleResolutionDownBy != nil) + currentParams.scaleResolutionDownBy = scaleResolutionDownBy; + } + } + + return parameters; +} + +- (NSString*)transceiverDirectionString:(RTCRtpTransceiverDirection)direction { + switch (direction) { + case RTCRtpTransceiverDirectionSendRecv: + return @"sendrecv"; + case RTCRtpTransceiverDirectionSendOnly: + return @"sendonly"; + case RTCRtpTransceiverDirectionRecvOnly: + return @"recvonly"; + case RTCRtpTransceiverDirectionInactive: + return @"inactive"; + case RTCRtpTransceiverDirectionStopped: + return @"stopped"; + break; + } + return nil; +} + +- (NSDictionary*)transceiverToMap:(RTCRtpTransceiver*)transceiver { + NSString* mid = transceiver.mid ? transceiver.mid : @""; + NSDictionary* params = @{ + @"transceiverId" : mid, + @"mid" : mid, + @"direction" : [self transceiverDirectionString:transceiver.direction], + @"sender" : [self rtpSenderToMap:transceiver.sender], + @"receiver" : [self receiverToMap:transceiver.receiver] + }; + return params; +} + +- (FlutterRTCVideoRenderer*)findRendererByTrackId:(NSString*)trackId { + for (FlutterRTCVideoRenderer* renderer in self.renders.allValues) { + if (renderer.videoTrack != nil && [renderer.videoTrack.trackId isEqualToString:trackId]) { + return renderer; + } + } + return nil; +} + +#pragma mark - Event Posting Helper + +- (void)postEventWithName:(NSString*)eventName data:(NSDictionary*)data { + if (!self.eventSink) { + return; + } + + NSMutableDictionary* eventData = [NSMutableDictionary dictionaryWithObject:eventName + forKey:@"event"]; + if (data) { + [eventData addEntriesFromDictionary:data]; + } + + postEvent(self.eventSink, eventData); +} + +#pragma mark - RTCAudioDeviceModuleDelegate methods + +// All RTCAudioDeviceModuleDelegate methods are @required (no @optional). +- (void)audioDeviceModuleDidUpdateDevices:(RTCAudioDeviceModule*)audioDeviceModule { + NSLog(@"audioDeviceModule did update devices"); + if (self.eventSink) { + postEvent(self.eventSink, @{@"event" : @"onDeviceChange"}); + } +} + +- (void)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + didUpdateAudioProcessingState:(RTCAudioProcessingState)state { + NSLog(@"audioDeviceModule did update audio processing state - stereoPlayoutEnabled: %d, " + @"voiceProcessingEnabled: %d, voiceProcessingBypassed: %d, voiceProcessingAGCEnabled: %d", + state.stereoPlayoutEnabled, state.voiceProcessingEnabled, state.voiceProcessingBypassed, + state.voiceProcessingAGCEnabled); + if (self.eventSink) { + postEvent(self.eventSink, @{ + @"event" : @"onAudioProcessingStateChanged", + @"stereoPlayoutEnabled" : @(state.stereoPlayoutEnabled), + @"voiceProcessingEnabled" : @(state.voiceProcessingEnabled), + @"voiceProcessingBypassed" : @(state.voiceProcessingBypassed), + @"voiceProcessingAGCEnabled" : @(state.voiceProcessingAGCEnabled) + }); + } +} + +- (void)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + didReceiveSpeechActivityEvent:(RTCSpeechActivityEvent)speechActivityEvent { + if (!self.eventSink) + return; + NSString* type = (speechActivityEvent == RTCSpeechActivityEventStarted) ? @"started" : @"ended"; + postEvent(self.eventSink, @{ + @"event" : @"onSpeechActivityChanged", + @"type" : type, + }); +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + didCreateEngine:(AVAudioEngine*)engine { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + willEnableEngine:(AVAudioEngine*)engine + isPlayoutEnabled:(BOOL)isPlayoutEnabled + isRecordingEnabled:(BOOL)isRecordingEnabled { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + willStartEngine:(AVAudioEngine*)engine + isPlayoutEnabled:(BOOL)isPlayoutEnabled + isRecordingEnabled:(BOOL)isRecordingEnabled { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + didStopEngine:(AVAudioEngine*)engine + isPlayoutEnabled:(BOOL)isPlayoutEnabled + isRecordingEnabled:(BOOL)isRecordingEnabled { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + didDisableEngine:(AVAudioEngine*)engine + isPlayoutEnabled:(BOOL)isPlayoutEnabled + isRecordingEnabled:(BOOL)isRecordingEnabled { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + willReleaseEngine:(AVAudioEngine*)engine { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + engine:(AVAudioEngine*)engine + configureInputFromSource:(AVAudioNode*)source + toDestination:(AVAudioNode*)destination + withFormat:(AVAudioFormat*)format + context:(NSDictionary*)context { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule*)audioDeviceModule + engine:(AVAudioEngine*)engine + configureOutputFromSource:(AVAudioNode*)source + toDestination:(AVAudioNode*)destination + withFormat:(AVAudioFormat*)format + context:(NSDictionary*)context { + return 0; +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/LocalAudioTrack.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/LocalAudioTrack.m new file mode 100644 index 0000000000..1f81a10077 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/LocalAudioTrack.m @@ -0,0 +1,38 @@ +#import "include/stream_webrtc_flutter/LocalAudioTrack.h" +#import "include/stream_webrtc_flutter/AudioManager.h" + +@implementation LocalAudioTrack { + RTCAudioTrack* _track; +} + +@synthesize audioTrack = _track; + +- (instancetype)initWithTrack:(RTCAudioTrack*)track { + self = [super init]; + if (self) { + _track = track; + } + return self; +} + +- (RTCMediaStreamTrack*)track { + return _track; +} + +- (void)addRenderer:(id)renderer { + [AudioManager.sharedInstance addLocalAudioRenderer:renderer]; +} + +- (void)removeRenderer:(id)renderer { + [AudioManager.sharedInstance removeLocalAudioRenderer:renderer]; +} + +- (void)addProcessing:(_Nonnull id)processor { + [AudioManager.sharedInstance.capturePostProcessingAdapter addProcessing:processor]; +} + +- (void)removeProcessing:(_Nonnull id)processor { + [AudioManager.sharedInstance.capturePostProcessingAdapter removeProcessing:processor]; +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/LocalVideoTrack.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/LocalVideoTrack.m new file mode 100644 index 0000000000..200bd6318d --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/LocalVideoTrack.m @@ -0,0 +1,47 @@ +#import "include/stream_webrtc_flutter/LocalVideoTrack.h" + +@implementation LocalVideoTrack { + RTCVideoTrack* _track; + VideoProcessingAdapter* _processing; +} + +@synthesize videoTrack = _track; +@synthesize processing = _processing; + +- (instancetype)initWithTrack:(RTCVideoTrack*)track + videoProcessing:(VideoProcessingAdapter*)processing { + self = [super init]; + if (self) { + _track = track; + _processing = processing; + } + return self; +} + +- (instancetype)initWithTrack:(RTCVideoTrack*)track { + return [self initWithTrack:track videoProcessing:nil]; +} + +- (RTCMediaStreamTrack*)track { + return _track; +} + +/** Register a renderer that will render all frames received on this track. */ +- (void)addRenderer:(id)renderer { + [_track addRenderer:renderer]; +} + +/** Deregister a renderer. */ +- (void)removeRenderer:(id)renderer { + [_track removeRenderer:renderer]; +} + +- (void)addProcessing:(id)processor { + [_processing addProcessing:processor]; +} + +- (void)removeProcessing:(id)processor { + [_processing removeProcessing:processor]; +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/NativePeerConnectionFactory.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/NativePeerConnectionFactory.m new file mode 100644 index 0000000000..0145507b92 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/NativePeerConnectionFactory.m @@ -0,0 +1,98 @@ +#import "include/stream_webrtc_flutter/NativePeerConnectionFactory.h" +#import "include/stream_webrtc_flutter/VideoFactoriesPrivate.h" + +#import + +@implementation NativePeerConnectionFactory { + RTCPeerConnectionFactory* _factory; + RTCAudioDeviceModule* _audioDeviceModule; + BOOL _disposed; +} + +- (instancetype)initWithFactoryId:(NSString*)factoryId + bypassVoiceProcessing:(BOOL)bypassVoiceProcessing + networkIgnoreMask:(NSArray*)networkIgnoreMask + audioProcessingModule:(RTCDefaultAudioProcessingModule*)apm + appleAudioConfiguration:(NSDictionary*)appleAudioConfiguration + admObserver:(id)admObserver { + if (self = [super init]) { + _factoryId = [factoryId copy]; + _bypassVoiceProcessing = bypassVoiceProcessing; + _audioConfigSnapshot = [appleAudioConfiguration copy]; + _ownedPcIds = [NSMutableSet new]; + _ownedTrackIds = [NSMutableSet new]; + _ownedStreamIds = [NSMutableSet new]; + _disposed = NO; + + VideoDecoderFactory* decoderFactory = [[VideoDecoderFactory alloc] init]; + VideoEncoderFactory* encoderFactory = [[VideoEncoderFactory alloc] init]; + VideoEncoderFactorySimulcast* simulcastFactory = + [[VideoEncoderFactorySimulcast alloc] initWithPrimary:encoderFactory + fallback:encoderFactory]; + + _factory = [[RTCPeerConnectionFactory alloc] + initWithAudioDeviceModuleType:RTCAudioDeviceModuleTypeAudioEngine + bypassVoiceProcessing:bypassVoiceProcessing + encoderFactory:simulcastFactory + decoderFactory:decoderFactory + audioProcessingModule:apm]; + + RTCPeerConnectionFactoryOptions* options = [[RTCPeerConnectionFactoryOptions alloc] init]; + for (NSString* adapter in networkIgnoreMask) { + if ([@"adapterTypeEthernet" isEqualToString:adapter]) { + options.ignoreEthernetNetworkAdapter = YES; + } else if ([@"adapterTypeWifi" isEqualToString:adapter]) { + options.ignoreWiFiNetworkAdapter = YES; + } else if ([@"adapterTypeCellular" isEqualToString:adapter]) { + options.ignoreCellularNetworkAdapter = YES; + } else if ([@"adapterTypeVpn" isEqualToString:adapter]) { + options.ignoreVPNNetworkAdapter = YES; + } else if ([@"adapterTypeLoopback" isEqualToString:adapter]) { + options.ignoreLoopbackNetworkAdapter = YES; + } else if ([@"adapterTypeAny" isEqualToString:adapter]) { + options.ignoreEthernetNetworkAdapter = YES; + options.ignoreWiFiNetworkAdapter = YES; + options.ignoreCellularNetworkAdapter = YES; + options.ignoreVPNNetworkAdapter = YES; + options.ignoreLoopbackNetworkAdapter = YES; + } + } + [_factory setOptions:options]; + + _audioDeviceModule = _factory.audioDeviceModule; + if (admObserver != nil) { + _audioDeviceModule.observer = admObserver; + } + + NSLog(@"[NativePeerConnectionFactory] built id: %@ bypass: %d", factoryId, + bypassVoiceProcessing); + } + return self; +} + +- (void)dispose { + if (_disposed) { + return; + } + _disposed = YES; + + if (_audioDeviceModule != nil) { + @try { + [_audioDeviceModule stopRecording]; + [_audioDeviceModule stopPlayout]; + } @catch (NSException* e) { + NSLog(@"[NativePeerConnectionFactory] stopRecording/stopPlayout failed: %@", e); + } + _audioDeviceModule.observer = nil; + _audioDeviceModule = nil; + } + _factory = nil; + + NSLog(@"[NativePeerConnectionFactory] disposed id: %@", _factoryId); +} + +- (BOOL)isDisposed { + return _disposed; +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/ProcessorProvider.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/ProcessorProvider.m new file mode 100644 index 0000000000..4d9b978a3a --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/ProcessorProvider.m @@ -0,0 +1,23 @@ +#import "include/stream_webrtc_flutter/ProcessorProvider.h" + +@implementation ProcessorProvider + +static NSMutableDictionary*>* processorMap; + ++ (void)initialize { + processorMap = [[NSMutableDictionary alloc] init]; +} + ++ (NSObject*)getProcessor:(NSString*)name { + return [processorMap objectForKey:name]; +} + ++ (void)addProcessor:(NSObject*)processor forName:(NSString*)name { + [processorMap setObject:processor forKey:name]; +} + ++ (void)removeProcessor:(NSString*)name { + [processorMap removeObjectForKey:name]; +} + +@end \ No newline at end of file diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/StreamMacAudioDevices.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/StreamMacAudioDevices.m new file mode 100644 index 0000000000..04d0a2fa6d --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/StreamMacAudioDevices.m @@ -0,0 +1,128 @@ +#import "include/stream_webrtc_flutter/StreamMacAudioDevices.h" + +#import + +@implementation StreamMacAudioDevices + +#pragma mark - Public + ++ (NSArray*>*)inputDevices { + return [self enumerateDevicesAsInput:YES]; +} + ++ (NSArray*>*)outputDevices { + return [self enumerateDevicesAsInput:NO]; +} + ++ (BOOL)deviceIdExists:(NSString*)deviceId asInput:(BOOL)input { + if (deviceId.length == 0) + return NO; + NSArray* list = input ? [self inputDevices] : [self outputDevices]; + for (NSDictionary* d in list) { + if ([deviceId isEqualToString:d[@"deviceId"]]) + return YES; + } + return NO; +} + +#pragma mark - Internal + +/// Enumerates all live AudioObjectIDs and returns those that have at least one +/// input (or output) channel — i.e. those that can be selected as a recording +/// or playback device respectively. ++ (NSArray*>*)enumerateDevicesAsInput:(BOOL)input { + NSMutableArray*>* result = [NSMutableArray array]; + + AudioObjectPropertyAddress devicesAddr = { + .mSelector = kAudioHardwarePropertyDevices, + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMain, + }; + UInt32 size = 0; + OSStatus status = + AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &devicesAddr, 0, NULL, &size); + if (status != noErr || size == 0) + return result; + + const NSUInteger count = size / sizeof(AudioObjectID); + AudioObjectID* ids = (AudioObjectID*)malloc(size); + if (ids == NULL) + return result; + status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &devicesAddr, 0, NULL, &size, ids); + if (status != noErr) { + free(ids); + return result; + } + + const AudioObjectPropertyScope scope = + input ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; + + for (NSUInteger i = 0; i < count; i++) { + const AudioObjectID dev = ids[i]; + if (![self device:dev hasChannelsForScope:scope]) + continue; + + NSString* uid = [self stringPropertyOf:dev selector:kAudioDevicePropertyDeviceUID]; + NSString* name = [self stringPropertyOf:dev selector:kAudioObjectPropertyName]; + if (uid.length == 0) + continue; + if (name.length == 0) + name = uid; + + [result addObject:@{ + @"deviceId" : uid, + @"label" : name, + }]; + } + + free(ids); + return result; +} + +/// Returns YES if the device exposes at least one channel for the given +/// scope (input or output). ++ (BOOL)device:(AudioObjectID)dev hasChannelsForScope:(AudioObjectPropertyScope)scope { + AudioObjectPropertyAddress addr = { + .mSelector = kAudioDevicePropertyStreamConfiguration, + .mScope = scope, + .mElement = kAudioObjectPropertyElementMain, + }; + UInt32 size = 0; + OSStatus status = AudioObjectGetPropertyDataSize(dev, &addr, 0, NULL, &size); + if (status != noErr || size == 0) + return NO; + + AudioBufferList* list = (AudioBufferList*)malloc(size); + if (list == NULL) + return NO; + status = AudioObjectGetPropertyData(dev, &addr, 0, NULL, &size, list); + BOOL hasChannels = NO; + if (status == noErr) { + for (UInt32 i = 0; i < list->mNumberBuffers; i++) { + if (list->mBuffers[i].mNumberChannels > 0) { + hasChannels = YES; + break; + } + } + } + free(list); + return hasChannels; +} + +/// Reads a CFString-typed Core Audio property and bridges it to NSString. ++ (NSString*)stringPropertyOf:(AudioObjectID)dev selector:(AudioObjectPropertySelector)sel { + AudioObjectPropertyAddress addr = { + .mSelector = sel, + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMain, + }; + CFStringRef value = NULL; + UInt32 size = sizeof(value); + OSStatus status = AudioObjectGetPropertyData(dev, &addr, 0, NULL, &size, &value); + if (status != noErr || value == NULL) + return @""; + NSString* result = (__bridge_transfer NSString*)value; + return result ?: @""; +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/VideoEffectProcessor.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/VideoEffectProcessor.m new file mode 100644 index 0000000000..772dbb344f --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/VideoEffectProcessor.m @@ -0,0 +1,21 @@ +#import "include/stream_webrtc_flutter/VideoEffectProcessor.h" +#import + +@implementation VideoEffectProcessor +- (instancetype)initWithProcessors: + (NSArray*>*)videoFrameProcessors + videoSource:(RTCVideoSource*)videoSource { + self = [super init]; + _videoFrameProcessors = videoFrameProcessors; + _videoSource = videoSource; + return self; +} +- (void)capturer:(nonnull RTCVideoCapturer*)capturer + didCaptureVideoFrame:(nonnull RTCVideoFrame*)frame { + RTCVideoFrame* processedFrame = frame; + for (NSObject* processor in _videoFrameProcessors) { + processedFrame = [processor capturer:capturer didCaptureVideoFrame:processedFrame]; + } + [self.videoSource capturer:capturer didCaptureVideoFrame:processedFrame]; +} +@end \ No newline at end of file diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/VideoProcessingAdapter.m b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/VideoProcessingAdapter.m new file mode 100644 index 0000000000..95869e3a4a --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/VideoProcessingAdapter.m @@ -0,0 +1,55 @@ +#import "include/stream_webrtc_flutter/VideoProcessingAdapter.h" +#import + +@implementation VideoProcessingAdapter { + RTCVideoSource* _videoSource; + CGSize _frameSize; + NSArray>* _processors; + os_unfair_lock _lock; +} + +- (instancetype)initWithRTCVideoSource:(RTCVideoSource*)source { + self = [super init]; + if (self) { + _lock = OS_UNFAIR_LOCK_INIT; + _videoSource = source; + _processors = [NSArray> new]; + } + return self; +} + +- (RTCVideoSource* _Nonnull)source { + return _videoSource; +} + +- (void)addProcessing:(id)processor { + os_unfair_lock_lock(&_lock); + _processors = [_processors arrayByAddingObject:processor]; + os_unfair_lock_unlock(&_lock); +} + +- (void)removeProcessing:(id)processor { + os_unfair_lock_lock(&_lock); + _processors = [_processors + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, + NSDictionary* bindings) { + return evaluatedObject != processor; + }]]; + os_unfair_lock_unlock(&_lock); +} + +- (void)setSize:(CGSize)size { + _frameSize = size; +} + +- (void)capturer:(RTC_OBJC_TYPE(RTCVideoCapturer) *)capturer + didCaptureVideoFrame:(RTC_OBJC_TYPE(RTCVideoFrame) *)frame { + os_unfair_lock_lock(&_lock); + for (id processor in _processors) { + frame = [processor onFrame:frame]; + } + [_videoSource capturer:capturer didCaptureVideoFrame:frame]; + os_unfair_lock_unlock(&_lock); +} + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/audio_sink_bridge.cpp b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/audio_sink_bridge.cpp new file mode 100644 index 0000000000..f2541e602c --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/audio_sink_bridge.cpp @@ -0,0 +1,19 @@ +#include "include/stream_webrtc_flutter/FlutterRTCAudioSink-Interface.h" +#include "include/stream_webrtc_flutter/media_stream_interface.h" + +class AudioSinkBridge : public webrtc::AudioTrackSinkInterface { + private: + void* sink; + + public: + AudioSinkBridge(void* sink1) { sink = sink1; } + void OnData(const void* audio_data, + int bits_per_sample, + int sample_rate, + size_t number_of_channels, + size_t number_of_frames) override { + RTCAudioSinkCallback(sink, audio_data, bits_per_sample, sample_rate, + number_of_channels, number_of_frames); + }; + int NumPreferredChannels() const override { return 1; } +}; diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/AudioManager.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/AudioManager.h new file mode 100644 index 0000000000..f6d9af4a38 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/AudioManager.h @@ -0,0 +1,19 @@ +#import +#import +#import "AudioProcessingAdapter.h" + +@interface AudioManager : NSObject + +@property(nonatomic, strong) RTCDefaultAudioProcessingModule* _Nonnull audioProcessingModule; + +@property(nonatomic, strong) AudioProcessingAdapter* _Nonnull capturePostProcessingAdapter; + +@property(nonatomic, strong) AudioProcessingAdapter* _Nonnull renderPreProcessingAdapter; + ++ (_Nonnull instancetype)sharedInstance; + +- (void)addLocalAudioRenderer:(nonnull id)renderer; + +- (void)removeLocalAudioRenderer:(nonnull id)renderer; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/AudioProcessingAdapter.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/AudioProcessingAdapter.h new file mode 100644 index 0000000000..1e93b7dd7b --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/AudioProcessingAdapter.h @@ -0,0 +1,26 @@ +#import +#import + +@protocol ExternalAudioProcessingDelegate + +- (void)audioProcessingInitializeWithSampleRate:(size_t)sampleRateHz channels:(size_t)channels; + +- (void)audioProcessingProcess:(RTC_OBJC_TYPE(RTCAudioBuffer) * _Nonnull)audioBuffer; + +- (void)audioProcessingRelease; + +@end + +@interface AudioProcessingAdapter : NSObject + +- (nonnull instancetype)init; + +- (void)addProcessing:(id _Nonnull)processor; + +- (void)removeProcessing:(id _Nonnull)processor; + +- (void)addAudioRenderer:(nonnull id)renderer; + +- (void)removeAudioRenderer:(nonnull id)renderer; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/CameraUtils.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/CameraUtils.h new file mode 100644 index 0000000000..d6859d88ee --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/CameraUtils.h @@ -0,0 +1,43 @@ +#import +#import "FlutterWebRTCPlugin.h" + +@interface FlutterWebRTCPlugin (CameraUtils) + +- (void)mediaStreamTrackHasTorch:(nonnull RTCMediaStreamTrack*)track result:(nonnull FlutterResult)result; + +- (void)mediaStreamTrackSetTorch:(nonnull RTCMediaStreamTrack*)track + torch:(BOOL)torch + result:(nonnull FlutterResult)result; + +- (void)mediaStreamTrackSetZoom:(nonnull RTCMediaStreamTrack*)track + zoomLevel:(double)zoomLevel + result:(nonnull FlutterResult)result; + +- (void)mediaStreamTrackSetFocusMode:(nonnull RTCMediaStreamTrack*)track + focusMode:(nonnull NSString*)focusMode + result:(nonnull FlutterResult)result; + +- (void)mediaStreamTrackSetFocusPoint:(nonnull RTCMediaStreamTrack*)track + focusPoint:(nonnull NSDictionary*)focusPoint + result:(nonnull FlutterResult)result; + +- (void)mediaStreamTrackSetExposureMode:(nonnull RTCMediaStreamTrack*)track + exposureMode:(nonnull NSString*)exposureMode + result:(nonnull FlutterResult)result; + +- (void)mediaStreamTrackSetExposurePoint:(nonnull RTCMediaStreamTrack*)track + exposurePoint:(nonnull NSDictionary*)exposurePoint + result:(nonnull FlutterResult)result; + +- (void)mediaStreamTrackSwitchCamera:(nonnull RTCMediaStreamTrack*)track result:(nonnull FlutterResult)result; + +- (NSInteger)selectFpsForFormat:(nonnull AVCaptureDeviceFormat*)format targetFps:(NSInteger)targetFps; + +- (nullable AVCaptureDeviceFormat*)selectFormatForDevice:(nonnull AVCaptureDevice*)device + targetWidth:(NSInteger)targetWidth + targetHeight:(NSInteger)targetHeight; + +- (nullable AVCaptureDevice*)findDeviceForPosition:(AVCaptureDevicePosition)position; + + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterDataPacketCryptor.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterDataPacketCryptor.h new file mode 100644 index 0000000000..b36847b54b --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterDataPacketCryptor.h @@ -0,0 +1,11 @@ +#import +#import + +#import "FlutterWebRTCPlugin.h" + +@interface FlutterWebRTCPlugin (DataPacketCryptor) + +- (void)handleDataPacketCryptorMethodCall:(nonnull FlutterMethodCall*)call + result:(nonnull FlutterResult)result; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCAudioSink-Interface.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCAudioSink-Interface.h new file mode 100644 index 0000000000..6887e4c669 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCAudioSink-Interface.h @@ -0,0 +1,8 @@ +#import + +void RTCAudioSinkCallback(void* object, + const void* audio_data, + int bits_per_sample, + int sample_rate, + size_t number_of_channels, + size_t number_of_frames); diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCAudioSink.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCAudioSink.h new file mode 100644 index 0000000000..5173b27076 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCAudioSink.h @@ -0,0 +1,14 @@ +#import +#import +#import + +@interface FlutterRTCAudioSink : NSObject + +@property(nonatomic, copy) void (^bufferCallback)(CMSampleBufferRef); +@property(nonatomic) CMAudioFormatDescriptionRef format; + +- (instancetype)initWithAudioTrack:(RTCAudioTrack*)audio; + +- (void)close; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCDataChannel.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCDataChannel.h new file mode 100644 index 0000000000..2b1d685274 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCDataChannel.h @@ -0,0 +1,30 @@ +#import "FlutterWebRTCPlugin.h" + +@interface RTCDataChannel (Flutter) +@property(nonatomic, strong, nonnull) NSString* peerConnectionId; +@property(nonatomic, strong, nonnull) NSString* flutterChannelId; +@property(nonatomic, strong, nullable) FlutterEventSink eventSink; +@property(nonatomic, strong, nullable) FlutterEventChannel* eventChannel; +@property(nonatomic, strong, nullable) NSArray* eventQueue; +@end + +@interface FlutterWebRTCPlugin (RTCDataChannel) + +- (void)createDataChannel:(nonnull NSString*)peerConnectionId + label:(nonnull NSString*)label + config:(nonnull RTCDataChannelConfiguration*)config + messenger:(nonnull NSObject*)messenger + result:(nonnull FlutterResult)result; + +- (void)dataChannelClose:(nonnull NSString*)peerConnectionId + dataChannelId:(nonnull NSString*)dataChannelId; + +- (void)dataChannelSend:(nonnull NSString*)peerConnectionId + dataChannelId:(nonnull NSString*)dataChannelId + data:(nonnull NSString*)data + type:(nonnull NSString*)type; + +- (void)dataChannelGetBufferedAmount:(nonnull NSString*)peerConnectionId + dataChannelId:(nonnull NSString*)dataChannelId + result:(nonnull FlutterResult)result; +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCDesktopCapturer.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCDesktopCapturer.h new file mode 100644 index 0000000000..f40bc328d8 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCDesktopCapturer.h @@ -0,0 +1,20 @@ +#import +#import +#import + +#import "FlutterWebRTCPlugin.h" + +@interface FlutterWebRTCPlugin (DesktopCapturer) + +- (void)getDisplayMedia:(nonnull NSDictionary*)constraints + factoryId:(nullable NSString*)factoryId + result:(nonnull FlutterResult)result; + +- (void)getDesktopSources:(nonnull NSDictionary*)argsMap result:(nonnull FlutterResult)result; + +- (void)updateDesktopSources:(nonnull NSDictionary*)argsMap result:(nonnull FlutterResult)result; + +- (void)getDesktopSourceThumbnail:(nonnull NSDictionary*)argsMap + result:(nonnull FlutterResult)result; + +@end \ No newline at end of file diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCFrameCapturer.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCFrameCapturer.h new file mode 100644 index 0000000000..4f9f6bc2dc --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCFrameCapturer.h @@ -0,0 +1,12 @@ +#import +#import + +@interface FlutterRTCFrameCapturer : NSObject + +- (instancetype)initWithTrack:(RTCVideoTrack*)track + toPath:(NSString*)path + result:(FlutterResult)result; + ++ (CVPixelBufferRef)convertToCVPixelBuffer:(RTCVideoFrame*)frame; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCMediaStream.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCMediaStream.h new file mode 100644 index 0000000000..8e1e5636b7 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCMediaStream.h @@ -0,0 +1,30 @@ +#import +#import "FlutterWebRTCPlugin.h" + +@interface RTCMediaStreamTrack (Flutter) +@property(nonatomic, strong, nonnull) id settings; +@end + +@interface FlutterWebRTCPlugin (RTCMediaStream) + +- (RTCVideoTrack* _Nullable)cloneTrack:(nonnull NSString*)trackId; + +- (void)getUserMedia:(nonnull NSDictionary*)constraints + factoryId:(nullable NSString*)factoryId + result:(nonnull FlutterResult)result; + +- (void)createLocalMediaStream:(nullable NSString*)factoryId + result:(nonnull FlutterResult)result; + +- (void)getSources:(nonnull FlutterResult)result; + +- (void)mediaStreamTrackCaptureFrame:(nonnull RTCMediaStreamTrack*)track + toPath:(nonnull NSString*)path + result:(nonnull FlutterResult)result; + +- (void)selectAudioInput:(nonnull NSString*)deviceId result:(nullable FlutterResult)result; + +- (void)selectAudioOutput:(nonnull NSString*)deviceId result:(nullable FlutterResult)result; + +- (void)triggeriOSAudioRouteSelectionUI:(nonnull FlutterResult)result; +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCPeerConnection.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCPeerConnection.h new file mode 100644 index 0000000000..bd86076209 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCPeerConnection.h @@ -0,0 +1,65 @@ +#import "FlutterWebRTCPlugin.h" + +@interface RTCPeerConnection (Flutter) +@property(nonatomic, strong, nonnull) NSMutableDictionary* dataChannels; +@property(nonatomic, strong, nonnull) + NSMutableDictionary* remoteStreams; +@property(nonatomic, strong, nonnull) + NSMutableDictionary* remoteTracks; +@property(nonatomic, strong, nonnull) NSString* flutterId; +@property(nonatomic, strong, nullable) FlutterEventSink eventSink; +@property(nonatomic, strong, nullable) FlutterEventChannel* eventChannel; +@end + +@interface FlutterWebRTCPlugin (RTCPeerConnection) + +- (void)peerConnectionCreateOffer:(nonnull NSDictionary*)constraints + peerConnection:(nonnull RTCPeerConnection*)peerConnection + result:(nonnull FlutterResult)result; + +- (void)peerConnectionCreateAnswer:(nonnull NSDictionary*)constraints + peerConnection:(nonnull RTCPeerConnection*)peerConnection + result:(nonnull FlutterResult)result; + +- (void)peerConnectionSetLocalDescription:(nonnull RTCSessionDescription*)sdp + peerConnection:(nonnull RTCPeerConnection*)peerConnection + result:(nonnull FlutterResult)result; + +- (void)peerConnectionSetRemoteDescription:(nonnull RTCSessionDescription*)sdp + peerConnection:(nonnull RTCPeerConnection*)peerConnection + result:(nonnull FlutterResult)result; + +- (void)peerConnectionAddICECandidate:(nonnull RTCIceCandidate*)candidate + peerConnection:(nonnull RTCPeerConnection*)peerConnection + result:(nonnull FlutterResult)result; + +- (void)peerConnectionGetStats:(nonnull RTCPeerConnection*)peerConnection + result:(nonnull FlutterResult)result; + +- (void)peerConnectionGetStatsForTrackId:(nonnull NSString*)trackID + peerConnection:(nonnull RTCPeerConnection*)peerConnection + result:(nonnull FlutterResult)result; + +- (nonnull RTCMediaConstraints*)parseMediaConstraints:(nonnull NSDictionary*)constraints; + +- (void)peerConnectionSetConfiguration:(nonnull RTCConfiguration*)configuration + peerConnection:(nonnull RTCPeerConnection*)peerConnection; + +- (void)peerConnectionGetRtpReceiverCapabilities:(nonnull NSDictionary*)argsMap + result:(nonnull FlutterResult)result; + +- (void)peerConnectionGetRtpSenderCapabilities:(nonnull NSDictionary*)argsMap + result:(nonnull FlutterResult)result; + +- (void)transceiverSetCodecPreferences:(nonnull NSDictionary*)argsMap + result:(nonnull FlutterResult)result; + +- (nullable NSString*)stringForSignalingState:(RTCSignalingState)state; + +- (nullable NSString*)stringForICEGatheringState:(RTCIceGatheringState)state; + +- (nullable NSString*)stringForICEConnectionState:(RTCIceConnectionState)state; + +- (nullable NSString*)stringForPeerConnectionState:(RTCPeerConnectionState)state; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCVideoRenderer.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCVideoRenderer.h new file mode 100644 index 0000000000..94bcefccf2 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterRTCVideoRenderer.h @@ -0,0 +1,32 @@ +#import +#import +#import +#import +#import "FlutterWebRTCPlugin.h" + +@interface FlutterRTCVideoRenderer + : NSObject + +/** + * The {@link RTCVideoTrack}, if any, which this instance renders. + */ +@property(nonatomic, strong) RTCVideoTrack* videoTrack; +@property(nonatomic) int64_t textureId; +@property(nonatomic, weak) id registry; +@property(nonatomic, strong) FlutterEventSink eventSink; + +- (instancetype)initWithTextureRegistry:(id)registry + messenger:(NSObject*)messenger; + +- (void)dispose; + +@end + +@interface FlutterWebRTCPlugin (FlutterVideoRendererManager) + +- (FlutterRTCVideoRenderer*)createWithTextureRegistry:(id)registry + messenger:(NSObject*)messenger; + +- (void)rendererSetSrcObject:(FlutterRTCVideoRenderer*)renderer stream:(RTCVideoTrack*)videoTrack; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterWebRTCPlugin.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterWebRTCPlugin.h new file mode 100644 index 0000000000..97711aafbb --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/FlutterWebRTCPlugin.h @@ -0,0 +1,122 @@ +#import +#import +#import +#import "LocalTrack.h" + +@class VideoEffectProcessor; +@class FlutterRTCVideoRenderer; +@class FlutterRTCFrameCapturer; +@class AudioManager; +@class NativePeerConnectionFactory; + +void postEvent(FlutterEventSink _Nullable sink, id _Nullable event); + +typedef void (^CompletionHandler)(void); + +typedef void (^CapturerStopHandler)(CompletionHandler _Nonnull handler); + +@interface FlutterWebRTCPlugin : NSObject + +/** + * Per-call factory registry, keyed by factoryId. + */ +@property(nonatomic, strong, readonly, nonnull) + NSMutableDictionary* factories; + +/** + * Maps a peer connection id to the factoryId of the NativePeerConnectionFactory + * that built it. + */ +@property(nonatomic, strong, readonly, nonnull) + NSMutableDictionary* pcFactoryId; + +/** + * Maps a track id (audio or video) to the factoryId of the + * NativePeerConnectionFactory whose RTCPeerConnectionFactory created the + * underlying RTCMediaSource. + */ +@property(nonatomic, strong, readonly, nonnull) + NSMutableDictionary* trackFactoryId; + +@property(nonatomic, strong) + NSMutableDictionary* _Nullable peerConnections; +@property(nonatomic, strong) + NSMutableDictionary* _Nullable localStreams; +@property(nonatomic, strong) NSMutableDictionary>* _Nullable localTracks; +@property(nonatomic, strong) + NSMutableDictionary* _Nullable renders; +@property(nonatomic, strong) + NSMutableDictionary* _Nullable videoCapturerStopHandlers; +@property(nonatomic, strong) + NSMutableDictionary* _Nullable videoCaptureState; + +@property(nonatomic, strong) FlutterEventSink _Nullable eventSink; +@property(nonatomic, strong) NSObject* _Nonnull messenger; +@property(nonatomic, strong) RTCCameraVideoCapturer* _Nullable videoCapturer; +@property(nonatomic, strong) FlutterRTCFrameCapturer* _Nullable frameCapturer; +@property(nonatomic, strong) VideoEffectProcessor* _Nullable videoEffectProcessor; + +@property(nonatomic, strong) NSString* _Nonnull focusMode; +@property(nonatomic, strong) NSString* _Nonnull exposureMode; + +@property(nonatomic) BOOL _usingFrontCamera; +@property(nonatomic) NSInteger _lastTargetWidth; +@property(nonatomic) NSInteger _lastTargetHeight; +@property(nonatomic) NSInteger _lastTargetFps; + +@property(nonatomic, strong) AudioManager* _Nullable audioManager; + +@property(nonatomic, strong) NSMutableDictionary* _Nonnull trackVolumeCache; +@property(nonatomic, strong) NSMutableDictionary* _Nonnull pausedTrackVolumes; +@property(nonatomic) BOOL isAudioPlayoutPaused; + +- (void)mediaStreamTrackSetVideoEffects:(nonnull NSString*)trackId + names:(nonnull NSArray*)names; +- (RTCMediaStream* _Nullable)streamForId:(NSString* _Nonnull)streamId + peerConnectionId:(NSString* _Nullable)peerConnectionId; +- (RTCMediaStreamTrack* _Nullable)trackForId:(NSString* _Nonnull)trackId + peerConnectionId:(NSString* _Nullable)peerConnectionId; +- (NSString* _Nullable)audioTrackIdForVideoTrackId:(NSString* _Nonnull)videoTrackId; +- (RTCRtpTransceiver* _Nullable)getRtpTransceiverById:(RTCPeerConnection* _Nonnull)peerConnection + Id:(NSString* _Nullable)Id; +- (NSDictionary* _Nullable)mediaStreamToMap:(RTCMediaStream* _Nonnull)stream + ownerTag:(NSString* _Nullable)ownerTag; +- (NSDictionary* _Nullable)mediaTrackToMap:(RTCMediaStreamTrack* _Nonnull)track; +- (NSDictionary* _Nullable)receiverToMap:(RTCRtpReceiver* _Nonnull)receiver; +- (NSDictionary* _Nullable)transceiverToMap:(RTCRtpTransceiver* _Nonnull)transceiver; + +- (RTCMediaStreamTrack* _Nullable)remoteTrackForId:(NSString* _Nonnull)trackId; + +- (BOOL)hasLocalAudioTrack; +- (void)ensureAudioSession; +- (void)deactiveRtcAudioSession; + +- (RTCRtpReceiver* _Nullable)getRtpReceiverById:(RTCPeerConnection* _Nonnull)peerConnection + Id:(NSString* _Nonnull)Id; +- (RTCRtpSender* _Nullable)getRtpSenderById:(RTCPeerConnection* _Nonnull)peerConnection + Id:(NSString* _Nonnull)Id; + +- (void)postEventWithName:(NSString* _Nonnull)eventName data:(NSDictionary* _Nullable)data; + +#pragma mark - Per-call factory resolution + +/** + * Resolves a factoryId to a NativePeerConnectionFactory. + */ +- (NativePeerConnectionFactory* _Nullable)resolveFactoryForId:(NSString* _Nullable)factoryId; + +/** + * Resolves the NativePeerConnectionFactory that owns the local stream + * registered under {@code streamId}. Returns nil when no registered + * factory owns the stream. + */ +- (NativePeerConnectionFactory* _Nullable)resolveFactoryForStreamId:(NSString* _Nullable)streamId; + ++ (FlutterWebRTCPlugin* _Nullable)sharedSingleton; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/LocalAudioTrack.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/LocalAudioTrack.h new file mode 100644 index 0000000000..f7e110e4e7 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/LocalAudioTrack.h @@ -0,0 +1,19 @@ +#import +#import "AudioProcessingAdapter.h" +#import "LocalTrack.h" + +@interface LocalAudioTrack : NSObject + +- (_Nonnull instancetype)initWithTrack:(RTCAudioTrack* _Nonnull)track; + +@property(nonatomic, strong) RTCAudioTrack* _Nonnull audioTrack; + +- (void)addRenderer:(_Nonnull id)renderer; + +- (void)removeRenderer:(_Nonnull id)renderer; + +- (void)addProcessing:(_Nonnull id)processor; + +- (void)removeProcessing:(_Nonnull id)processor; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/LocalTrack.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/LocalTrack.h new file mode 100644 index 0000000000..34f2e1e29e --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/LocalTrack.h @@ -0,0 +1,7 @@ +#import + +@protocol LocalTrack + +- (RTCMediaStreamTrack*)track; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/LocalVideoTrack.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/LocalVideoTrack.h new file mode 100644 index 0000000000..e0acba6b45 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/LocalVideoTrack.h @@ -0,0 +1,24 @@ +#import +#import "LocalTrack.h" +#import "VideoProcessingAdapter.h" + +@interface LocalVideoTrack : NSObject + +- (_Nonnull instancetype)initWithTrack:(RTCVideoTrack* _Nonnull)track; + +- (_Nonnull instancetype)initWithTrack:(RTCVideoTrack* _Nonnull)track + videoProcessing:(VideoProcessingAdapter* _Nullable)processing; + +@property(nonatomic, strong) RTCVideoTrack* _Nonnull videoTrack; + +@property(nonatomic, strong) VideoProcessingAdapter* _Nonnull processing; + +- (void)addRenderer:(_Nonnull id)renderer; + +- (void)removeRenderer:(_Nonnull id)renderer; + +- (void)addProcessing:(_Nonnull id)processor; + +- (void)removeProcessing:(_Nonnull id)processor; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/NativePeerConnectionFactory.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/NativePeerConnectionFactory.h new file mode 100644 index 0000000000..d67b75476e --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/NativePeerConnectionFactory.h @@ -0,0 +1,68 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Encapsulates a per-call RTCPeerConnectionFactory together with the + * RTCAudioDeviceModule it owns. + */ +@interface NativePeerConnectionFactory : NSObject + +/** Id used by Dart-side callers to address this factory. */ +@property(nonatomic, copy, readonly) NSString* factoryId; + +/** The factory that builds peer connections + tracks. */ +@property(nonatomic, strong, readonly, nullable) RTCPeerConnectionFactory* factory; + +/** + * The factory's own ADM. Not owned by this class — the underlying factory owns it. + */ +@property(nonatomic, strong, readonly, nullable) RTCAudioDeviceModule* audioDeviceModule; + +/** Snapshot of the appleAudioConfiguration used to build this factory. */ +@property(nonatomic, copy, readonly, nullable) NSDictionary* audioConfigSnapshot; + +/** Whether voice-processing was bypassed at build time. */ +@property(nonatomic, assign, readonly) BOOL bypassVoiceProcessing; + +/** + * Peer-connection ids whose PC was created against this factory. + */ +@property(nonatomic, strong, readonly) NSMutableSet* ownedPcIds; + +/** + * Track ids created via this factory. + */ +@property(nonatomic, strong, readonly) NSMutableSet* ownedTrackIds; + +/** + * Stream ids created via this factory. + */ +@property(nonatomic, strong, readonly) NSMutableSet* ownedStreamIds; + +@property(nonatomic, assign, readonly) BOOL isDisposed; + +/** + * Snapshot of the ADM's playout/recording state captured at suspend time + * so resumeAudio can restore only what was active. Toggled by the + * suspendAudio/resumeAudio method-channel handlers in FlutterWebRTCPlugin. + */ +@property(nonatomic, assign) BOOL wasPlayingBeforeSuspend; +@property(nonatomic, assign) BOOL wasRecordingBeforeSuspend; + +- (instancetype)initWithFactoryId:(NSString*)factoryId + bypassVoiceProcessing:(BOOL)bypassVoiceProcessing + networkIgnoreMask:(NSArray*)networkIgnoreMask + audioProcessingModule:(RTCDefaultAudioProcessingModule*)apm + appleAudioConfiguration:(nullable NSDictionary*)appleAudioConfiguration + admObserver:(nullable id)admObserver; + +/** + * Releases the factory and its ADM. + */ +- (void)dispose; + +@end + +NS_ASSUME_NONNULL_END diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/ProcessorProvider.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/ProcessorProvider.h new file mode 100644 index 0000000000..702ccc5e87 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/ProcessorProvider.h @@ -0,0 +1,10 @@ +#import "VideoFrameProcessor.h" + +@interface ProcessorProvider : NSObject + ++ (NSObject *)getProcessor:(NSString *)name; ++ (void)addProcessor:(NSObject *)processor + forName:(NSString *)name; ++ (void)removeProcessor:(NSString *)name; + +@end \ No newline at end of file diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/RTCAudioSource+Private.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/RTCAudioSource+Private.h new file mode 100644 index 0000000000..755d9ca0c6 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/RTCAudioSource+Private.h @@ -0,0 +1,14 @@ +#ifdef __cplusplus +#import "StreamWebRTC/RTCAudioSource.h" +#include "media_stream_interface.h" + +@interface RTCAudioSource () + +/** + * The AudioSourceInterface object passed to this RTCAudioSource during + * construction. + */ +@property(nonatomic, readonly) rtc::scoped_refptr nativeAudioSource; + +@end +#endif diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/StreamMacAudioDevices.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/StreamMacAudioDevices.h new file mode 100644 index 0000000000..537a6af3fb --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/StreamMacAudioDevices.h @@ -0,0 +1,23 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Core Audio HAL helpers used to enumerate macOS input / output devices and + * change the system-default device + */ +@interface StreamMacAudioDevices : NSObject + +/// Returns all currently-attached input devices (mics, line-ins, virtual). ++ (NSArray*>*)inputDevices; + +/// Returns all currently-attached output devices (speakers, headphones, virtual). ++ (NSArray*>*)outputDevices; + +/// Looks up the deviceId-strings as returned by [inputDevices] / +/// [outputDevices] and returns YES if a matching device exists. ++ (BOOL)deviceIdExists:(NSString*)deviceId asInput:(BOOL)input; + +@end + +NS_ASSUME_NONNULL_END diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoEffectProcessor.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoEffectProcessor.h new file mode 100644 index 0000000000..6d8dcdb0ac --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoEffectProcessor.h @@ -0,0 +1,13 @@ +#import +#import "VideoFrameProcessor.h" + +@interface VideoEffectProcessor : NSObject + +@property(nonatomic, strong) NSArray*>* videoFrameProcessors; +@property(nonatomic, strong) RTCVideoSource* videoSource; + +- (instancetype)initWithProcessors: + (NSArray*>*)videoFrameProcessors + videoSource:(RTCVideoSource*)videoSource; + +@end \ No newline at end of file diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoFactoriesPrivate.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoFactoriesPrivate.h new file mode 100644 index 0000000000..797a88d211 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoFactoriesPrivate.h @@ -0,0 +1,15 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface VideoEncoderFactory : RTCDefaultVideoEncoderFactory +@end + +@interface VideoDecoderFactory : RTCDefaultVideoDecoderFactory +@end + +@interface VideoEncoderFactorySimulcast : RTCVideoEncoderFactorySimulcast +@end + +NS_ASSUME_NONNULL_END diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoFrameProcessor.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoFrameProcessor.h new file mode 100644 index 0000000000..c988f34333 --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoFrameProcessor.h @@ -0,0 +1,7 @@ + +#import +#import + +@protocol VideoFrameProcessorDelegate +- (RTCVideoFrame*)capturer:(RTCVideoCapturer*)capturer didCaptureVideoFrame:(RTCVideoFrame*)frame; +@end \ No newline at end of file diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoProcessingAdapter.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoProcessingAdapter.h new file mode 100644 index 0000000000..9fba86edec --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/VideoProcessingAdapter.h @@ -0,0 +1,18 @@ +#import +#import + +@protocol ExternalVideoProcessingDelegate +- (RTC_OBJC_TYPE(RTCVideoFrame) * _Nonnull)onFrame:(RTC_OBJC_TYPE(RTCVideoFrame) * _Nonnull)frame; +@end + +@interface VideoProcessingAdapter : NSObject + +- (_Nonnull instancetype)initWithRTCVideoSource:(RTCVideoSource* _Nonnull)source; + +- (void)addProcessing:(_Nonnull id)processor; + +- (void)removeProcessing:(_Nonnull id)processor; + +- (RTCVideoSource* _Nonnull)source; + +@end diff --git a/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/media_stream_interface.h b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/media_stream_interface.h new file mode 100644 index 0000000000..e25553f9fa --- /dev/null +++ b/macos/stream_webrtc_flutter/Sources/stream_webrtc_flutter/include/stream_webrtc_flutter/media_stream_interface.h @@ -0,0 +1,199 @@ +/* + * Copyright 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/// Source https://webrtc.googlesource.com/src/+/master/api/media_stream_interface.h + +#ifdef __cplusplus +#ifndef API_MEDIA_STREAM_INTERFACE_H_ +#define API_MEDIA_STREAM_INTERFACE_H_ + +#include +#include +#include +#include +#include + +namespace webrtc { + + // Generic observer interface. + class ObserverInterface { + public: + virtual void OnChanged() = 0; + protected: + virtual ~ObserverInterface() {} + }; + class NotifierInterface { + public: + virtual void RegisterObserver(ObserverInterface* observer) = 0; + virtual void UnregisterObserver(ObserverInterface* observer) = 0; + virtual ~NotifierInterface() {} + }; + + enum class RefCountReleaseStatus { kDroppedLastRef, kOtherRefsRemained }; + // Interfaces where refcounting is part of the public api should + // inherit this abstract interface. The implementation of these + // methods is usually provided by the RefCountedObject template class, + // applied as a leaf in the inheritance tree. + class RefCountInterface { + public: + virtual void AddRef() const = 0; + virtual RefCountReleaseStatus Release() const = 0; + // Non-public destructor, because Release() has exclusive responsibility for + // destroying the object. + protected: + virtual ~RefCountInterface() {} + }; + + // Base class for sources. A MediaStreamTrack has an underlying source that + // provides media. A source can be shared by multiple tracks. + class MediaSourceInterface : public RefCountInterface, + public NotifierInterface { + public: + enum SourceState { kInitializing, kLive, kEnded, kMuted }; + virtual SourceState state() const = 0; + virtual bool remote() const = 0; + protected: + ~MediaSourceInterface() override = default; + }; + + // Interface for receiving audio data from a AudioTrack. + class AudioTrackSinkInterface { + public: + virtual void OnData(const void* audio_data, + int bits_per_sample, + int sample_rate, + size_t number_of_channels, + size_t number_of_frames) { + + }; + virtual void OnData(const void* audio_data, + int bits_per_sample, + int sample_rate, + size_t number_of_channels, + size_t number_of_frames, + void* absolute_capture_timestamp_ms) { + // TODO(bugs.webrtc.org/10739): Deprecate the old OnData and make this one + // pure virtual. + return OnData(audio_data, bits_per_sample, sample_rate, number_of_channels, + number_of_frames); + } + virtual int NumPreferredChannels() const { return -1; } + protected: + virtual ~AudioTrackSinkInterface() {} + }; + // AudioSourceInterface is a reference counted source used for AudioTracks. + // The same source can be used by multiple AudioTracks. + class AudioSourceInterface : public MediaSourceInterface { + public: + class AudioObserver { + public: + virtual void OnSetVolume(double volume) = 0; + protected: + virtual ~AudioObserver() {} + }; + // TODO(deadbeef): Makes all the interfaces pure virtual after they're + // implemented in chromium. + // Sets the volume of the source. |volume| is in the range of [0, 10]. + // TODO(tommi): This method should be on the track and ideally volume should + // be applied in the track in a way that does not affect clones of the track. + virtual void SetVolume(double volume) {} + // Registers/unregisters observers to the audio source. + virtual void RegisterAudioObserver(AudioObserver* observer) {} + virtual void UnregisterAudioObserver(AudioObserver* observer) {} + // TODO(tommi): Make pure virtual. + virtual void AddSink(AudioTrackSinkInterface* sink) {} + virtual void RemoveSink(AudioTrackSinkInterface* sink) {} + // Returns options for the AudioSource. + // (for some of the settings this approach is broken, e.g. setting + // audio network adaptation on the source is the wrong layer of abstraction). +// virtual const AudioOptions options() const; + }; +} +namespace rtc { + + template + class scoped_refptr { + public: + typedef T element_type; + scoped_refptr() : ptr_(nullptr) {} + scoped_refptr(std::nullptr_t) : ptr_(nullptr) {} // NOLINT(runtime/explicit) + explicit scoped_refptr(T* p) : ptr_(p) { + if (ptr_) + ptr_->AddRef(); + } + scoped_refptr(const scoped_refptr& r) : ptr_(r.ptr_) { + if (ptr_) + ptr_->AddRef(); + } + template + scoped_refptr(const scoped_refptr& r) : ptr_(r.get()) { + if (ptr_) + ptr_->AddRef(); + } + // Move constructors. + scoped_refptr(scoped_refptr&& r) noexcept : ptr_(r.release()) {} + template + scoped_refptr(scoped_refptr&& r) noexcept : ptr_(r.release()) {} + ~scoped_refptr() { + if (ptr_) + ptr_->Release(); + } + T* get() const { return ptr_; } + explicit operator bool() const { return ptr_ != nullptr; } + T& operator*() const { return *ptr_; } + T* operator->() const { return ptr_; } + // Returns the (possibly null) raw pointer, and makes the scoped_refptr hold a + // null pointer, all without touching the reference count of the underlying + // pointed-to object. The object is still reference counted, and the caller of + // release() is now the proud owner of one reference, so it is responsible for + // calling Release() once on the object when no longer using it. + T* release() { + T* retVal = ptr_; + ptr_ = nullptr; + return retVal; + } + scoped_refptr& operator=(T* p) { + // AddRef first so that self assignment should work + if (p) + p->AddRef(); + if (ptr_) + ptr_->Release(); + ptr_ = p; + return *this; + } + scoped_refptr& operator=(const scoped_refptr& r) { + return *this = r.ptr_; + } + template + scoped_refptr& operator=(const scoped_refptr& r) { + return *this = r.get(); + } + scoped_refptr& operator=(scoped_refptr&& r) noexcept { + scoped_refptr(std::move(r)).swap(*this); + return *this; + } + template + scoped_refptr& operator=(scoped_refptr&& r) noexcept { + scoped_refptr(std::move(r)).swap(*this); + return *this; + } + void swap(T** pp) noexcept { + T* p = ptr_; + ptr_ = *pp; + *pp = p; + } + void swap(scoped_refptr& r) noexcept { swap(&r.ptr_); } + protected: + T* ptr_; + }; +}; + +#endif // API_MEDIA_STREAM_INTERFACE_H_ +#endif // __cplusplus