├── WKWebViewRTC ├── Assets │ └── .gitkeep ├── Classes │ ├── .gitkeep │ ├── iUtils.swift │ ├── WkWebviewCommand.swift │ ├── WkWebviewCmdResult.swift │ ├── iRTCTypes.swift │ ├── iRTCDTMFSender.swift │ ├── iMediaStreamTrack.swift │ ├── iRTCPeerConnectionConstraints.swift │ ├── iEnumerateDevices.swift │ ├── iMediaStream.swift │ ├── iGetUserMedia.swift │ ├── iRTCAudioController.swift │ ├── iRTCPeerConnectionConfig.swift │ ├── iRTCDataChannel.swift │ └── iMediaStreamRenderer.swift └── Js │ ├── .gitignore │ ├── src │ ├── RTCRtpReceiver.js │ ├── MediaTrackSettings.js │ ├── MediaTrackCapabilities.js │ ├── RTCSessionDescription.js │ ├── RTCStatsReport.js │ ├── RTCStatsResponse.js │ ├── RTCRtpTransceiver.js │ ├── MediaDeviceInfo.js │ ├── EventTarget.js │ ├── enumerateDevices.js │ ├── Errors.js │ ├── RTCRtpSender.js │ ├── IOSExec.js │ ├── MediaDevices.js │ ├── RTCDTMFSender.js │ ├── MediaStreamTrack.js │ ├── RTCIceCandidate.js │ ├── RTCDataChannel.js │ ├── iosrtc.js │ ├── MediaStreamRenderer.js │ ├── videoElementsHandler.js │ ├── MediaStream.js │ └── getUserMedia.js │ └── package.json ├── _Pods.xcodeproj ├── Example ├── Podfile ├── WKWebViewRTC.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── WKWebViewRTC-Example.xcscheme ├── WKWebViewRTC.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── WKWebViewRTC │ ├── ViewController.swift │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── AppDelegate.swift │ └── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.xib └── WKWebViewRTC_ExampleTests │ ├── Info.plist │ └── WKWebViewRTC_ExampleTests.swift ├── .travis.yml ├── .gitignore ├── LICENSE ├── WKWebViewRTC.podspec └── README.md /WKWebViewRTC/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /WKWebViewRTC/Js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | target 'WKWebViewRTC_Example' do 4 | pod 'WKWebViewRTC', :path => '../' 5 | 6 | end 7 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iUtils.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | 12 | class iUtils { 13 | class func randomInt(_ min: Int, max: Int) -> Int { 14 | return Int(arc4random_uniform(UInt32(max - min))) + min 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode11 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/WKWebViewRTC.xcworkspace -scheme WKWebViewRTC-Example -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint --allow-warnings 15 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/RTCRtpReceiver.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the RTCRtpReceiver class. 12 | */ 13 | module.exports = RTCRtpReceiver; 14 | 15 | 16 | function RTCRtpReceiver(data) { 17 | data = data || {}; 18 | 19 | this._pc = data.pc; 20 | this.track = data.track; 21 | } 22 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/MediaTrackSettings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the MediaTrackSettings class. 12 | */ 13 | module.exports = MediaTrackSettings; 14 | 15 | // Ref https://www.w3.org/TR/mediacapture-streams/#dom-mediatracksettings 16 | function MediaTrackSettings(data) { 17 | data = data || {}; 18 | } 19 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/MediaTrackCapabilities.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the MediaTrackSettings class. 12 | */ 13 | module.exports = MediaTrackCapabilities; 14 | 15 | // Ref https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities 16 | function MediaTrackCapabilities(data) { 17 | data = data || {}; 18 | } 19 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/RTCSessionDescription.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the RTCSessionDescription class. 12 | */ 13 | module.exports = RTCSessionDescription; 14 | 15 | 16 | function RTCSessionDescription(data) { 17 | data = data || {}; 18 | 19 | // Public atributes. 20 | this.type = data.type; 21 | this.sdp = data.sdp; 22 | } 23 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wkwebview-rtc", 3 | "version": "0.0.1", 4 | "description": "WKWebViewRTC", 5 | "author": "", 6 | "contributors": [], 7 | "license": "MIT", 8 | "keywords": [ 9 | "webrtc", 10 | "ios", 11 | "wkwebview" 12 | ], 13 | "homepage": "", 14 | "repository": { 15 | "type": "git", 16 | "url": "" 17 | }, 18 | "scripts": { 19 | "build": "browserify src/iosrtc.js -o jsWKWebViewRTC.js -s jsWKWebViewRTC" 20 | }, 21 | "dependencies": { 22 | "debug": "^4.1.1", 23 | "domready": "^1.0.8", 24 | "random-number": "^0.0.7", 25 | "yaeti": "^1.0.2" 26 | }, 27 | "devDependencies": { 28 | "browserify": "^14.4.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // WKWebViewRTC 4 | // 5 | // Created by JustDoIt9 on 07/28/2020. 6 | // Copyright (c) 2020 JustDoIt9. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebKit 11 | import WKWebViewRTC 12 | 13 | class ViewController: UIViewController { 14 | 15 | @IBOutlet weak var webView: WKWebView! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | // Do any additional setup after loading the view, typically from a nib. 21 | WKWebViewRTC(wkwebview: webView, contentController: webView.configuration.userContentController) 22 | 23 | webView.load(URLRequest(url: URL(string: "https://cordova-rtc.github.io/cordova-plugin-iosrtc-sample/index.html")!)) 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC_ExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/RTCStatsReport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the RTCStatsReport class. 12 | */ 13 | module.exports = RTCStatsReport; 14 | 15 | function RTCStatsReport(data) { 16 | data = data || {}; 17 | 18 | this.id = data.reportId; 19 | this.timestamp = data.timestamp; 20 | this.type = data.type; 21 | 22 | this.names = function () { 23 | return Object.keys(data.values); 24 | }; 25 | 26 | this.stat = function (key) { 27 | return data.values[key] || ''; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 26 | # Carthage/Checkouts 27 | 28 | Carthage/Build 29 | 30 | # We recommend against adding the Pods directory to your .gitignore. However 31 | # you should judge for yourself, the pros and cons are mentioned at: 32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 33 | # 34 | # Note: if you ignore the Pods directory, make sure to uncomment 35 | # `pod install` in .travis.yml 36 | # 37 | Pods/ 38 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/RTCStatsResponse.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the RTCStatsResponse class. 12 | */ 13 | module.exports = RTCStatsResponse; 14 | 15 | function RTCStatsResponse(data) { 16 | data = data || []; 17 | 18 | this.result = function () { 19 | return data; 20 | }; 21 | 22 | this.forEach = function (callback, thisArg) { 23 | return data.forEach(callback, thisArg); 24 | }; 25 | 26 | this.namedItem = function () { 27 | return null; 28 | }; 29 | 30 | this.values = function () { 31 | return data; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/RTCRtpTransceiver.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the RTCRtpTransceiver class. 12 | */ 13 | module.exports = RTCRtpTransceiver; 14 | 15 | 16 | function RTCRtpTransceiver(data) { 17 | data = data || {}; 18 | 19 | this.receiver = data.receiver; 20 | this.sender = data.sender; 21 | } 22 | 23 | // TODO 24 | // https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/currentDirection 25 | // https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiverDirection 26 | // https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/mid 27 | // https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/stop 28 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/MediaDeviceInfo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the MediaDeviceInfo class. 12 | */ 13 | module.exports = MediaDeviceInfo; 14 | 15 | function MediaDeviceInfo(data) { 16 | data = data || {}; 17 | 18 | Object.defineProperties(this, { 19 | // MediaDeviceInfo spec. 20 | deviceId: { 21 | value: data.deviceId 22 | }, 23 | kind: { 24 | value: data.kind 25 | }, 26 | label: { 27 | value: data.label 28 | }, 29 | groupId: { 30 | value: data.groupId || '' 31 | }, 32 | // SourceInfo old spec. 33 | id: { 34 | value: data.deviceId 35 | }, 36 | // Deprecated, but useful until there is an alternative 37 | facing: { 38 | value: '' 39 | } 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Open Telecom Foundation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC_ExampleTests/WKWebViewRTC_ExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WKWebViewRTC_ExampleTests.swift 3 | // WKWebViewRTC_ExampleTests 4 | // 5 | // Created by Harold Thétiot on 21/05/2021. 6 | // Copyright © 2021 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class WKWebViewRTC_ExampleTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/EventTarget.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Dependencies. 12 | */ 13 | var 14 | YaetiEventTarget = require('yaeti').EventTarget; 15 | 16 | var EventTarget = function () { 17 | YaetiEventTarget.call(this); 18 | }; 19 | 20 | EventTarget.prototype = Object.create(YaetiEventTarget.prototype); 21 | EventTarget.prototype.constructor = EventTarget; 22 | 23 | Object.defineProperties(EventTarget.prototype, Object.getOwnPropertyDescriptors(YaetiEventTarget.prototype)); 24 | 25 | EventTarget.prototype.dispatchEvent = function (event) { 26 | 27 | Object.defineProperty(event, 'target', { 28 | value: this, 29 | writable: false 30 | }); 31 | 32 | YaetiEventTarget.prototype.dispatchEvent.call(this, event); 33 | }; 34 | 35 | /** 36 | * Expose the EventTarget class. 37 | */ 38 | module.exports = EventTarget; 39 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/WkWebviewCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WkWebviewCommand.swift 3 | // WKWebViewRTC 4 | // 5 | // Created by Open Telecom Foundation on 2020/6/30. 6 | // Copyright © 2020 Open Telecom Foundation. All rights reserved. 7 | // The MIT License (MIT) 8 | // 9 | 10 | import Foundation 11 | 12 | class WkWebviewCommand : NSObject { 13 | var callbackId : String = "" 14 | var className : String = "" 15 | var methodName : String = "" 16 | var arguments : [Any] = [] 17 | 18 | init(commandFromJson:[Any]) 19 | { 20 | super.init() 21 | let count = commandFromJson.count 22 | if count < 4 { 23 | print("argument is less") 24 | return 25 | } 26 | 27 | callbackId = commandFromJson[0] as! String 28 | className = commandFromJson[1] as! String 29 | methodName = commandFromJson[2] as! String 30 | arguments = commandFromJson[3] as! [Any] 31 | 32 | } 33 | 34 | func argument(at :UInt) -> Any?{ 35 | let count = arguments.count 36 | if count > at { 37 | let arg = arguments[Int(at)] 38 | if arg is NSNull 39 | { 40 | return nil 41 | } 42 | else { 43 | return arguments[Int(at)] 44 | } 45 | } 46 | return nil 47 | 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/enumerateDevices.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the enumerateDevices function. 12 | */ 13 | module.exports = enumerateDevices; 14 | 15 | 16 | /** 17 | * Dependencies. 18 | */ 19 | var 20 | debug = require('debug')('iosrtc:enumerateDevices'), 21 | exec = require('./IOSExec'), 22 | MediaDeviceInfo = require('./MediaDeviceInfo'), 23 | Errors = require('./Errors'); 24 | 25 | 26 | function enumerateDevices() { 27 | 28 | return new Promise(function (resolve) { 29 | function onResultOK(data) { 30 | debug('enumerateDevices() | success'); 31 | resolve(getMediaDeviceInfos(data.devices)); 32 | } 33 | 34 | exec.execNative(onResultOK, null, 'WKWebViewRTC', 'enumerateDevices', []); 35 | }); 36 | } 37 | 38 | 39 | /** 40 | * Private API. 41 | */ 42 | 43 | 44 | function getMediaDeviceInfos(devices) { 45 | debug('getMediaDeviceInfos() | [devices:%o]', devices); 46 | 47 | var id, 48 | mediaDeviceInfos = []; 49 | 50 | for (id in devices) { 51 | if (devices.hasOwnProperty(id)) { 52 | mediaDeviceInfos.push(new MediaDeviceInfo(devices[id])); 53 | } 54 | } 55 | 56 | return mediaDeviceInfos; 57 | } 58 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSMicrophoneUsageDescription 6 | WKWebViewRTC requires acess to mic. 7 | NSCameraUsageDescription 8 | WKWebViewRTC requires acess to cam. 9 | CFBundleDevelopmentRegion 10 | en 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 1 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/Errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose an object with WebRTC Errors. 12 | */ 13 | var Errors = module.exports = {}, 14 | 15 | 16 | /** 17 | * Local variables. 18 | */ 19 | IntermediateInheritor = function () {}; 20 | 21 | 22 | IntermediateInheritor.prototype = Error.prototype; 23 | 24 | 25 | /** 26 | * Create error classes. 27 | */ 28 | addError('InvalidStateError'); 29 | addError('InvalidSessionDescriptionError'); 30 | addError('InternalError'); 31 | addError('MediaStreamError'); 32 | 33 | 34 | function addError(name) { 35 | Errors[name] = function () { 36 | var tmp = Error.apply(this, arguments); 37 | 38 | this.name = tmp.name = name; 39 | this.message = tmp.message; 40 | 41 | Object.defineProperty(this, 'stack', { 42 | get: function () { 43 | return tmp.stack; 44 | } 45 | }); 46 | 47 | return this; 48 | }; 49 | 50 | Errors[name].prototype = new IntermediateInheritor(); 51 | } 52 | 53 | // Detect callback usage to assist 5.0.1 to 5.0.2 migration 54 | // TODO remove on 6.0.0 55 | Errors.detectDeprecatedCallbaksUsage = function detectDeprecatedCallbaksUsage(funcName, arg) { 56 | if ( 57 | typeof arg[1] === 'function' || 58 | typeof arg[2] === 'function' 59 | ) { 60 | throw new Error('Callbacks are not supported by "' + funcName + '" anymore, use Promise instead.'); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /WKWebViewRTC.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint WKWebViewRTC.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'WKWebViewRTC' 11 | s.version = '0.5.1' 12 | s.summary = 'WebRTC library for WKWebView for Swift on iOS' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | WebRTC library for WKWebView for Swift on iOS (based on cordova-plugin-iosrtc: https://github.com/cordova-rtc/cordova-plugin-iosrtc) 22 | DESC 23 | 24 | s.homepage = 'https://github.com/OpenTelecom/WKWebViewRTC' 25 | s.license = { :type => 'MIT', :file => 'LICENSE' } 26 | s.author = { 'OpenTelecom' => 'contact@OpenTele.com' } 27 | s.source = { :git => 'https://github.com/OpenTelecom/WKWebViewRTC.git', :tag => s.version.to_s } 28 | 29 | s.swift_version = '4.2' 30 | s.ios.deployment_target = '11.0' 31 | 32 | s.source_files = 'WKWebViewRTC/Classes/**/*' 33 | s.resources = 'WKWebViewRTC/Js/jsWKWebViewRTC.js' 34 | 35 | s.dependency 'GoogleWebRTC', '1.1.29229' 36 | 37 | s.xcconfig = { 'ENABLE_BITCODE' => 'NO', 'ONLY_ACTIVE_ARCH' => 'Yes' } 38 | end 39 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/WkWebviewCmdResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WkWebviewCmdResult.swift 3 | // WKWebViewRTC 4 | // 5 | // Created by Open Telecom Foundation on 2020/6/30. 6 | // Copyright © 2020 Open Telecom Foundation. All rights reserved. 7 | // The MIT License (MIT) 8 | // 9 | 10 | import Foundation 11 | 12 | enum WkWebviewCmdStatus : UInt { 13 | case WkWebviewCmdStatus_NO_RESULT = 0 14 | case WkWebviewCmdStatus_OK = 1 15 | case WkWebviewCmdStatus_ERROR = 2 16 | } 17 | 18 | class WkWebviewCmdResult { 19 | var status : WkWebviewCmdStatus 20 | var message : Any? 21 | init(status: WkWebviewCmdStatus) { 22 | self.status = status 23 | message = nil 24 | } 25 | init(status: WkWebviewCmdStatus, messageAs : Any?) { 26 | self.status = status 27 | message = messageAs 28 | } 29 | init(status: WkWebviewCmdStatus, messageAsArrayBuffer : Data) { 30 | self.status = status 31 | message = messageFromArrayBuffer(data: messageAsArrayBuffer) 32 | 33 | } 34 | func messageFromArrayBuffer(data: Data) -> Any 35 | { 36 | return ["Type" : "ArrayBuffer", 37 | "Data" : data.base64EncodedString()] 38 | } 39 | func argumentAsJSON() -> String 40 | { 41 | var argInArray : NSArray 42 | if let arg = self.message { 43 | argInArray = NSArray(objects: arg) 44 | } 45 | else{ 46 | argInArray = NSArray(objects: NSNull()) 47 | } 48 | if let data = try? JSONSerialization.data(withJSONObject: argInArray, options: []) { 49 | guard let argJSON = String(data: data, encoding: .utf8) else {return ""} 50 | return argJSON 51 | } 52 | return "" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/RTCRtpSender.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the RTCRtpSender class. 12 | */ 13 | module.exports = RTCRtpSender; 14 | 15 | function RTCRtpSender(data) { 16 | data = data || {}; 17 | 18 | this._pc = data.pc; 19 | this.track = data.track; 20 | this.params = data.params || {}; 21 | } 22 | 23 | RTCRtpSender.prototype.getParameters = function () { 24 | return this.params; 25 | }; 26 | 27 | RTCRtpSender.prototype.setParameters = function (params) { 28 | Object.assign(this.params, params); 29 | return Promise.resolve(this.params); 30 | }; 31 | 32 | RTCRtpSender.prototype.replaceTrack = function (withTrack) { 33 | var self = this, 34 | pc = self._pc; 35 | 36 | return new Promise(function (resolve, reject) { 37 | pc.removeTrack(self); 38 | pc.addTrack(withTrack); 39 | self.track = withTrack; 40 | 41 | // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/negotiationneeded_event 42 | var event = new Event('negotiationneeded'); 43 | pc.dispatchEvent('negotiationneeded', event); 44 | 45 | pc.addEventListener("signalingstatechange", function listener() { 46 | if (pc.signalingState === "closed") { 47 | pc.removeEventListener("signalingstatechange", listener); 48 | reject(); 49 | } else if (pc.signalingState === "stable") { 50 | pc.removeEventListener("signalingstatechange", listener); 51 | resolve(); 52 | } 53 | }); 54 | }); 55 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WKWebViewRTC 2 | 3 | [![CI Status](https://img.shields.io/travis/JustDoIt9/WKWebViewRTC.svg?style=flat)](https://travis-ci.org/JustDoIt9/WKWebViewRTC) 4 | [![Version](https://img.shields.io/cocoapods/v/WKWebViewRTC.svg?style=flat)](https://cocoapods.org/pods/WKWebViewRTC) 5 | [![License](https://img.shields.io/cocoapods/l/WKWebViewRTC.svg?style=flat)](https://cocoapods.org/pods/WKWebViewRTC) 6 | [![Platform](https://img.shields.io/cocoapods/p/WKWebViewRTC.svg?style=flat)](https://cocoapods.org/pods/WKWebViewRTC) 7 | 8 | WebRTC library for WKWebView for Swift on iOS (based on cordova-plugin-iosrtc: https://github.com/cordova-rtc/cordova-plugin-iosrtc) 9 | 10 | ## Example 11 | 12 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 13 | 14 | ## Requirements 15 | 16 | In order to make this framework run into a iOS application some requirements must be satisfied in both development computer and target devices: 17 | 18 | * Xcode >= 11.1 (11A1027) 19 | * iOS >= 10.2 (run on lower versions at your own risk, don't report issues) 20 | * `swift-version` => 4.2 21 | 22 | ## Installation 23 | 24 | WKWebViewRTC is available through [CocoaPods](https://cocoapods.org). To install 25 | it, simply add the following line to your Podfile: 26 | 27 | ```ruby 28 | pod 'WKWebViewRTC' 29 | ``` 30 | 31 | ## Dependencies 32 | * [GoogleWebRTC](https://cocoapods.org/pods/GoogleWebRTC) => v1.1.29229 33 | 34 | ## Build JS (WKWebViewRTC/Js/jsWKWebViewRTC.js) 35 | 36 | * From the WKWebViewRTC/Js run `npm i` and then `npm run-script build` 37 | 38 | ### Third-Party Supported Library 39 | 40 | * Janus => 0.7.4 41 | * JSSip => 3.1.2 42 | * Sip.js => 0.15.6 43 | 44 | ## Author 45 | 46 | Open Telecom Foundation 47 | 48 | ## License 49 | 50 | WKWebViewRTC is available under the MIT license. See the LICENSE file for more info. 51 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/IOSExec.js: -------------------------------------------------------------------------------- 1 | // 2 | // IOSExec.js 3 | // WKWebViewRTC 4 | // 5 | // Created by Open Telecom Foundation on 2020/6/30. 6 | // Copyright © 2020 Open Telecom Foundation. All rights reserved. 7 | // The MIT License (MIT) 8 | // 9 | 10 | module.exports = execNative; 11 | module.exports.execNative = execNative; 12 | module.exports.nativeCallback = nativeCallback; 13 | 14 | var exec_queue = { 15 | callbackId : Math.floor(Math.random() * 2000000), 16 | callbacks: {} 17 | }; 18 | 19 | function execNative(successCallback, failCallback, service, action, actionArgs) 20 | { 21 | if (window.webkit && window.webkit.messageHandlers) 22 | { 23 | if (window.webkit.messageHandlers[service]) 24 | { 25 | var callbackId = service + exec_queue.callbackId++; 26 | if (successCallback || failCallback) 27 | { 28 | exec_queue.callbacks[callbackId] = {success: successCallback, fail:failCallback}; 29 | } 30 | command = [callbackId, service, action, actionArgs] 31 | window.webkit.messageHandlers[service].postMessage(JSON.stringify(command)) 32 | } 33 | } 34 | } 35 | 36 | function nativeCallback(callbackId, status, argumentsAsJson) 37 | { 38 | try { 39 | var callback = exec_queue.callbacks[callbackId]; 40 | if (callback) 41 | { 42 | if (status == 1 && callback.success) 43 | { 44 | callback.success.apply(null, argumentsAsJson); 45 | } 46 | else if (status == 0 && callback.fail) 47 | { 48 | callback.fail.apply(null, argumentsAsJson); 49 | } 50 | //delete exec_queue.callbacks[callbackId]; 51 | } 52 | } 53 | catch (err){ 54 | var msg = "Error in callbackId " + callbackId + " : " + err; 55 | console.log(msg) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iRTCTypes.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import WebRTC 11 | struct iRTCTypes { 12 | static let signalingStates = [ 13 | RTCSignalingState.stable.rawValue: "stable", 14 | RTCSignalingState.haveLocalOffer.rawValue: "have-local-offer", 15 | RTCSignalingState.haveLocalPrAnswer.rawValue: "have-local-pranswer", 16 | RTCSignalingState.haveRemoteOffer.rawValue: "have-remote-offer", 17 | RTCSignalingState.haveRemotePrAnswer.rawValue: "have-remote-pranswer", 18 | RTCSignalingState.closed.rawValue: "closed" 19 | ] 20 | 21 | static let iceGatheringStates = [ 22 | RTCIceGatheringState.new.rawValue: "new", 23 | RTCIceGatheringState.gathering.rawValue: "gathering", 24 | RTCIceGatheringState.complete.rawValue: "complete" 25 | ] 26 | 27 | static let iceConnectionStates = [ 28 | RTCIceConnectionState.new.rawValue: "new", 29 | RTCIceConnectionState.checking.rawValue: "checking", 30 | RTCIceConnectionState.connected.rawValue: "connected", 31 | RTCIceConnectionState.completed.rawValue: "completed", 32 | RTCIceConnectionState.failed.rawValue: "failed", 33 | RTCIceConnectionState.disconnected.rawValue: "disconnected", 34 | RTCIceConnectionState.closed.rawValue: "closed" 35 | ] 36 | 37 | static let dataChannelStates = [ 38 | RTCDataChannelState.connecting.rawValue: "connecting", 39 | RTCDataChannelState.open.rawValue: "open", 40 | RTCDataChannelState.closing.rawValue: "closing", 41 | RTCDataChannelState.closed.rawValue: "closed" 42 | ] 43 | 44 | static let mediaStreamTrackStates = [ 45 | RTCMediaStreamTrackState.live.rawValue: "live", 46 | RTCMediaStreamTrackState.ended.rawValue: "ended" 47 | ] 48 | 49 | } 50 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iRTCDTMFSender.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | import WebRTC 12 | 13 | class iRTCDTMFSender : NSObject { 14 | var rtcRtpSender: RTCRtpSender? 15 | var eventListener: ((_ data: NSDictionary) -> Void)? 16 | 17 | /** 18 | * Constructor for pc.createDTMFSender(). 19 | */ 20 | init( 21 | rtcPeerConnection: RTCPeerConnection, 22 | track: RTCMediaStreamTrack, 23 | streamId: String, 24 | eventListener: @escaping (_ data: NSDictionary) -> Void 25 | ) { 26 | NSLog("iRTCDTMFSender#init()") 27 | 28 | self.eventListener = eventListener 29 | 30 | // TODO check if new rtcRtpSender can be used one Unified-Plan merged 31 | //let streamIds = [streamId] 32 | //self.rtcRtpSender = rtcPeerConnection.add(track, streamIds: streamIds); 33 | self.rtcRtpSender = rtcPeerConnection.senders[0] 34 | 35 | if self.rtcRtpSender == nil { 36 | NSLog("iRTCDTMFSender#init() | rtcPeerConnection.createDTMFSenderForTrack() failed") 37 | return 38 | } 39 | } 40 | 41 | deinit { 42 | NSLog("iRTCDTMFSender#deinit()") 43 | } 44 | 45 | func run() { 46 | NSLog("iRTCDTMFSender#run()") 47 | } 48 | 49 | func insertDTMF(_ tones: String, duration: TimeInterval, interToneGap: TimeInterval) { 50 | NSLog("iRTCDTMFSender#insertDTMF()") 51 | 52 | let dtmfSender = self.rtcRtpSender?.dtmfSender 53 | let durationMs = duration / 100 54 | let interToneGapMs = interToneGap / 100 55 | let result = dtmfSender!.insertDtmf(tones, duration: durationMs, interToneGap: interToneGapMs) 56 | if !result { 57 | NSLog("iRTCDTMFSender#indertDTMF() | RTCDTMFSender#indertDTMF() failed") 58 | } 59 | } 60 | 61 | /** 62 | * Methods inherited from RTCDTMFSenderDelegate. 63 | */ 64 | func toneChange(_ tone: String) { 65 | NSLog("iRTCDTMFSender | tone change [tone:%@]", tone) 66 | 67 | if self.eventListener != nil { 68 | self.eventListener!([ 69 | "type": "tonechange", 70 | "tone": tone 71 | ]) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/MediaDevices.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the MediaDevices class. 12 | */ 13 | module.exports = MediaDevices; 14 | 15 | /** 16 | * Spec: https://w3c.github.io/mediacapture-main/#dom-mediadevices 17 | */ 18 | 19 | /** 20 | * Dependencies. 21 | */ 22 | var 23 | EventTarget = require('./EventTarget'), 24 | getUserMedia = require('./getUserMedia'), 25 | enumerateDevices = require('./enumerateDevices'); 26 | 27 | function MediaDevices(data) { 28 | 29 | //ondevicechange 30 | //enumerateDevices 31 | //getDisplayMedia 32 | //getSupportedConstraints 33 | //getUserMedia 34 | 35 | var self = this; 36 | 37 | // Make this an EventTarget. 38 | EventTarget.call(self); 39 | 40 | data = data || {}; 41 | } 42 | 43 | MediaDevices.prototype = Object.create(EventTarget.prototype); 44 | MediaDevices.prototype.constructor = MediaDevices; 45 | 46 | MediaDevices.prototype.getUserMedia = function (constraints) { 47 | return getUserMedia(constraints); 48 | }; 49 | 50 | MediaDevices.prototype.enumerateDevices = function () { 51 | return enumerateDevices(); 52 | }; 53 | 54 | MediaDevices.prototype.getSupportedConstraints = function () { 55 | return { 56 | // Supported 57 | height: true, 58 | width: true, 59 | deviceId: true, 60 | frameRate: true, 61 | sampleRate: true, 62 | aspectRatio: true, 63 | // Not Supported 64 | autoGainControl: false, 65 | brightness: false, 66 | channelCount: false, 67 | colorTemperature: false, 68 | contrast: false, 69 | echoCancellation: false, 70 | exposureCompensation: false, 71 | exposureMode: false, 72 | exposureTime: false, 73 | facingMode: true, 74 | focusDistance: false, 75 | focusMode: false, 76 | groupId: false, 77 | iso: false, 78 | latency: false, 79 | noiseSuppression: false, 80 | pointsOfInterest: false, 81 | resizeMode: false, 82 | sampleSize: false, 83 | saturation: false, 84 | sharpness: false, 85 | torch: false, 86 | whiteBalanceMode: false, 87 | zoom: false 88 | }; 89 | }; 90 | 91 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // WKWebViewRTC 4 | // 5 | // Created by JustDoIt9 on 07/28/2020. 6 | // Copyright (c) 2020 JustDoIt9. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/RTCDTMFSender.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the RTCDTMFSender class. 12 | */ 13 | module.exports = RTCDTMFSender; 14 | 15 | 16 | /** 17 | * Dependencies. 18 | */ 19 | var 20 | debug = require('debug')('iosrtc:RTCDTMFSender'), 21 | debugerror = require('debug')('iosrtc:ERROR:RTCDTMFSender'), 22 | exec = require('./IOSExec'), 23 | randomNumber = require('random-number').generator({min: 10000, max: 99999, integer: true}), 24 | EventTarget = require('./EventTarget'); 25 | 26 | 27 | debugerror.log = console.warn.bind(console); 28 | 29 | 30 | function RTCDTMFSender(peerConnection, track) { 31 | var self = this; 32 | 33 | // Make this an EventTarget. 34 | EventTarget.call(this); 35 | 36 | debug('new() | [track:%o]', track); 37 | 38 | // Public atributes (accessed as read-only properties) 39 | this._track = track; 40 | // TODO: read these from the properties exposed in Swift? 41 | this._duration = 100; 42 | this._interToneGap = 70; 43 | this._toneBuffer = ''; 44 | 45 | // Private attributes. 46 | this.peerConnection = peerConnection; 47 | this.dsId = randomNumber(); 48 | 49 | function onResultOK(data) { 50 | onEvent.call(self, data); 51 | } 52 | 53 | exec.execNative(onResultOK, null, 'WKWebViewRTC', 'RTCPeerConnection_createDTMFSender', [this.peerConnection.pcId, this.dsId, this._track.id]); 54 | 55 | } 56 | 57 | RTCDTMFSender.prototype = Object.create(EventTarget.prototype); 58 | RTCDTMFSender.prototype.constructor = RTCDTMFSender; 59 | 60 | Object.defineProperty(RTCDTMFSender.prototype, 'canInsertDTMF', { 61 | get: function () { 62 | // TODO: check if it's muted or stopped? 63 | return this._track && this._track.kind === 'audio' && this._track.enabled; 64 | } 65 | }); 66 | 67 | 68 | Object.defineProperty(RTCDTMFSender.prototype, 'track', { 69 | get: function () { 70 | return this._track; 71 | } 72 | }); 73 | 74 | 75 | Object.defineProperty(RTCDTMFSender.prototype, 'duration', { 76 | get: function () { 77 | return this._duration; 78 | } 79 | }); 80 | 81 | 82 | Object.defineProperty(RTCDTMFSender.prototype, 'interToneGap', { 83 | get: function () { 84 | return this._interToneGap; 85 | } 86 | }); 87 | 88 | 89 | Object.defineProperty(RTCDTMFSender.prototype, 'toneBuffer', { 90 | get: function () { 91 | return this._toneBuffer; 92 | } 93 | }); 94 | 95 | 96 | RTCDTMFSender.prototype.insertDTMF = function (tones, duration, interToneGap) { 97 | if (isClosed.call(this)) { 98 | return; 99 | } 100 | 101 | debug('insertDTMF() | [tones:%o, duration:%o, interToneGap:%o]', tones, duration, interToneGap); 102 | 103 | if (!tones) { 104 | return; 105 | } 106 | 107 | this._duration = duration || 100; 108 | this._interToneGap = interToneGap || 70; 109 | 110 | var self = this; 111 | 112 | function onResultOK(data) { 113 | onEvent.call(self, data); 114 | } 115 | 116 | exec.execNative(onResultOK, null, 'WKWebViewRTC', 'RTCPeerConnection_RTCDTMFSender_insertDTMF', [this.peerConnection.pcId, this.dsId, tones, this._duration, this._interToneGap]); 117 | }; 118 | 119 | 120 | /** 121 | * Private API. 122 | */ 123 | 124 | 125 | function isClosed() { 126 | return this.peerConnection.signalingState === 'closed'; 127 | } 128 | 129 | 130 | function onEvent(data) { 131 | var type = data.type, 132 | event; 133 | 134 | debug('onEvent() | [type:%s, data:%o]', type, data); 135 | 136 | if (type === 'tonechange') { 137 | event = new Event('tonechange'); 138 | event.tone = data.tone; 139 | this.dispatchEvent(event); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/WKWebViewRTC.xcodeproj/xcshareddata/xcschemes/WKWebViewRTC-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 78 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iMediaStreamTrack.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | import WebRTC 12 | 13 | class iMediaStreamTrack : NSObject { 14 | var rtcMediaStreamTrack: RTCMediaStreamTrack 15 | var id: String 16 | var kind: String 17 | var eventListener: ((_ data: NSDictionary) -> Void)? 18 | var eventListenerForEnded: (() -> Void)? 19 | var lostStates = Array() 20 | var renders: [String : iMediaStreamRenderer] 21 | 22 | init(rtcMediaStreamTrack: RTCMediaStreamTrack, trackId: String? = nil) { 23 | NSLog("iMediaStreamTrack#init()") 24 | 25 | self.rtcMediaStreamTrack = rtcMediaStreamTrack 26 | 27 | if (trackId == nil) { 28 | // Handle possible duplicate remote trackId with janus or short duplicate name 29 | // See: https://github.com/cordova-rtc/cordova-plugin-iosrtc/issues/432 30 | if (rtcMediaStreamTrack.trackId.count<36) { 31 | self.id = rtcMediaStreamTrack.trackId + "_" + UUID().uuidString; 32 | } else { 33 | self.id = rtcMediaStreamTrack.trackId; 34 | } 35 | } else { 36 | self.id = trackId!; 37 | } 38 | 39 | self.kind = rtcMediaStreamTrack.kind 40 | self.renders = [:] 41 | } 42 | 43 | deinit { 44 | NSLog("iMediaStreamTrack#deinit()") 45 | stop() 46 | } 47 | 48 | func run() { 49 | NSLog("iMediaStreamTrack#run() [kind:%@, id:%@]", String(self.kind), String(self.id)) 50 | } 51 | 52 | func getReadyState() -> String { 53 | switch self.rtcMediaStreamTrack.readyState { 54 | case RTCMediaStreamTrackState.live: 55 | return "live" 56 | case RTCMediaStreamTrackState.ended: 57 | return "ended" 58 | default: 59 | return "ended" 60 | } 61 | } 62 | 63 | func getJSON() -> NSDictionary { 64 | return [ 65 | "id": self.id, 66 | "kind": self.kind, 67 | "trackId": self.rtcMediaStreamTrack.trackId, 68 | "enabled": self.rtcMediaStreamTrack.isEnabled ? true : false, 69 | "capabilities": self.rtcMediaStreamTrack.capabilities, 70 | "readyState": self.getReadyState() 71 | ] 72 | } 73 | 74 | func setListener( 75 | _ eventListener: @escaping (_ data: NSDictionary) -> Void, 76 | eventListenerForEnded: @escaping () -> Void 77 | ) { 78 | if(self.eventListener != nil){ 79 | NSLog("iMediaStreamTrack#setListener():Error Listener already Set [kind:%@, id:%@]", String(self.kind), String(self.id)); 80 | return; 81 | } 82 | 83 | NSLog("iMediaStreamTrack#setListener() [kind:%@, id:%@]", String(self.kind), String(self.id)) 84 | 85 | self.eventListener = eventListener 86 | self.eventListenerForEnded = eventListenerForEnded 87 | 88 | for readyState in self.lostStates { 89 | self.eventListener!([ 90 | "type": "statechange", 91 | "readyState": readyState, 92 | "enabled": self.rtcMediaStreamTrack.isEnabled ? true : false 93 | ]) 94 | 95 | if readyState == "ended" { 96 | if(self.eventListenerForEnded != nil) { 97 | self.eventListenerForEnded!() 98 | } 99 | } 100 | } 101 | self.lostStates.removeAll() 102 | } 103 | 104 | func setEnabled(_ value: Bool) { 105 | NSLog("iMediaStreamTrack#setEnabled() [kind:%@, id:%@, value:%@]", 106 | String(self.kind), String(self.id), String(value)) 107 | 108 | if (self.rtcMediaStreamTrack.isEnabled != value) { 109 | self.rtcMediaStreamTrack.isEnabled = value 110 | if (value) { 111 | self.rtcMediaStreamTrack.videoCaptureController?.startCapture() 112 | }else { 113 | self.rtcMediaStreamTrack.videoCaptureController?.stopCapture() 114 | } 115 | } 116 | } 117 | 118 | func switchCamera() { 119 | self.rtcMediaStreamTrack.videoCaptureController?.switchCamera() 120 | } 121 | 122 | func registerRender(render: iMediaStreamRenderer) { 123 | if let exist = self.renders[render.id] { 124 | _ = exist 125 | } else { 126 | self.renders[render.id] = render 127 | } 128 | } 129 | 130 | func unregisterRender(render: iMediaStreamRenderer) { 131 | self.renders.removeValue(forKey: render.id); 132 | } 133 | 134 | func stop() { 135 | NSLog("iMediaStreamTrack#stop() [kind:%@, id:%@]", String(self.kind), String(self.id)) 136 | 137 | self.rtcMediaStreamTrack.videoCaptureController?.stopCapture(); 138 | 139 | // Let's try setEnabled(false), but it also fails. 140 | self.rtcMediaStreamTrack.isEnabled = false 141 | // eventListener could be null if the track is never used 142 | if(self.eventListener != nil){ 143 | self.eventListener!([ 144 | "type": "statechange", 145 | "readyState": "ended", 146 | "enabled": self.rtcMediaStreamTrack.isEnabled ? true : false 147 | ]) 148 | } 149 | 150 | for (_, render) in self.renders { 151 | render.stop() 152 | } 153 | self.renders.removeAll(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/MediaStreamTrack.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the MediaStreamTrack class. 12 | */ 13 | module.exports = MediaStreamTrack; 14 | 15 | 16 | /** 17 | * Spec: http://w3c.github.io/mediacapture-main/#mediastreamtrack 18 | */ 19 | 20 | 21 | /** 22 | * Dependencies. 23 | */ 24 | var 25 | debug = require('debug')('iosrtc:MediaStreamTrack'), 26 | exec = require('./IOSExec'), 27 | enumerateDevices = require('./enumerateDevices'), 28 | MediaTrackCapabilities = require('./MediaTrackCapabilities'), 29 | MediaTrackSettings = require('./MediaTrackSettings'), 30 | EventTarget = require('./EventTarget'); 31 | 32 | // Save original MediaStreamTrack 33 | var originalMediaStreamTrack = window.MediaStreamTrack || function dummyMediaStreamTrack() {}; 34 | 35 | function newMediaStreamTrackId() { 36 | return window.crypto.getRandomValues(new Uint32Array(4)).join('-'); 37 | } 38 | 39 | function MediaStreamTrack(dataFromEvent) { 40 | if (!dataFromEvent) { 41 | throw new Error('Illegal constructor'); 42 | } 43 | 44 | debug('new() | [dataFromEvent:%o]', dataFromEvent); 45 | 46 | var self = this; 47 | 48 | // Make this an EventTarget. 49 | EventTarget.call(this); 50 | 51 | // Public atributes. 52 | this.id = dataFromEvent.id; // NOTE: It's a string. 53 | this.kind = dataFromEvent.kind; 54 | this.label = dataFromEvent.label; 55 | this.muted = false; // TODO: No "muted" property in ObjC API. 56 | this.capabilities = dataFromEvent.capabilities; 57 | this.readyState = dataFromEvent.readyState; 58 | 59 | // Private attributes. 60 | this._enabled = dataFromEvent.enabled; 61 | this._ended = false; 62 | 63 | function onResultOK(data) { 64 | onEvent.call(self, data); 65 | } 66 | 67 | exec.execNative(onResultOK, null, 'WKWebViewRTC', 'MediaStreamTrack_setListener', [this.id]); 68 | } 69 | 70 | MediaStreamTrack.prototype = Object.create(EventTarget.prototype); 71 | MediaStreamTrack.prototype.constructor = MediaStreamTrack; 72 | 73 | // Static reference to original MediaStreamTrack 74 | MediaStreamTrack.originalMediaStreamTrack = originalMediaStreamTrack; 75 | 76 | // Setters. 77 | Object.defineProperty(MediaStreamTrack.prototype, 'enabled', { 78 | get: function () { 79 | return this._enabled; 80 | }, 81 | set: function (value) { 82 | debug('enabled = %s', !!value); 83 | 84 | this._enabled = !!value; 85 | exec.execNative(null, null, 'WKWebViewRTC', 'MediaStreamTrack_setEnabled', [this.id, this._enabled]); 86 | } 87 | }); 88 | 89 | MediaStreamTrack.prototype.getConstraints = function () { 90 | return {}; 91 | }; 92 | 93 | MediaStreamTrack.prototype.applyConstraints = function () { 94 | return Promise.reject(new Error('applyConstraints is not implemented.')); 95 | }; 96 | 97 | MediaStreamTrack.prototype.clone = function () { 98 | var newTrackId = newMediaStreamTrackId(); 99 | 100 | exec.execNative(null, null, 'WKWebViewRTC', 'MediaStreamTrack_clone', [this.id, newTrackId]); 101 | 102 | return new MediaStreamTrack({ 103 | id: newTrackId, 104 | kind: this.kind, 105 | label: this.label, 106 | readyState: this.readyState, 107 | enabled: this.enabled 108 | }); 109 | }; 110 | 111 | MediaStreamTrack.prototype.getCapabilities = function () { 112 | //throw new Error('Not implemented.'); 113 | // SHAM 114 | return new MediaTrackCapabilities(this.capabilities); 115 | }; 116 | 117 | MediaStreamTrack.prototype.getSettings = function () { 118 | //throw new Error('Not implemented.'); 119 | // SHAM 120 | return new MediaTrackSettings(); 121 | }; 122 | 123 | MediaStreamTrack.prototype.stop = function () { 124 | debug('stop()'); 125 | 126 | if (this._ended) { 127 | return; 128 | } 129 | 130 | exec.execNative(null, null, 'WKWebViewRTC', 'MediaStreamTrack_stop', [this.id]); 131 | }; 132 | 133 | 134 | // TODO: API methods and events. 135 | 136 | 137 | /** 138 | * Class methods. 139 | */ 140 | 141 | 142 | MediaStreamTrack.getSources = function () { 143 | debug('getSources()'); 144 | 145 | return enumerateDevices.apply(this, arguments); 146 | }; 147 | 148 | 149 | /** 150 | * Private API. 151 | */ 152 | 153 | 154 | function onEvent(data) { 155 | var type = data.type; 156 | 157 | debug('onEvent() | [type:%s, data:%o]', type, data); 158 | 159 | switch (type) { 160 | case 'statechange': 161 | this.readyState = data.readyState; 162 | this._enabled = data.enabled; 163 | 164 | switch (data.readyState) { 165 | case 'initializing': 166 | break; 167 | case 'live': 168 | break; 169 | case 'ended': 170 | this._ended = true; 171 | this.dispatchEvent(new Event('ended')); 172 | break; 173 | case 'failed': 174 | break; 175 | } 176 | break; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iRTCPeerConnectionConstraints.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | import WebRTC 12 | 13 | class iRTCPeerConnectionConstraints { 14 | fileprivate var constraints: RTCMediaConstraints 15 | 16 | init(pcConstraints: NSDictionary?) { 17 | NSLog("iRTCPeerConnectionConstraints#init()") 18 | var mandatoryConstraints: [String : String] = [:] 19 | var optionalConstraints: [String : String] = [:] 20 | 21 | // Handle constraints at the root of pcConstraints.mandatory 22 | let _mandatoryConstraints = pcConstraints?.object(forKey: "mandatory") as? NSDictionary 23 | if _mandatoryConstraints != nil { 24 | mandatoryConstraints = iRTCPeerConnectionConstraints.parseConstraints(constraints: _mandatoryConstraints!) 25 | } 26 | 27 | // Handle constraints at the root of pcConstraints.optional 28 | let _optionalConstraints = pcConstraints?.object(forKey: "optional") as? NSArray 29 | if _optionalConstraints != nil { 30 | optionalConstraints = iRTCPeerConnectionConstraints.parseOptionalConstraints(optionalConstraints: _optionalConstraints!) 31 | } 32 | 33 | // Handle constraints at the root of pcConstraints 34 | if pcConstraints != nil && _optionalConstraints == nil && _mandatoryConstraints == nil { 35 | mandatoryConstraints = iRTCPeerConnectionConstraints.parseConstraints(constraints: pcConstraints!) 36 | } 37 | 38 | NSLog("iRTCPeerConnectionConstraints#init() | [mandatoryConstraints:%@, optionalConstraints:%@]", 39 | mandatoryConstraints, optionalConstraints) 40 | 41 | self.constraints = RTCMediaConstraints.init( 42 | mandatoryConstraints: mandatoryConstraints, 43 | optionalConstraints: optionalConstraints 44 | ) 45 | } 46 | 47 | // Custom proprietary constrains in libwebrtc 48 | fileprivate static let allowedOptionalConstraints : Array = [ 49 | // Temporary pseudo-constraints used to enable DTLS-SRT 50 | "DtlsSrtpKeyAgreement", 51 | // Constraint to enable IPv6 52 | "googIPv6", 53 | // Constraint to enable combined audio+video bandwidth estimation. 54 | "googImprovedWifiBwe", 55 | // Temporary pseudo-constraint for enabling DSCP 56 | "googDscp", 57 | // If enabled, will lower outgoing video quality and video resolution 58 | "googCpuOveruseDetection", 59 | // Min CPU load (percents), used in pair with 60 | "googCpuUnderuseThreshold", 61 | // Max CPU (percents), used in pair with googCpuOveruseDetection 62 | "googCpuOveruseThreshold", 63 | // Cause video track to be automatically disabled if the detected bandwidth drops below "min bitrate" 64 | "googSuspendBelowMinBitrate", 65 | //"googScreencastMinBitrate" 66 | ] 67 | 68 | fileprivate static func parseOptionalConstraints(optionalConstraints: NSArray) -> [String : String] { 69 | var _constraints : [String : String] = [:] 70 | 71 | for optionalConstraint in optionalConstraints { 72 | if (optionalConstraint is NSDictionary) { 73 | for (key, value) in optionalConstraint as! NSDictionary { 74 | let finalKey: String = key as! String; 75 | 76 | // Parse only allowedOptionalConstraints 77 | if (allowedOptionalConstraints.firstIndex(of: finalKey) != nil) { 78 | if value is Int32 { 79 | _constraints[finalKey] = String(describing: value); 80 | } else if value is Bool { 81 | _constraints[finalKey] = value as! Bool ? "true" : "false" 82 | } else if value is String { 83 | _constraints[finalKey] = (value as! String) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | return _constraints; 91 | } 92 | 93 | // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer#RTCOfferOptions_dictionary 94 | // TODO TODO voiceActivityDetection This option defaults to true 95 | fileprivate static let allowedConstraints : Array = [ 96 | "IceRestart", 97 | "OfferToReceiveVideo", 98 | "OfferToReceiveAudio", 99 | "voiceActivityDetection" 100 | ] 101 | 102 | fileprivate static func parseConstraints(constraints: NSDictionary) -> [String : String] { 103 | var _constraints : [String : String] = [:] 104 | 105 | for (key, value) in constraints { 106 | var finalValue: String, 107 | finalKey: String = key as! String; 108 | 109 | if value is Bool { 110 | finalValue = value as! Bool ? "true" : "false" 111 | } else if value is String { 112 | finalValue = value as! String 113 | } else { 114 | continue 115 | } 116 | 117 | // Handle Spec for offerToReceiveAudio|offerToReceiveVideo but 118 | // libwebrtc still use OfferToReceiveAudio|OfferToReceiveVideo 119 | if (finalKey == "offerToReceiveAudio") { 120 | finalKey = "OfferToReceiveAudio"; 121 | } else if (finalKey == "offerToReceiveVideo") { 122 | finalKey = "OfferToReceiveVideo"; 123 | } else if (finalKey == "iceRestart") { 124 | finalKey = "IceRestart"; 125 | } 126 | 127 | // Filter to avoid injection 128 | if (allowedConstraints.firstIndex(of: finalKey) != nil) { 129 | _constraints[finalKey] = finalValue 130 | } 131 | } 132 | 133 | return _constraints; 134 | } 135 | 136 | deinit { 137 | NSLog("iRTCPeerConnectionConstraints#deinit()") 138 | } 139 | 140 | func getConstraints() -> RTCMediaConstraints { 141 | NSLog("iRTCPeerConnectionConstraints#getConstraints()") 142 | 143 | return self.constraints 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iEnumerateDevices.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | import AVFoundation 12 | import WebRTC 13 | 14 | /** 15 | * Doc: https://developer.apple.com/library/mac/documentation/AVFoundation/Reference/AVCaptureDevice_Class/index.html 16 | */ 17 | 18 | struct MediaDeviceInfo { 19 | let deviceId, groupId, kind, label: String 20 | init(deviceId: String, kind: String, label: String) { 21 | self.deviceId = deviceId 22 | self.groupId = "" 23 | self.kind = kind 24 | self.label = label 25 | } 26 | } 27 | 28 | // Helper function inserted by Swift 4.2 migrator. 29 | fileprivate func convertFromAVMediaType(_ input: AVMediaType) -> String { 30 | return input.rawValue 31 | } 32 | 33 | class iEnumerateDevices { 34 | class func call(_ callback: (_ data: NSDictionary) -> Void) { 35 | NSLog("iEnumerateDevices#call()") 36 | 37 | let audioDevices: [MediaDeviceInfo] = getAllAudioDevices() 38 | let videoDevices: [MediaDeviceInfo] = getAllVideoDevices() 39 | let allDevices = videoDevices + audioDevices; 40 | 41 | let json: NSMutableDictionary = [ 42 | "devices": NSMutableArray() 43 | ] 44 | 45 | // Casting to NSMutableDictionary 46 | for device: MediaDeviceInfo in allDevices { 47 | (json["devices"] as! NSMutableArray).add([ 48 | "deviceId": device.deviceId, 49 | "kind": device.kind, 50 | "label": device.label, 51 | "groupId": device.groupId 52 | ]) 53 | } 54 | 55 | print("DEVICES => ", json) 56 | callback(json as NSDictionary) 57 | } 58 | } 59 | 60 | fileprivate func getAllVideoDevices() -> [MediaDeviceInfo] { 61 | 62 | var videoDevicesArr : [MediaDeviceInfo] = [] 63 | var deviceTypes: [AVCaptureDevice.DeviceType] = [.builtInTelephotoCamera, .builtInWideAngleCamera] 64 | if #available(iOS 10.2, *) { 65 | deviceTypes.append(.builtInDualCamera) 66 | 67 | // Disabled tp prevent duplicate front camera 68 | //if #available(iOS 11.1, *) { 69 | // deviceTypes.append(.builtInTrueDepthCamera) 70 | //} 71 | } 72 | 73 | let videoDevices: [AVCaptureDevice] = AVCaptureDevice.DiscoverySession.init( 74 | deviceTypes: deviceTypes, 75 | mediaType: AVMediaType.video, 76 | position: AVCaptureDevice.Position.unspecified 77 | ).devices 78 | 79 | for device: AVCaptureDevice in videoDevices { 80 | var facing: String 81 | var facingLabel: String; 82 | let hasAudio = device.hasMediaType(AVMediaType(rawValue: convertFromAVMediaType(AVMediaType.audio))) 83 | let hasVideo = device.hasMediaType(AVMediaType(rawValue: convertFromAVMediaType(AVMediaType.video))) 84 | 85 | switch device.position { 86 | case AVCaptureDevice.Position.unspecified: 87 | facing = "unknown" 88 | facingLabel = ""; 89 | case AVCaptureDevice.Position.back: 90 | facing = "back" 91 | facingLabel = "Back Camera" 92 | case AVCaptureDevice.Position.front: 93 | facing = "front" 94 | facingLabel = "Front Camera" 95 | } 96 | 97 | if device.isConnected == false || (hasAudio == false && hasVideo == false) { 98 | continue 99 | } 100 | 101 | NSLog("- device [uniqueID:'%@', localizedName:'%@', facing:%@, audio:%@, video:%@, connected:%@]", 102 | String(device.uniqueID), String(device.localizedName), String(facing), 103 | String(hasAudio), String(hasVideo), String(device.isConnected)) 104 | 105 | if hasAudio == false { 106 | // Add English facingLabel suffix if localizedName does not match for facing detection using label 107 | let deviceLabel = device.localizedName.contains(facingLabel) ? 108 | device.localizedName : device.localizedName + " (" + facingLabel + ")"; 109 | 110 | let device = MediaDeviceInfo( 111 | deviceId: device.uniqueID, 112 | kind: "videoinput", 113 | label: deviceLabel 114 | ) 115 | 116 | // Put Front devices at beginning of the videoDevicesArr 117 | if (facing == "front") { 118 | // Simple Swift 4 array unshift 119 | let videoDevicesArrFirst : [MediaDeviceInfo] = [device] 120 | videoDevicesArr = videoDevicesArrFirst + videoDevicesArr; 121 | } else { 122 | videoDevicesArr.append(device) 123 | } 124 | } 125 | } 126 | 127 | return videoDevicesArr 128 | } 129 | 130 | // Getter function inserted by get all audio devices connected 131 | fileprivate func getAllAudioDevices() -> [MediaDeviceInfo] { 132 | 133 | let audioSession: AVAudioSession = AVAudioSession.sharedInstance() 134 | var audioDevicesArr : [MediaDeviceInfo] = [] 135 | let audioInputDevices: [AVAudioSessionPortDescription] = audioSession.availableInputs! 136 | var bluetoothDevice: AVAudioSessionPortDescription? = nil 137 | var wiredDevice: AVAudioSessionPortDescription? = nil 138 | var builtMicDevice: AVAudioSessionPortDescription? = nil 139 | 140 | for audioInput in audioInputDevices { 141 | 142 | let device = MediaDeviceInfo( 143 | deviceId: audioInput.uid, 144 | kind: "audioinput", 145 | label: audioInput.portName 146 | ); 147 | 148 | audioDevicesArr.append(device) 149 | 150 | // Initialize audioInputSelected. Default Built-In Microphone 151 | if audioInput.portType == AVAudioSession.Port.builtInMic { 152 | builtMicDevice = audioInput 153 | } 154 | 155 | if audioInput.portType == .bluetoothHFP || audioInput.portType == .bluetoothA2DP { 156 | bluetoothDevice = audioInput 157 | } 158 | 159 | if audioInput.portType == .usbAudio || audioInput.portType == .headsetMic { 160 | wiredDevice = audioInput 161 | } 162 | } 163 | 164 | // Initialize audioInputSelected. Priority: [Wired - Wireless - Built-In Microphone] 165 | if wiredDevice != nil { 166 | iRTCAudioController.saveInputAudioDevice(inputDeviceUID: wiredDevice!.uid) 167 | } else if bluetoothDevice != nil { 168 | iRTCAudioController.saveInputAudioDevice(inputDeviceUID: bluetoothDevice!.uid) 169 | } else if builtMicDevice != nil { 170 | iRTCAudioController.saveInputAudioDevice(inputDeviceUID: builtMicDevice!.uid) 171 | } 172 | return audioDevicesArr 173 | } 174 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iMediaStream.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | import WebRTC 12 | 13 | class iMediaStream : NSObject { 14 | 15 | var rtcMediaStream: RTCMediaStream 16 | var id: String 17 | var audioTracks: [String : iMediaStreamTrack] = [:] 18 | var videoTracks: [String : iMediaStreamTrack] = [:] 19 | var eventListener: ((_ data: NSDictionary) -> Void)? 20 | var eventListenerForAddTrack: ((_ pluginMediaStreamTrack: iMediaStreamTrack) -> Void)? 21 | var eventListenerForRemoveTrack: ((_ pluginMediaStreamTrack: iMediaStreamTrack) -> Void)? 22 | 23 | /** 24 | * Constructor for pc.onaddstream event and getUserMedia(). 25 | */ 26 | init(rtcMediaStream: RTCMediaStream, streamId: String? = nil) { 27 | NSLog("iMediaStream#init()") 28 | 29 | self.rtcMediaStream = rtcMediaStream; 30 | 31 | if (streamId == nil) { 32 | // Handle possible duplicate remote trackId with janus or short duplicate name 33 | // See: https://github.com/cordova-rtc/cordova-plugin-iosrtc/issues/432 34 | if (rtcMediaStream.streamId.count < 36) { 35 | self.id = rtcMediaStream.streamId + "_" + UUID().uuidString; 36 | } else { 37 | self.id = rtcMediaStream.streamId; 38 | } 39 | } else { 40 | self.id = streamId!; 41 | } 42 | 43 | for track: RTCMediaStreamTrack in (self.rtcMediaStream.audioTracks as Array) { 44 | let pluginMediaStreamTrack = iMediaStreamTrack(rtcMediaStreamTrack: track) 45 | 46 | pluginMediaStreamTrack.run() 47 | self.audioTracks[pluginMediaStreamTrack.id] = pluginMediaStreamTrack 48 | } 49 | 50 | for track: RTCMediaStreamTrack in (self.rtcMediaStream.videoTracks as Array) { 51 | let pluginMediaStreamTrack = iMediaStreamTrack(rtcMediaStreamTrack: track) 52 | 53 | pluginMediaStreamTrack.run() 54 | self.videoTracks[pluginMediaStreamTrack.id] = pluginMediaStreamTrack 55 | } 56 | } 57 | 58 | deinit { 59 | NSLog("iMediaStream#deinit()") 60 | stop(); 61 | } 62 | 63 | func run() { 64 | NSLog("iMediaStream#run()") 65 | } 66 | 67 | func stop() { 68 | NSLog("iMediaStream#stop()") 69 | 70 | for (_, track) in audioTracks { 71 | if(self.eventListenerForRemoveTrack != nil) { 72 | self.eventListenerForRemoveTrack!(track) 73 | } 74 | } 75 | for (_, track) in videoTracks { 76 | if(self.eventListenerForRemoveTrack != nil) { 77 | self.eventListenerForRemoveTrack!(track) 78 | } 79 | } 80 | } 81 | 82 | func getJSON() -> NSDictionary { 83 | let json: NSMutableDictionary = [ 84 | "id": self.id, 85 | "audioTracks": NSMutableDictionary(), 86 | "videoTracks": NSMutableDictionary() 87 | ] 88 | 89 | for (id, pluginMediaStreamTrack) in self.audioTracks { 90 | (json["audioTracks"] as! NSMutableDictionary)[id] = pluginMediaStreamTrack.getJSON() 91 | } 92 | 93 | for (id, pluginMediaStreamTrack) in self.videoTracks { 94 | (json["videoTracks"] as! NSMutableDictionary)[id] = pluginMediaStreamTrack.getJSON() 95 | } 96 | 97 | return json as NSDictionary 98 | } 99 | 100 | func setListener( 101 | _ eventListener: @escaping (_ data: NSDictionary) -> Void, 102 | eventListenerForAddTrack: ((_ pluginMediaStreamTrack: iMediaStreamTrack) -> Void)?, 103 | eventListenerForRemoveTrack: ((_ pluginMediaStreamTrack: iMediaStreamTrack) -> Void)? 104 | ) { 105 | NSLog("iMediaStream#setListener()") 106 | 107 | self.eventListener = eventListener 108 | self.eventListenerForAddTrack = eventListenerForAddTrack 109 | self.eventListenerForRemoveTrack = eventListenerForRemoveTrack 110 | } 111 | 112 | func hasTrack(_ pluginMediaStreamTrack: iMediaStreamTrack) -> Bool { 113 | if pluginMediaStreamTrack.kind == "audio" { 114 | return self.audioTracks[pluginMediaStreamTrack.id] != nil; 115 | } else if pluginMediaStreamTrack.kind == "video" { 116 | return self.videoTracks[pluginMediaStreamTrack.id] != nil; 117 | } else { 118 | return false 119 | } 120 | } 121 | 122 | func addTrack(_ pluginMediaStreamTrack: iMediaStreamTrack) -> Bool { 123 | NSLog("iMediaStream#addTrack()") 124 | 125 | if pluginMediaStreamTrack.kind == "audio" { 126 | self.rtcMediaStream.addAudioTrack(pluginMediaStreamTrack.rtcMediaStreamTrack as! RTCAudioTrack) 127 | NSLog("iMediaStream#addTrack() | audio track added") 128 | self.audioTracks[pluginMediaStreamTrack.id] = pluginMediaStreamTrack 129 | } else if pluginMediaStreamTrack.kind == "video" { 130 | self.rtcMediaStream.addVideoTrack(pluginMediaStreamTrack.rtcMediaStreamTrack as! RTCVideoTrack) 131 | NSLog("iMediaStream#addTrack() | video track added") 132 | self.videoTracks[pluginMediaStreamTrack.id] = pluginMediaStreamTrack 133 | } else { 134 | return false 135 | } 136 | 137 | onAddTrack(pluginMediaStreamTrack) 138 | return true 139 | } 140 | 141 | func removeTrack(_ pluginMediaStreamTrack: iMediaStreamTrack) -> Bool { 142 | NSLog("iMediaStream#removeTrack()") 143 | 144 | if pluginMediaStreamTrack.kind == "audio" { 145 | self.audioTracks[pluginMediaStreamTrack.id] = nil 146 | self.rtcMediaStream.removeAudioTrack(pluginMediaStreamTrack.rtcMediaStreamTrack as! RTCAudioTrack) 147 | NSLog("iMediaStream#removeTrack() | audio track removed") 148 | } else if pluginMediaStreamTrack.kind == "video" { 149 | self.videoTracks[pluginMediaStreamTrack.id] = nil 150 | self.rtcMediaStream.removeVideoTrack(pluginMediaStreamTrack.rtcMediaStreamTrack as! RTCVideoTrack) 151 | NSLog("iMediaStream#removeTrack() | video track removed") 152 | } else { 153 | return false 154 | } 155 | 156 | onRemoveTrack(pluginMediaStreamTrack) 157 | return true 158 | } 159 | 160 | func onAddTrack(_ track: iMediaStreamTrack) { 161 | NSLog("iMediaStream | OnAddTrack [label:%@]", String(track.id)) 162 | 163 | track.run() 164 | 165 | if self.eventListener != nil { 166 | self.eventListenerForAddTrack!(track) 167 | 168 | self.eventListener!([ 169 | "type": "addtrack", 170 | "track": track.getJSON() 171 | ]) 172 | } 173 | } 174 | 175 | func onRemoveTrack(_ track: iMediaStreamTrack) { 176 | NSLog("iMediaStream | OnRemoveTrack [label:%@]", String(track.id)) 177 | 178 | // It may happen that track was removed due to user action (removeTrack()). 179 | if self.audioTracks[track.id] != nil { 180 | self.audioTracks[track.id] = nil 181 | } else if self.videoTracks[track.id] != nil { 182 | self.videoTracks[track.id] = nil 183 | } else { 184 | return 185 | } 186 | 187 | if self.eventListener != nil { 188 | self.eventListenerForRemoveTrack!(track) 189 | 190 | self.eventListener!([ 191 | "type": "removetrack", 192 | "track": [ 193 | "id": track.id, 194 | "kind": track.kind 195 | ] 196 | ]) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iGetUserMedia.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | import AVFoundation 12 | import WebRTC 13 | 14 | class iGetUserMedia { 15 | 16 | var rtcPeerConnectionFactory: RTCPeerConnectionFactory 17 | 18 | init(rtcPeerConnectionFactory: RTCPeerConnectionFactory) { 19 | NSLog("iGetUserMedia#init()") 20 | self.rtcPeerConnectionFactory = rtcPeerConnectionFactory 21 | } 22 | 23 | deinit { 24 | NSLog("iGetUserMedia#deinit()") 25 | } 26 | 27 | func call( 28 | _ constraints: NSDictionary, 29 | callback: (_ data: NSDictionary) -> Void, 30 | errback: (_ error: String) -> Void, 31 | eventListenerForNewStream: (_ pluginMediaStream: iMediaStream) -> Void 32 | ) { 33 | 34 | NSLog("iGetUserMedia#call()") 35 | 36 | var videoRequested: Bool = false 37 | var audioRequested: Bool = false 38 | 39 | if (constraints.object(forKey: "video") != nil) { 40 | videoRequested = true 41 | } 42 | 43 | if constraints.object(forKey: "audio") != nil { 44 | audioRequested = true 45 | } 46 | 47 | var rtcMediaStream: RTCMediaStream 48 | var pluginMediaStream: iMediaStream? 49 | var rtcAudioTrack: RTCAudioTrack? 50 | var rtcVideoTrack: RTCVideoTrack? 51 | var rtcVideoSource: RTCVideoSource? 52 | 53 | if videoRequested == true { 54 | switch AVCaptureDevice.authorizationStatus(for: AVMediaType.video) { 55 | case AVAuthorizationStatus.notDetermined: 56 | NSLog("iGetUserMedia#call() | video authorization: not determined") 57 | AVCaptureDevice.requestAccess(for: .video) { granted in 58 | if granted { 59 | NSLog("iGetUserMedia#call() | video authorization: authorized") 60 | } 61 | } 62 | case AVAuthorizationStatus.authorized: 63 | NSLog("iGetUserMedia#call() | video authorization: authorized") 64 | case AVAuthorizationStatus.denied: 65 | NSLog("iGetUserMedia#call() | video authorization: denied") 66 | errback("video denied") 67 | return 68 | case AVAuthorizationStatus.restricted: 69 | NSLog("iGetUserMedia#call() | video authorization: restricted") 70 | errback("video restricted") 71 | return 72 | } 73 | } 74 | 75 | if audioRequested == true { 76 | switch AVCaptureDevice.authorizationStatus(for: AVMediaType.audio) { 77 | case AVAuthorizationStatus.notDetermined: 78 | NSLog("iGetUserMedia#call() | audio authorization: not determined") 79 | AVCaptureDevice.requestAccess(for: .audio) { granted in 80 | if granted { 81 | NSLog("iGetUserMedia#call() | audio authorization: authorized") 82 | } 83 | } 84 | case AVAuthorizationStatus.authorized: 85 | NSLog("iGetUserMedia#call() | audio authorization: authorized") 86 | case AVAuthorizationStatus.denied: 87 | NSLog("iGetUserMedia#call() | audio authorization: denied") 88 | errback("audio denied") 89 | return 90 | case AVAuthorizationStatus.restricted: 91 | NSLog("iGetUserMedia#call() | audio authorization: restricted") 92 | errback("audio restricted") 93 | return 94 | } 95 | } 96 | 97 | rtcMediaStream = self.rtcPeerConnectionFactory.mediaStream(withStreamId: UUID().uuidString) 98 | 99 | if videoRequested { 100 | 101 | NSLog("iGetUserMedia#call() | video requested") 102 | 103 | rtcVideoSource = self.rtcPeerConnectionFactory.videoSource() 104 | 105 | rtcVideoTrack = self.rtcPeerConnectionFactory.videoTrack(with: rtcVideoSource!, trackId: UUID().uuidString) 106 | 107 | // Handle legacy plugin instance or video: true 108 | var videoConstraints : NSDictionary = [:]; 109 | if (!(constraints.object(forKey: "video") is Bool)) { 110 | videoConstraints = constraints.object(forKey: "video") as! NSDictionary 111 | } 112 | 113 | NSLog("iGetUserMedia#call() | chosen video constraints: %@", videoConstraints) 114 | 115 | // Ignore Simulator cause does not support Camera 116 | #if !targetEnvironment(simulator) 117 | let videoCapturer: RTCCameraVideoCapturer = RTCCameraVideoCapturer(delegate: rtcVideoSource!) 118 | let videoCaptureController: iRTCVideoCaptureController = iRTCVideoCaptureController(capturer: videoCapturer) 119 | rtcVideoTrack!.videoCaptureController = videoCaptureController 120 | 121 | let constraintsSatisfied = videoCaptureController.setConstraints(constraints: videoConstraints) 122 | if (!constraintsSatisfied) { 123 | errback("constraints not satisfied") 124 | return 125 | } 126 | 127 | let captureStarted = videoCaptureController.startCapture() 128 | if (!captureStarted) { 129 | errback("constraints failed") 130 | return 131 | } 132 | #endif 133 | 134 | // If videoSource state is "ended" it means that constraints were not satisfied so 135 | // invoke the given errback. 136 | if (rtcVideoSource!.state == RTCSourceState.ended) { 137 | NSLog("iGetUserMedia() | rtcVideoSource.state is 'ended', constraints not satisfied") 138 | 139 | errback("constraints not satisfied") 140 | return 141 | } 142 | 143 | if let device = rtcVideoTrack!.videoCaptureController?.device { 144 | rtcVideoTrack!.capabilities["deviceId"] = device.uniqueID 145 | } 146 | 147 | rtcMediaStream.addVideoTrack(rtcVideoTrack!) 148 | } 149 | 150 | if audioRequested == true { 151 | 152 | NSLog("iGetUserMedia#call() | audio requested") 153 | 154 | // Handle legacy plugin instance or audio: true 155 | var audioConstraints : NSDictionary = [:]; 156 | if (!(constraints.object(forKey: "audio") is Bool)) { 157 | audioConstraints = constraints.object(forKey: "audio") as! NSDictionary 158 | } 159 | 160 | NSLog("iGetUserMedia#call() | chosen audio constraints: %@", audioConstraints) 161 | 162 | 163 | var audioDeviceId = audioConstraints.object(forKey: "deviceId") as? String 164 | if(audioDeviceId == nil && audioConstraints.object(forKey: "deviceId") != nil){ 165 | let audioId = audioConstraints.object(forKey: "deviceId") as! NSDictionary 166 | audioDeviceId = audioId.object(forKey: "exact") as? String 167 | if(audioDeviceId == nil){ 168 | audioDeviceId = audioId.object(forKey: "ideal") as? String 169 | } 170 | } 171 | 172 | rtcAudioTrack = self.rtcPeerConnectionFactory.audioTrack(withTrackId: UUID().uuidString) 173 | rtcMediaStream.addAudioTrack(rtcAudioTrack!) 174 | 175 | if audioDeviceId == "default" { 176 | audioDeviceId = "Built-In Microphone" 177 | } 178 | 179 | if (audioDeviceId != nil) { 180 | iRTCAudioController.saveInputAudioDevice(inputDeviceUID: audioDeviceId!) 181 | } 182 | } 183 | 184 | pluginMediaStream = iMediaStream(rtcMediaStream: rtcMediaStream) 185 | pluginMediaStream!.run() 186 | 187 | // Let the plugin store it in its dictionary. 188 | eventListenerForNewStream(pluginMediaStream!) 189 | 190 | callback([ 191 | "stream": pluginMediaStream!.getJSON() 192 | ]) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iRTCAudioController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | import AVFoundation 12 | import WebRTC 13 | 14 | class iRTCAudioController { 15 | 16 | static private var audioCategory : AVAudioSession.Category = AVAudioSession.Category.playAndRecord 17 | 18 | static private var audioCategoryOptions : AVAudioSession.CategoryOptions = [ 19 | AVAudioSession.CategoryOptions.mixWithOthers, 20 | AVAudioSession.CategoryOptions.allowBluetooth, 21 | AVAudioSession.CategoryOptions.allowAirPlay, 22 | AVAudioSession.CategoryOptions.allowBluetoothA2DP 23 | ] 24 | 25 | /* 26 | This mode is intended for Voice over IP (VoIP) apps and can only be used with the playAndRecord category. When this mode is used, the device’s tonal equalization is optimized for voice and the set of allowable audio routes is reduced to only those appropriate for voice chat. 27 | 28 | See: https://developer.apple.com/documentation/avfoundation/avaudiosession/mode/1616455-voicechat 29 | */ 30 | static private var audioMode = AVAudioSession.Mode.voiceChat 31 | static private var audioModeDefault : AVAudioSession.Mode = AVAudioSession.Mode.default 32 | 33 | static private var audioInputSelected: AVAudioSessionPortDescription? = nil 34 | 35 | // 36 | // Audio Input 37 | // 38 | 39 | static func initAudioDevices() -> Void { 40 | 41 | iRTCAudioController.setCategory() 42 | 43 | do { 44 | let audioSession: AVAudioSession = AVAudioSession.sharedInstance() 45 | try audioSession.setActive(true) 46 | } catch { 47 | print("Error messing with audio session: \(error)") 48 | } 49 | } 50 | 51 | static func setCategory() -> Void { 52 | // Enable speaker 53 | NSLog("iRTCAudioController#setCategory()") 54 | 55 | do { 56 | let audioSession: AVAudioSession = AVAudioSession.sharedInstance() 57 | try audioSession.setCategory( 58 | iRTCAudioController.audioCategory, 59 | mode: iRTCAudioController.audioMode, 60 | options: iRTCAudioController.audioCategoryOptions 61 | ) 62 | } catch { 63 | NSLog("iRTCAudioController#setCategory() | ERROR \(error)") 64 | }; 65 | } 66 | 67 | // Setter function inserted by save specific audio device 68 | static func saveInputAudioDevice(inputDeviceUID: String) -> Void { 69 | let audioSession: AVAudioSession = AVAudioSession.sharedInstance() 70 | if let audioInput: AVAudioSessionPortDescription = audioSession.availableInputs!.first(where: { $0.uid == inputDeviceUID }) { 71 | iRTCAudioController.audioInputSelected = audioInput 72 | } else { 73 | NSLog("iRTCAudioController#saveInputAudioDevice() | ERROR invalid deviceId \(inputDeviceUID)") 74 | iRTCAudioController.audioInputSelected = audioSession.availableInputs!.first 75 | } 76 | } 77 | 78 | // Setter function inserted by set specific audio device 79 | static func restoreInputOutputAudioDevice() -> Void { 80 | let audioSession: AVAudioSession = AVAudioSession.sharedInstance() 81 | 82 | do { 83 | try audioSession.setPreferredInput(iRTCAudioController.audioInputSelected) 84 | } catch { 85 | NSLog("iRTCAudioController:restoreInputOutputAudioDevice: Error setting audioSession preferred input.") 86 | } 87 | 88 | iRTCAudioController.setOutputSpeakerIfNeed(enabled: speakerEnabled); 89 | } 90 | 91 | static func setOutputSpeakerIfNeed(enabled: Bool) { 92 | 93 | speakerEnabled = enabled 94 | 95 | let audioSession: AVAudioSession = AVAudioSession.sharedInstance() 96 | let currentRoute = audioSession.currentRoute 97 | 98 | if currentRoute.outputs.count != 0 { 99 | for description in currentRoute.outputs { 100 | if ( 101 | description.portType == AVAudioSession.Port.headphones || 102 | description.portType == AVAudioSession.Port.bluetoothA2DP || 103 | description.portType == AVAudioSession.Port.carAudio || 104 | description.portType == AVAudioSession.Port.airPlay || 105 | description.portType == AVAudioSession.Port.lineOut 106 | ) { 107 | NSLog("iRTCAudioController#setOutputSpeakerIfNeed() | external audio output plugged in -> do nothing") 108 | } else { 109 | NSLog("iRTCAudioController#setOutputSpeakerIfNeed() | external audio pulled out") 110 | 111 | if (speakerEnabled) { 112 | do { 113 | try audioSession.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker) 114 | } catch { 115 | NSLog("iRTCAudioController#setOutputSpeakerIfNeed() | ERROR \(error)") 116 | }; 117 | } 118 | } 119 | } 120 | } else { 121 | NSLog("iRTCAudioController#setOutputSpeakerIfNeed() | requires connection to device") 122 | } 123 | } 124 | 125 | static func selectAudioOutputSpeaker() { 126 | // Enable speaker 127 | NSLog("iRTCAudioController#selectAudioOutputSpeaker()") 128 | 129 | speakerEnabled = true; 130 | 131 | setCategory() 132 | 133 | do { 134 | let audioSession: AVAudioSession = AVAudioSession.sharedInstance() 135 | try audioSession.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker) 136 | } catch { 137 | NSLog("iRTCAudioController#selectAudioOutputSpeaker() | ERROR \(error)") 138 | }; 139 | } 140 | 141 | static func selectAudioOutputEarpiece() { 142 | // Disable speaker, switched to default 143 | NSLog("iRTCAudioController#selectAudioOutputEarpiece()") 144 | 145 | speakerEnabled = false; 146 | 147 | setCategory() 148 | 149 | do { 150 | let audioSession: AVAudioSession = AVAudioSession.sharedInstance() 151 | try audioSession.overrideOutputAudioPort(AVAudioSession.PortOverride.none) 152 | } catch { 153 | NSLog("iRTCAudioController#selectAudioOutputEarpiece() | ERROR \(error)") 154 | }; 155 | } 156 | 157 | // 158 | // Audio Output 159 | // 160 | 161 | static private var speakerEnabled: Bool = false 162 | 163 | init() { 164 | let shouldManualInit = Bundle.main.object(forInfoDictionaryKey: "ManualInitAudioDevice") as? String 165 | 166 | if(shouldManualInit == "FALSE") { 167 | iRTCAudioController.initAudioDevices() 168 | } 169 | 170 | NotificationCenter.default.addObserver( 171 | self, 172 | selector: #selector(self.audioRouteChangeListener(_:)), 173 | name: AVAudioSession.routeChangeNotification, 174 | object: nil) 175 | } 176 | 177 | @objc dynamic fileprivate func audioRouteChangeListener(_ notification:Notification) { 178 | let audioRouteChangeReason = notification.userInfo![AVAudioSessionRouteChangeReasonKey] as! UInt 179 | 180 | switch audioRouteChangeReason { 181 | case AVAudioSession.RouteChangeReason.newDeviceAvailable.rawValue: 182 | NSLog("iRTCAudioController#audioRouteChangeListener() | headphone plugged in") 183 | case AVAudioSession.RouteChangeReason.oldDeviceUnavailable.rawValue: 184 | NSLog("iRTCAudioController#audioRouteChangeListener() | headphone pulled out -> restore state speakerEnabled: %@", iRTCAudioController.speakerEnabled ? "true" : "false") 185 | iRTCAudioController.setOutputSpeakerIfNeed(enabled: iRTCAudioController.speakerEnabled) 186 | default: 187 | break 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/RTCIceCandidate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the RTCIceCandidate class. 12 | */ 13 | module.exports = RTCIceCandidate; 14 | 15 | 16 | 17 | /** 18 | * RFC-5245: http://tools.ietf.org/html/rfc5245#section-15.1 19 | * 20 | * candidate-attribute = "candidate" ":" foundation SP component-id SP 21 | transport SP 22 | priority SP 23 | connection-address SP ;from RFC 4566 24 | port ;port from RFC 4566 25 | SP cand-type 26 | [SP rel-addr] 27 | [SP rel-port] 28 | *(SP extension-att-name SP 29 | extension-att-value) 30 | * 31 | * foundation = 1*32ice-char 32 | * component-id = 1*5DIGIT 33 | * transport = "UDP" / transport-extension 34 | * transport-extension = token ; from RFC 3261 35 | * priority = 1*10DIGIT 36 | * cand-type = "typ" SP candidate-types 37 | * candidate-types = "host" / "srflx" / "prflx" / "relay" / token 38 | * rel-addr = "raddr" SP connection-address 39 | * rel-port = "rport" SP port 40 | * extension-att-name = byte-string ;from RFC 4566 41 | * extension-att-value = byte-string 42 | * ice-char = ALPHA / DIGIT / "+" / "/" 43 | */ 44 | 45 | /** 46 | * RFC-3261: https://tools.ietf.org/html/rfc3261#section-25.1 47 | * 48 | * token = 1*(alphanum / "-" / "." / "!" / "%" / "*" 49 | / "_" / "+" / "`" / "'" / "~" ) 50 | */ 51 | 52 | /* 53 | * RFC-4566: https://tools.ietf.org/html/rfc4566#section-9 54 | * 55 | * port = 1*DIGIT 56 | * IP4-address = b1 3("." decimal-uchar) 57 | * b1 = decimal-uchar 58 | ; less than "224" 59 | * ; The following is consistent with RFC 2373 [30], Appendix B. 60 | * IP6-address = hexpart [ ":" IP4-address ] 61 | * hexpart = hexseq / hexseq "::" [ hexseq ] / 62 | "::" [ hexseq ] 63 | * hexseq = hex4 *( ":" hex4) 64 | * hex4 = 1*4HEXDIG 65 | * decimal-uchar = DIGIT 66 | / POS-DIGIT DIGIT 67 | / ("1" 2*(DIGIT)) 68 | / ("2" ("0"/"1"/"2"/"3"/"4") DIGIT) 69 | / ("2" "5" ("0"/"1"/"2"/"3"/"4"/"5")) 70 | */ 71 | 72 | var candidateToJson = (function () { 73 | var candidateFieldName = { 74 | FOUNDATION: 'foundation', 75 | COMPONENT_ID: 'componentId', 76 | TRANSPORT: 'transport', 77 | PRIORITY: 'priority', 78 | CONNECTION_ADDRESS: 'connectionAddress', 79 | PORT: 'port', 80 | CANDIDATE_TYPE: 'candidateType', 81 | REMOTE_CANDIDATE_ADDRESS: 'remoteConnectionAddress', 82 | REMOTE_CANDIDATE_PORT: 'remotePort' 83 | }; 84 | 85 | var candidateType = { 86 | HOST: 'host', 87 | SRFLX: 'srflx', 88 | PRFLX: 'prflx', 89 | RELAY: 'relay' 90 | }; 91 | 92 | var transport = { 93 | TCP: 'TCP', 94 | UDP: 'UDP' 95 | }; 96 | 97 | var IPV4SEG = '(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])'; 98 | var IPV4ADDR = `(?:${IPV4SEG}\\.){3}${IPV4SEG}`; 99 | var IPV6SEG = '[0-9a-fA-F]{1,4}'; 100 | var IPV6ADDR = 101 | `(?:${IPV6SEG}:){7,7}${IPV6SEG}|` + // 1:2:3:4:5:6:7:8 102 | `(?:${IPV6SEG}:){1,7}:|` + // 1:: 1:2:3:4:5:6:7:: 103 | `(?:${IPV6SEG}:){1,6}:${IPV6SEG}|` + // 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 104 | `(?:${IPV6SEG}:){1,5}(?::${IPV6SEG}){1,2}|` + // 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 105 | `(?:${IPV6SEG}:){1,4}(?::${IPV6SEG}){1,3}|` + // 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 106 | `(?:${IPV6SEG}:){1,3}(?::${IPV6SEG}){1,4}|` + // 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 107 | `(?:${IPV6SEG}:){1,2}(?::${IPV6SEG}){1,5}|` + // 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 108 | `${IPV6SEG}:(?:(?::${IPV6SEG}){1,6})|` + // 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 109 | `:(?:(?::${IPV6SEG}){1,7}|:)|` + // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: 110 | `fe80:(?::${IPV6SEG}){0,4}%[0-9a-zA-Z]{1,}|` + // fe80::7:8%eth0 fe80::7:8%1 (link-local IPv6 addresses with zone index) 111 | `::(?:ffff(?::0{1,4}){0,1}:){0,1}${IPV4ADDR}|` + // ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses) 112 | `(?:${IPV6SEG}:){1,4}:${IPV4ADDR}`; // 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address) 113 | 114 | var TOKEN = "[0-9a-zA-Z\\-\\.!\\%\\*_\\+\\`\\'\\~]+"; 115 | 116 | var CANDIDATE_TYPE = ''; 117 | Object.keys(candidateType).forEach(function (key) { 118 | CANDIDATE_TYPE += candidateType[key] + '|'; 119 | }); 120 | CANDIDATE_TYPE += TOKEN; 121 | 122 | var pattern = { 123 | COMPONENT_ID: '[0-9]{1,5}', 124 | FOUNDATION: '[a-zA-Z0-9\\+\\/\\-]+', 125 | PRIORITY: '[0-9]{1,10}', 126 | TRANSPORT: transport.UDP + '|' + TOKEN, 127 | CONNECTION_ADDRESS: IPV4ADDR + '|' + IPV6ADDR, 128 | PORT: '[0-9]{1,5}', 129 | CANDIDATE_TYPE: CANDIDATE_TYPE 130 | }; 131 | 132 | return function candidateToJson(iceCandidate) { 133 | var iceCandidateJson = null; 134 | 135 | if (iceCandidate && typeof iceCandidate === 'string') { 136 | var ICE_CANDIDATE_PATTERN = new RegExp( 137 | `candidate:(${pattern.FOUNDATION})` + // 10 138 | `\\s(${pattern.COMPONENT_ID})` + // 1 139 | `\\s(${pattern.TRANSPORT})` + // UDP 140 | `\\s(${pattern.PRIORITY})` + // 1845494271 141 | `\\s(${pattern.CONNECTION_ADDRESS})` + // 13.93.107.159 142 | `\\s(${pattern.PORT})` + // 53705 143 | '\\s' + 144 | 'typ' + 145 | `\\s(${pattern.CANDIDATE_TYPE})` + // typ prflx 146 | '(?:\\s' + 147 | 'raddr' + 148 | `\\s(${pattern.CONNECTION_ADDRESS})` + // raddr 10.1.221.7 149 | '\\s' + 150 | 'rport' + 151 | `\\s(${pattern.PORT}))?` // rport 54805 152 | ); 153 | 154 | var iceCandidateFields = iceCandidate.match(ICE_CANDIDATE_PATTERN); 155 | if (iceCandidateFields) { 156 | iceCandidateJson = {}; 157 | Object.keys(candidateFieldName).forEach(function (key, i) { 158 | // i+1 because match returns the entire match result 159 | // and the parentheses-captured matched results. 160 | if (iceCandidateFields.length > i + 1 && iceCandidateFields[i + 1]) { 161 | iceCandidateJson[candidateFieldName[key]] = iceCandidateFields[i + 1]; 162 | } 163 | }); 164 | } 165 | } 166 | 167 | return iceCandidateJson; 168 | }; 169 | })(); 170 | 171 | // See https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/RTCIceCandidate 172 | function RTCIceCandidate(data) { 173 | data = data || {}; 174 | 175 | // Public atributes. 176 | this.sdpMid = data.sdpMid; 177 | this.sdpMLineIndex = data.sdpMLineIndex; 178 | this.candidate = data.candidate; 179 | 180 | // Parse candidate SDP: 181 | // Example: candidate:1829696681 1 udp 2122262783 2a01:cb05:8d3e:a300:e1ad:79c1:7096:8ba0 49778 typ host generation 0 ufrag c9L6 network-id 2 network-cost 10 182 | var iceCandidateFields = candidateToJson(this.candidate); 183 | if (iceCandidateFields) { 184 | this.foundation = iceCandidateFields.foundation; 185 | this.component = iceCandidateFields.componentId; 186 | this.priority = iceCandidateFields.priority; 187 | this.type = iceCandidateFields.candidateType; 188 | 189 | this.address = iceCandidateFields.connectionAddress; 190 | this.ip = iceCandidateFields.connectionAddress; 191 | this.protocol = iceCandidateFields.transport; 192 | this.port = iceCandidateFields.port; 193 | 194 | this.relatedAddress = iceCandidateFields.remoteConnectionAddress || null; 195 | this.relatedPort = iceCandidateFields.remotePort || null; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/RTCDataChannel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the RTCDataChannel class. 12 | */ 13 | module.exports = RTCDataChannel; 14 | 15 | 16 | /** 17 | * Dependencies. 18 | */ 19 | var 20 | debug = require('debug')('iosrtc:RTCDataChannel'), 21 | debugerror = require('debug')('iosrtc:ERROR:RTCDataChannel'), 22 | exec = require('./IOSExec'), 23 | randomNumber = require('random-number').generator({min: 10000, max: 99999, integer: true}), 24 | EventTarget = require('./EventTarget'); 25 | 26 | 27 | debugerror.log = console.warn.bind(console); 28 | 29 | 30 | function RTCDataChannel(peerConnection, label, options, dataFromEvent) { 31 | var self = this; 32 | 33 | // Make this an EventTarget. 34 | EventTarget.call(this); 35 | 36 | // Created via pc.createDataChannel(). 37 | if (!dataFromEvent) { 38 | debug('new() | [label:%o, options:%o]', label, options); 39 | 40 | if (typeof label !== 'string') { 41 | label = ''; 42 | } 43 | 44 | options = options || {}; 45 | 46 | if (options.hasOwnProperty('maxPacketLifeTime') && options.hasOwnProperty('maxRetransmits')) { 47 | throw new SyntaxError('both maxPacketLifeTime and maxRetransmits can not be present'); 48 | } 49 | 50 | if (options.hasOwnProperty('id')) { 51 | if (typeof options.id !== 'number' || isNaN(options.id) || options.id < 0) { 52 | throw new SyntaxError('id must be a number'); 53 | } 54 | // TODO: 55 | // https://code.google.com/p/webrtc/issues/detail?id=4618 56 | if (options.id > 1023) { 57 | throw new SyntaxError('id cannot be greater than 1023 (https://code.google.com/p/webrtc/issues/detail?id=4614)'); 58 | } 59 | } 60 | 61 | // Public atributes. 62 | this.label = label; 63 | this.ordered = options.hasOwnProperty('ordered') ? !!options.ordered : true; 64 | this.maxPacketLifeTime = options.hasOwnProperty('maxPacketLifeTime') ? Number(options.maxPacketLifeTime) : null; 65 | this.maxRetransmits = options.hasOwnProperty('maxRetransmits') ? Number(options.maxRetransmits) : null; 66 | this.protocol = options.hasOwnProperty('protocol') ? String(options.protocol) : ''; 67 | this.negotiated = options.hasOwnProperty('negotiated') ? !!options.negotiated : false; 68 | this.id = options.hasOwnProperty('id') ? Number(options.id) : undefined; 69 | this.readyState = 'connecting'; 70 | this.bufferedAmount = 0; 71 | this.bufferedAmountLowThreshold = 0; 72 | 73 | // Private attributes. 74 | this.peerConnection = peerConnection; 75 | this.dcId = randomNumber(); 76 | 77 | exec.execNative(onResultOK, null, 'WKWebViewRTC', 'RTCPeerConnection_createDataChannel', [this.peerConnection.pcId, this.dcId, label, options]); 78 | // Created via pc.ondatachannel. 79 | } else { 80 | debug('new() | [dataFromEvent:%o]', dataFromEvent); 81 | 82 | // Public atributes. 83 | this.label = dataFromEvent.label; 84 | this.ordered = dataFromEvent.ordered; 85 | this.maxPacketLifeTime = dataFromEvent.maxPacketLifeTime; 86 | this.maxRetransmits = dataFromEvent.maxRetransmits; 87 | this.protocol = dataFromEvent.protocol; 88 | this.negotiated = dataFromEvent.negotiated; 89 | this.id = dataFromEvent.id; 90 | this.readyState = dataFromEvent.readyState; 91 | this.bufferedAmount = dataFromEvent.bufferedAmount; 92 | this.bufferedAmountLowThreshold = dataFromEvent.bufferedAmountLowThreshold; 93 | 94 | // Private attributes. 95 | this.peerConnection = peerConnection; 96 | this.dcId = dataFromEvent.dcId; 97 | 98 | exec.execNative(onResultOK, null, 'WKWebViewRTC', 'RTCPeerConnection_RTCDataChannel_setListener', [this.peerConnection.pcId, this.dcId]); 99 | } 100 | 101 | function onResultOK(data) { 102 | if (data.type) { 103 | onEvent.call(self, data); 104 | // Special handler for received binary mesage. 105 | } else { 106 | onEvent.call(self, { 107 | type: 'message', 108 | message: data 109 | }); 110 | } 111 | } 112 | } 113 | 114 | RTCDataChannel.prototype = Object.create(EventTarget.prototype); 115 | RTCDataChannel.prototype.constructor = RTCDataChannel; 116 | 117 | // Just 'arraybuffer' binaryType is implemented in Chromium. 118 | Object.defineProperty(RTCDataChannel.prototype, 'binaryType', { 119 | get: function () { 120 | return 'arraybuffer'; 121 | }, 122 | set: function (type) { 123 | if (type !== 'arraybuffer') { 124 | throw new Error('just "arraybuffer" is implemented for binaryType'); 125 | } 126 | } 127 | }); 128 | 129 | 130 | RTCDataChannel.prototype.send = function (data) { 131 | if (isClosed.call(this) || this.readyState !== 'open') { 132 | return; 133 | } 134 | 135 | debug('send() | [data:%o]', data); 136 | 137 | if (!data) { 138 | return; 139 | } 140 | 141 | if (typeof data === 'string' || data instanceof String) { 142 | exec.execNative(null, null, 'WKWebViewRTC', 'RTCPeerConnection_RTCDataChannel_sendString', [this.peerConnection.pcId, this.dcId, data]); 143 | } else if (window.ArrayBuffer && data instanceof window.ArrayBuffer) { 144 | exec.execNative(null, null, 'WKWebViewRTC', 'RTCPeerConnection_RTCDataChannel_sendBinary', [this.peerConnection.pcId, this.dcId, data]); 145 | } else if ( 146 | (window.Int8Array && data instanceof window.Int8Array) || 147 | (window.Uint8Array && data instanceof window.Uint8Array) || 148 | (window.Uint8ClampedArray && data instanceof window.Uint8ClampedArray) || 149 | (window.Int16Array && data instanceof window.Int16Array) || 150 | (window.Uint16Array && data instanceof window.Uint16Array) || 151 | (window.Int32Array && data instanceof window.Int32Array) || 152 | (window.Uint32Array && data instanceof window.Uint32Array) || 153 | (window.Float32Array && data instanceof window.Float32Array) || 154 | (window.Float64Array && data instanceof window.Float64Array) || 155 | (window.DataView && data instanceof window.DataView) 156 | ) { 157 | exec.execNative(null, null, 'WKWebViewRTC', 'RTCPeerConnection_RTCDataChannel_sendBinary', [this.peerConnection.pcId, this.dcId, data.buffer]); 158 | } else { 159 | throw new Error('invalid data type'); 160 | } 161 | }; 162 | 163 | 164 | RTCDataChannel.prototype.close = function () { 165 | if (isClosed.call(this)) { 166 | return; 167 | } 168 | 169 | debug('close()'); 170 | 171 | this.readyState = 'closing'; 172 | 173 | exec.execNative(null, null, 'WKWebViewRTC', 'RTCPeerConnection_RTCDataChannel_close', [this.peerConnection.pcId, this.dcId]); 174 | }; 175 | 176 | 177 | /** 178 | * Private API. 179 | */ 180 | 181 | 182 | function isClosed() { 183 | return this.readyState === 'closed' || this.readyState === 'closing' || this.peerConnection.signalingState === 'closed'; 184 | } 185 | 186 | 187 | function onEvent(data) { 188 | var type = data.type, 189 | event; 190 | 191 | debug('onEvent() | [type:%s, data:%o]', type, data); 192 | 193 | switch (type) { 194 | case 'new': 195 | // Update properties and exit without firing the event. 196 | this.ordered = data.channel.ordered; 197 | this.maxPacketLifeTime = data.channel.maxPacketLifeTime; 198 | this.maxRetransmits = data.channel.maxRetransmits; 199 | this.protocol = data.channel.protocol; 200 | this.negotiated = data.channel.negotiated; 201 | this.id = data.channel.id; 202 | this.readyState = data.channel.readyState; 203 | this.bufferedAmount = data.channel.bufferedAmount; 204 | break; 205 | 206 | case 'statechange': 207 | this.readyState = data.readyState; 208 | 209 | switch (data.readyState) { 210 | case 'connecting': 211 | break; 212 | case 'open': 213 | this.dispatchEvent(new Event('open')); 214 | break; 215 | case 'closing': 216 | break; 217 | case 'closed': 218 | this.dispatchEvent(new Event('close')); 219 | break; 220 | } 221 | break; 222 | 223 | case 'message': 224 | event = new Event('message'); 225 | event.data = data.message; 226 | this.dispatchEvent(event); 227 | break; 228 | 229 | case 'bufferedamount': 230 | this.bufferedAmount = data.bufferedAmount; 231 | 232 | if (this.bufferedAmountLowThreshold > 0 && this.bufferedAmountLowThreshold > this.bufferedAmount) { 233 | event = new Event('bufferedamountlow'); 234 | event.bufferedAmount = this.bufferedAmount; 235 | this.dispatchEvent(event); 236 | } 237 | 238 | break; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iRTCPeerConnectionConfig.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | import WebRTC 12 | 13 | class iRTCPeerConnectionConfig { 14 | 15 | fileprivate let allowedSdpSemantics = [ 16 | "plan-b": RTCSdpSemantics.planB, 17 | "unified-plan": RTCSdpSemantics.unifiedPlan 18 | ] 19 | 20 | fileprivate let allowedBundlePolicy = [ 21 | "balanced": RTCBundlePolicy.balanced, 22 | "max-compat": RTCBundlePolicy.maxCompat, 23 | "max-bundle": RTCBundlePolicy.maxBundle 24 | ] 25 | 26 | fileprivate let allowedIceTransportPolicy = [ 27 | "all": RTCIceTransportPolicy.all, 28 | "relay": RTCIceTransportPolicy.relay 29 | ] 30 | 31 | fileprivate let allowedRtcpMuxPolicy = [ 32 | "require": RTCRtcpMuxPolicy.require, 33 | "negotiate": RTCRtcpMuxPolicy.negotiate, 34 | ] 35 | 36 | fileprivate var rtcConfiguration: RTCConfiguration 37 | 38 | init(pcConfig: NSDictionary?) { 39 | NSLog("iRTCPeerConnectionConfig#init()") 40 | 41 | self.rtcConfiguration = RTCConfiguration() 42 | 43 | /* 44 | Since Chrome 65, this has been an experimental feature that you can opt-in to by passing 45 | sdpSemantics:'unified-plan' to the RTCPeerConnection constructor, with transceivers added in M69. 46 | History: https://crbug.com/799030. Canary/Dev experiments making Unified Plan the default behavior started in 71. 47 | The target is to release Unified Plan by default in 72 Stable. For more information, see https://webrtc.org/web-apis/chrome/unified-plan/. 48 | See: https://www.chromestatus.com/feature/5723303167655936 49 | */ 50 | 51 | let sdpSemanticsConfig = pcConfig?.object(forKey: "sdpSemantics") as? String; 52 | let sdpSemantics = (sdpSemanticsConfig != nil && allowedSdpSemantics[sdpSemanticsConfig!] != nil) ? 53 | allowedSdpSemantics[sdpSemanticsConfig!] : RTCSdpSemantics.planB 54 | 55 | rtcConfiguration.sdpSemantics = sdpSemantics!; 56 | 57 | /* 58 | Specifies how to handle negotiation of candidates when the remote peer is not compatible with the SDP BUNDLE standard. 59 | This must be one of the values from the enum RTCBundlePolicy. If this value isn't included in the dictionary, "balanced" is assumed. 60 | 61 | "balanced" On BUNDLE-aware connections, the ICE agent should gather candidates for all of the media types in use (audio, video, and data). Otherwise, the ICE agent should only negotiate one audio and video track on separate transports. 62 | "max-compat" The ICE agent should gather candidates for each track, using separate transports to negotiate all media tracks for connections which aren't BUNDLE-compatible. 63 | "max-bundle" The ICE agent should gather candidates for just one track. If the connection isn't BUNDLE-compatible, then the ICE agent should negotiate just one media track. 64 | */ 65 | 66 | let bundlePolicyConfig = pcConfig?.object(forKey: "bundlePolicy") as? String; 67 | let bundlePolicy = (bundlePolicyConfig != nil && allowedBundlePolicy[bundlePolicyConfig!] != nil) ? 68 | allowedBundlePolicy[bundlePolicyConfig!] : RTCBundlePolicy.balanced 69 | 70 | rtcConfiguration.bundlePolicy = bundlePolicy!; 71 | 72 | /* 73 | An Array of objects of type RTCCertificate which are used by the connection for authentication. 74 | If this property isn't specified, a set of certificates is generated automatically for each RTCPeerConnection instance. 75 | Although only one certificate is used by a given connection, providing certificates for multiple algorithms may improve 76 | the odds of successfully connecting in some circumstances. See Using certificates below for additional information. 77 | This configuration option cannot be changed after it is first specified; once the certificates have been set, 78 | this property is ignored in future calls to RTCPeerConnection.setConfiguration(). 79 | */ 80 | // TODO certificates 81 | 82 | /* 83 | An unsigned 16-bit integer value which specifies the size of the prefetched ICE candidate pool. 84 | The default value is 0 (meaning no candidate prefetching will occur). 85 | You may find in some cases that connections can be established more quickly by allowing the ICE agent 86 | to start fetching ICE candidates before you start trying to connect, so that they're already available 87 | for inspection when RTCPeerConnection.setLocalDescription() is called. 88 | Changing the size of the ICE candidate pool may trigger the beginning of ICE gathering. 89 | */ 90 | 91 | let iceCandidatePoolSizeConfig = pcConfig?.object(forKey: "iceCandidatePoolSize") as? Int32; 92 | rtcConfiguration.iceCandidatePoolSize = iceCandidatePoolSizeConfig != nil ? iceCandidatePoolSizeConfig! : 0; 93 | 94 | /* 95 | The current ICE transport policy; this must be one of the values from the RTCIceTransportPolicy enum. 96 | If this isn't specified, "all" is assumed. 97 | 98 | - "all" All ICE candidates will be considered. 99 | - "public" Only ICE candidates with public IP addresses will be considered. Removed from the specification's May 13, 2016 working draft 100 | - "relay" Only ICE candidates whose IP addresses are being relayed, such as those being passed through a TURN server, will be considered. 101 | */ 102 | 103 | let iceTransportPolicyConfig = pcConfig?.object(forKey: "iceTransportPolicy") as? String; 104 | let iceTransportPolicy = (iceTransportPolicyConfig != nil && allowedIceTransportPolicy[iceTransportPolicyConfig!] != nil) ? 105 | allowedIceTransportPolicy[iceTransportPolicyConfig!] : RTCIceTransportPolicy.all 106 | 107 | rtcConfiguration.iceTransportPolicy = iceTransportPolicy!; 108 | 109 | /* 110 | The RTCP mux policy to use when gathering ICE candidates, in order to support non-multiplexed RTCP. 111 | The value must be one of those from the RTCRtcpMuxPolicy enum. The default is "require". 112 | 113 | - "negotiate" Instructs the ICE agent to gather both RTP and RTCP candidates. If the remote peer can multiplex RTCP, 114 | then RTCP candidates are multiplexed atop the corresponding RTP candidates. Otherwise, both the RTP and RTCP candidates are returned, separately. 115 | - "require" Tells the ICE agent to gather ICE candidates for only RTP, and to multiplex RTCP atop them. 116 | If the remote peer doesn't support RTCP multiplexing, then session negotiation fails. 117 | */ 118 | 119 | let rtcpMuxPolicyConfig = pcConfig?.object(forKey: "rtcpMuxPolicy") as? String; 120 | let rtcpMuxPolicy = (rtcpMuxPolicyConfig != nil && allowedRtcpMuxPolicy[rtcpMuxPolicyConfig!] != nil) ? 121 | allowedRtcpMuxPolicy[rtcpMuxPolicyConfig!] : RTCRtcpMuxPolicy.require 122 | 123 | rtcConfiguration.rtcpMuxPolicy = rtcpMuxPolicy!; 124 | 125 | /* 126 | A DOMString which specifies the target peer identity for the RTCPeerConnection. 127 | If this value is set (it defaults to null), the RTCPeerConnection will not connect to a remote peer 128 | unless it can successfully authenticate with the given name. 129 | */ 130 | // TODO peerIdentity 131 | 132 | /* 133 | An array of RTCIceServer objects, each describing one server which may be used by the ICE agent; 134 | these are typically STUN and/or TURN servers. If this isn't specified, the connection attempt 135 | will be made with no STUN or TURN server available, which limits the connection to local peers. 136 | */ 137 | let iceServers = pcConfig?.object(forKey: "iceServers") as? [NSDictionary] 138 | 139 | if iceServers != nil { 140 | for iceServer: NSDictionary in iceServers! { 141 | let urlsConfig = (iceServer.object(forKey: "url") != nil ? 142 | iceServer.object(forKey: "url") : iceServer.object(forKey: "urls")) 143 | 144 | let urls = urlsConfig is String ? [urlsConfig as? String ?? ""] : urlsConfig as? [String] ?? nil; 145 | let username = iceServer.object(forKey: "username") as? String ?? "" 146 | let password = iceServer.object(forKey: "credential") as? String ?? "" 147 | 148 | if (urls != nil && urls!.count > 0) { 149 | NSLog("iRTCPeerConnectionConfig#init() | adding ICE server [url:'%@', username:'%@', password:'******']", 150 | String(urls![0]), String(username)) 151 | 152 | self.rtcConfiguration.iceServers.append(RTCIceServer( 153 | urlStrings: urls!, 154 | username: username, 155 | credential: password 156 | )) 157 | } 158 | } 159 | } 160 | } 161 | 162 | deinit { 163 | NSLog("iRTCPeerConnectionConfig#deinit()") 164 | } 165 | 166 | func getConfiguration() -> RTCConfiguration { 167 | NSLog("iRTCPeerConnectionConfig#getConfiguration()") 168 | 169 | return self.rtcConfiguration 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/iosrtc.js: -------------------------------------------------------------------------------- 1 | // 2 | // iosrtc.js 3 | // WKWebViewRTC 4 | // 5 | // Created by Open Telecom Foundation on 2020/6/30. 6 | // Copyright © 2020 Open Telecom Foundation. All rights reserved. 7 | // The MIT License (MIT) 8 | // 9 | 10 | /** 11 | * Variables. 12 | */ 13 | 14 | var 15 | // Dictionary of MediaStreamRenderers. 16 | // - key: MediaStreamRenderer id. 17 | // - value: MediaStreamRenderer. 18 | mediaStreamRenderers = {}, 19 | 20 | // Dictionary of MediaStreams. 21 | // - key: MediaStream blobId. 22 | // - value: MediaStream. 23 | mediaStreams = {}, 24 | 25 | 26 | /** 27 | * Dependencies. 28 | */ 29 | debug = require('debug')('iosrtc'), 30 | exec = require('./IOSExec'), 31 | domready = require('domready'), 32 | getUserMedia = require('./getUserMedia'), 33 | enumerateDevices = require('./enumerateDevices'), 34 | RTCPeerConnection = require('./RTCPeerConnection'), 35 | RTCSessionDescription = require('./RTCSessionDescription'), 36 | RTCIceCandidate = require('./RTCIceCandidate'), 37 | MediaDevices = require('./MediaDevices'), 38 | MediaStream = require('./MediaStream'), 39 | MediaStreamTrack = require('./MediaStreamTrack'), 40 | videoElementsHandler = require('./videoElementsHandler'); 41 | 42 | 43 | /** 44 | * Expose the iosrtc object. 45 | */ 46 | module.exports = { 47 | // Expose WebRTC classes and functions. 48 | getUserMedia: getUserMedia, 49 | enumerateDevices: enumerateDevices, 50 | getMediaDevices: enumerateDevices, // TMP 51 | RTCPeerConnection: RTCPeerConnection, 52 | RTCSessionDescription: RTCSessionDescription, 53 | RTCIceCandidate: RTCIceCandidate, 54 | MediaDevices: MediaDevices, 55 | MediaStream: MediaStream, 56 | MediaStreamTrack: MediaStreamTrack, 57 | 58 | // Expose a function to refresh current videos rendering a MediaStream. 59 | refreshVideos: videoElementsHandler.refreshVideos, 60 | 61 | // Expose a function to handle a video not yet inserted in the DOM. 62 | observeVideo: videoElementsHandler.observeVideo, 63 | 64 | // Select audio output (earpiece or speaker). 65 | selectAudioOutput: selectAudioOutput, 66 | 67 | // turnOnSpeaker with options 68 | turnOnSpeaker: turnOnSpeaker, 69 | 70 | // Checking permision (audio and camera) 71 | requestPermission: requestPermission, 72 | 73 | // Expose a function to initAudioDevices if needed, sets the audio session active 74 | initAudioDevices: initAudioDevices, 75 | 76 | // Expose a function to pollute window and naigator namespaces. 77 | registerGlobals: registerGlobals, 78 | 79 | // Expose the debug module. 80 | debug: require('debug'), 81 | 82 | // Debug function to see what happens internally. 83 | dump: dump, 84 | 85 | // Debug Stores to see what happens internally. 86 | mediaStreamRenderers: mediaStreamRenderers, 87 | mediaStreams: mediaStreams, 88 | nativeCallback: exec.nativeCallback 89 | }; 90 | 91 | // register global variables right after webview loading 92 | registerGlobals(); 93 | 94 | MediaStreamTrack.prototype.clone = function () { 95 | return this; 96 | }; 97 | 98 | domready(function () { 99 | // Let the MediaStream class and the videoElementsHandler share same MediaStreams container. 100 | MediaStream.setMediaStreams(mediaStreams); 101 | videoElementsHandler(mediaStreams, mediaStreamRenderers); 102 | 103 | // refreshVideos on device orientation change to resize peers video 104 | // while local video will resize du orientation change 105 | window.addEventListener('resize', function () { 106 | videoElementsHandler.refreshVideos(); 107 | }); 108 | }); 109 | 110 | function selectAudioOutput(output) { 111 | debug('selectAudioOutput() | [output:"%s"]', output); 112 | 113 | switch (output) { 114 | case 'earpiece': 115 | exec.execNative(null, null, 'WKWebViewRTC', 'selectAudioOutputEarpiece', []); 116 | break; 117 | case 'speaker': 118 | exec.execNative(null, null, 'WKWebViewRTC', 'selectAudioOutputSpeaker', []); 119 | break; 120 | default: 121 | throw new Error('output must be "earpiece" or "speaker"'); 122 | } 123 | } 124 | 125 | function turnOnSpeaker(isTurnOn) { 126 | debug('turnOnSpeaker() | [isTurnOn:"%s"]', isTurnOn); 127 | 128 | exec.execNative(null, null, 'WKWebViewRTC', "RTCTurnOnSpeaker", [isTurnOn]); 129 | } 130 | 131 | function requestPermission(needMic, needCamera, callback) { 132 | debug('requestPermission() | [needMic:"%s", needCamera:"%s"]', needMic, needCamera); 133 | 134 | function ok() { 135 | callback(true); 136 | } 137 | 138 | function error() { 139 | callback(false); 140 | } 141 | exec.execNative(ok, error, 'WKWebViewRTC', "RTCRequestPermission", [needMic, needCamera]); 142 | } 143 | 144 | function initAudioDevices() { 145 | debug('initAudioDevices()'); 146 | 147 | exec.execNative(null, null, 'WKWebViewRTC', "initAudioDevices", []); 148 | } 149 | 150 | function callbackifyMethod(originalMethod) { 151 | return function (arg) { // jshint ignore:line 152 | var success, failure, 153 | originalArgs = Array.prototype.slice.call(arguments); 154 | 155 | var callbackArgs = []; 156 | originalArgs.forEach(function (arg) { 157 | if (typeof arg === 'function') { 158 | if (!success) { 159 | success = arg; 160 | } else { 161 | failure = arg; 162 | } 163 | } else { 164 | callbackArgs.push(arg); 165 | } 166 | }); 167 | 168 | var promiseResult = originalMethod.apply(this, callbackArgs); 169 | 170 | // Only apply then if callback success available 171 | if (typeof success === 'function') { 172 | promiseResult = promiseResult.then(success); 173 | } 174 | 175 | // Only apply catch if callback failure available 176 | if (typeof failure === 'function') { 177 | promiseResult = promiseResult.catch(failure); 178 | } 179 | 180 | return promiseResult; 181 | }; 182 | } 183 | 184 | function callbackifyPrototype(proto, method) { 185 | var originalMethod = proto[method]; 186 | proto[method] = callbackifyMethod(originalMethod); 187 | } 188 | 189 | function restoreCallbacksSupport() { 190 | debug('restoreCallbacksSupport()'); 191 | getUserMedia = callbackifyMethod(getUserMedia); 192 | enumerateDevices = callbackifyMethod(enumerateDevices); 193 | callbackifyPrototype(RTCPeerConnection.prototype, 'createAnswer'); 194 | callbackifyPrototype(RTCPeerConnection.prototype, 'createOffer'); 195 | callbackifyPrototype(RTCPeerConnection.prototype, 'setRemoteDescription'); 196 | callbackifyPrototype(RTCPeerConnection.prototype, 'setLocalDescription'); 197 | callbackifyPrototype(RTCPeerConnection.prototype, 'addIceCandidate'); 198 | callbackifyPrototype(RTCPeerConnection.prototype, 'getStats'); 199 | } 200 | 201 | function registerGlobals(doNotRestoreCallbacksSupport) { 202 | debug('registerGlobals()'); 203 | 204 | if (!global.navigator) { 205 | global.navigator = {}; 206 | } 207 | 208 | // Restore Callback support 209 | if (!doNotRestoreCallbacksSupport) { 210 | restoreCallbacksSupport(); 211 | } 212 | 213 | navigator.getUserMedia = getUserMedia; 214 | navigator.webkitGetUserMedia = getUserMedia; 215 | 216 | // Prevent WebRTC-adapter to overide navigator.mediaDevices after shim is applied since ios 14.3 217 | if (!(navigator.mediaDevices instanceof MediaDevices)) { 218 | Object.defineProperty( 219 | navigator, 220 | 'mediaDevices', 221 | { 222 | value: new MediaDevices(), 223 | writable: false 224 | }, 225 | { 226 | enumerable: false, 227 | configurable: false, 228 | writable: false, 229 | value: 'static' 230 | }); 231 | } 232 | 233 | window.RTCPeerConnection = RTCPeerConnection; 234 | window.webkitRTCPeerConnection = RTCPeerConnection; 235 | window.RTCSessionDescription = RTCSessionDescription; 236 | window.RTCIceCandidate = RTCIceCandidate; 237 | window.MediaStream = MediaStream; 238 | window.webkitMediaStream = MediaStream; 239 | window.MediaStreamTrack = MediaStreamTrack; 240 | 241 | // Apply CanvasRenderingContext2D.drawImage monkey patch 242 | var drawImage = CanvasRenderingContext2D.prototype.drawImage; 243 | CanvasRenderingContext2D.prototype.drawImage = function (arg) { 244 | var args = Array.prototype.slice.call(arguments); 245 | var context = this; 246 | if (arg instanceof HTMLVideoElement && arg.render) { 247 | arg.render.save(function (data) { 248 | var img = new window.Image(); 249 | img.addEventListener("load", function () { 250 | args.splice(0, 1, img); 251 | drawImage.apply(context, args); 252 | img.src = null; 253 | }); 254 | img.setAttribute("src", "data:image/jpg;base64," + data); 255 | }); 256 | } else { 257 | return drawImage.apply(context, args); 258 | } 259 | }; 260 | } 261 | 262 | function dump() { 263 | exec.execNative(null, null, 'WKWebViewRTC', 'dump', []); 264 | } 265 | -------------------------------------------------------------------------------- /WKWebViewRTC/Classes/iRTCDataChannel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | import Foundation 11 | import WebRTC 12 | 13 | // FIXME: comparison operators with optionals were removed from the Swift Standard Libary. 14 | // Consider refactoring the code to use the non-optional operators. 15 | fileprivate func < (lhs: T?, rhs: T?) -> Bool { 16 | switch (lhs, rhs) { 17 | case let (l?, r?): 18 | return l < r 19 | case (nil, _?): 20 | return true 21 | default: 22 | return false 23 | } 24 | } 25 | 26 | // FIXME: comparison operators with optionals were removed from the Swift Standard Libary. 27 | // Consider refactoring the code to use the non-optional operators. 28 | fileprivate func > (lhs: T?, rhs: T?) -> Bool { 29 | switch (lhs, rhs) { 30 | case let (l?, r?): 31 | return l > r 32 | default: 33 | return rhs < lhs 34 | } 35 | } 36 | 37 | class iRTCDataChannel : NSObject, RTCDataChannelDelegate { 38 | var rtcDataChannel: RTCDataChannel? 39 | var eventListener: ((_ data: NSDictionary) -> Void)? 40 | var eventListenerForBinaryMessage: ((_ data: Data) -> Void)? 41 | var lostStates = Array() 42 | var lostMessages = Array() 43 | 44 | /** 45 | * Constructor for pc.createDataChannel(). 46 | */ 47 | init( 48 | rtcPeerConnection: RTCPeerConnection, 49 | label: String, 50 | options: NSDictionary?, 51 | eventListener: @escaping (_ data: NSDictionary) -> Void, 52 | eventListenerForBinaryMessage: @escaping (_ data: Data) -> Void 53 | ) { 54 | NSLog("iRTCDataChannel#init()") 55 | 56 | self.eventListener = eventListener 57 | self.eventListenerForBinaryMessage = eventListenerForBinaryMessage 58 | 59 | let rtcDataChannelInit = RTCDataChannelConfiguration.init() 60 | 61 | if options?.object(forKey: "ordered") != nil { 62 | rtcDataChannelInit.isOrdered = options!.object(forKey: "ordered") as! Bool 63 | } 64 | 65 | if options?.object(forKey: "maxPacketLifeTime") != nil { 66 | // TODO: rtcDataChannel.maxRetransmitTime always reports 0. 67 | rtcDataChannelInit.maxRetransmitTimeMs = options!.object(forKey: "maxPacketLifeTime") as! Int 68 | } 69 | 70 | if options?.object(forKey: "maxRetransmits") != nil { 71 | rtcDataChannelInit.maxRetransmits = options!.object(forKey: "maxRetransmits") as! Int32 72 | } 73 | 74 | // TODO: error: expected member name following '.' 75 | // https://code.google.com/p/webrtc/issues/detail?id=4614 76 | // if options?.objectForKey("protocol") != nil { 77 | // rtcDataChannelInit.protocol = options!.objectForKey("protocol") as! String 78 | // } 79 | if options?.object(forKey: "protocol") != nil { 80 | rtcDataChannelInit.`protocol` = options!.object(forKey: "protocol") as! String 81 | } 82 | 83 | if options?.object(forKey: "negotiated") != nil { 84 | rtcDataChannelInit.isNegotiated = options!.object(forKey: "negotiated") as! Bool 85 | } 86 | 87 | if options?.object(forKey: "id") != nil { 88 | rtcDataChannelInit.channelId = options!.object(forKey: "id") as! Int32 89 | } 90 | 91 | self.rtcDataChannel = rtcPeerConnection.dataChannel(forLabel: label, configuration: rtcDataChannelInit) 92 | 93 | if self.rtcDataChannel == nil { 94 | NSLog("iRTCDataChannel#init() | rtcPeerConnection.createDataChannelWithLabel() failed") 95 | return 96 | } 97 | 98 | // Report definitive data to update the JS instance. 99 | self.eventListener!([ 100 | "type": "new", 101 | "channel": [ 102 | "ordered": self.rtcDataChannel!.isOrdered, 103 | "maxPacketLifeTime": self.rtcDataChannel!.maxPacketLifeTime, 104 | "maxRetransmits": self.rtcDataChannel!.maxRetransmits, 105 | "protocol": self.rtcDataChannel!.`protocol`, 106 | "negotiated": self.rtcDataChannel!.isNegotiated, 107 | "id": self.rtcDataChannel!.channelId, 108 | "readyState": iRTCDataChannel.stateToString(state: self.rtcDataChannel!.readyState), 109 | "bufferedAmount": self.rtcDataChannel!.bufferedAmount 110 | ] 111 | ]) 112 | } 113 | 114 | deinit { 115 | NSLog("iRTCDataChannel#deinit()") 116 | } 117 | 118 | /** 119 | * Constructor for pc.ondatachannel event. 120 | */ 121 | init(rtcDataChannel: RTCDataChannel) { 122 | NSLog("iRTCDataChannel#init()") 123 | 124 | self.rtcDataChannel = rtcDataChannel 125 | } 126 | 127 | func run() { 128 | NSLog("iRTCDataChannel#run()") 129 | 130 | self.rtcDataChannel!.delegate = self 131 | 132 | //if data channel is created after there is a connection, 133 | // we need to dispatch its current state. 134 | if (self.rtcDataChannel?.readyState != RTCDataChannelState.connecting) { 135 | dataChannelDidChangeState(self.rtcDataChannel!); 136 | } 137 | } 138 | 139 | func setListener( 140 | _ eventListener: @escaping (_ data: NSDictionary) -> Void, 141 | eventListenerForBinaryMessage: @escaping (_ data: Data) -> Void 142 | ) { 143 | NSLog("iRTCDataChannel#setListener()") 144 | 145 | self.eventListener = eventListener 146 | self.eventListenerForBinaryMessage = eventListenerForBinaryMessage 147 | 148 | for readyState in self.lostStates { 149 | self.eventListener!([ 150 | "type": "statechange", 151 | "readyState": readyState 152 | ]) 153 | } 154 | self.lostStates.removeAll() 155 | 156 | for buffer in self.lostMessages { 157 | self.emitReceivedMessage(buffer) 158 | } 159 | self.lostMessages.removeAll() 160 | } 161 | 162 | func sendString( 163 | _ data: String, 164 | callback: (_ data: NSDictionary) -> Void 165 | ) { 166 | NSLog("iRTCDataChannel#sendString()") 167 | 168 | let buffer = RTCDataBuffer( 169 | data: (data.data(using: String.Encoding.utf8))!, 170 | isBinary: false 171 | ) 172 | 173 | let result = self.rtcDataChannel!.sendData(buffer) 174 | if !result { 175 | NSLog("iRTCDataChannel#sendString() | RTCDataChannel#sendData() failed") 176 | } 177 | } 178 | 179 | func sendBinary( 180 | _ data: Data, 181 | callback: (_ data: NSDictionary) -> Void 182 | ) { 183 | NSLog("iRTCDataChannel#sendBinary()") 184 | 185 | let buffer = RTCDataBuffer( 186 | data: data, 187 | isBinary: true 188 | ) 189 | 190 | let result = self.rtcDataChannel!.sendData(buffer) 191 | if !result { 192 | NSLog("iRTCDataChannel#sendBinary() | RTCDataChannel#sendData() failed") 193 | } 194 | } 195 | 196 | func close() { 197 | NSLog("iRTCDataChannel#close()") 198 | 199 | self.rtcDataChannel!.close() 200 | } 201 | 202 | static func stateToString(state: RTCDataChannelState) -> String { 203 | switch state { 204 | case RTCDataChannelState.connecting: 205 | return "connecting" 206 | case RTCDataChannelState.open: 207 | return "open" 208 | case RTCDataChannelState.closing: 209 | return "closing" 210 | case RTCDataChannelState.closed: 211 | return "closed" 212 | } 213 | } 214 | 215 | func getState() -> String { 216 | return iRTCDataChannel.stateToString(state: self.rtcDataChannel!.readyState) 217 | } 218 | 219 | /** 220 | * Methods inherited from RTCDataChannelDelegate. 221 | */ 222 | 223 | /** The data channel state changed. */ 224 | func dataChannelDidChangeState(_ channel: RTCDataChannel) { 225 | let state_str = self.getState() 226 | 227 | NSLog("iRTCDataChannel | state changed [state:%@]", String(describing: state_str)) 228 | 229 | if self.eventListener != nil { 230 | self.eventListener!([ 231 | "type": "statechange", 232 | "readyState": state_str 233 | ]) 234 | } else { 235 | // It may happen that the eventListener is not yet set, so store the lost states. 236 | self.lostStates.append(state_str) 237 | } 238 | } 239 | 240 | /** The data channel successfully received a data buffer. */ 241 | func dataChannel(_ channel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) { 242 | if !buffer.isBinary { 243 | NSLog("iRTCDataChannel | utf8 message received") 244 | 245 | if self.eventListener != nil { 246 | self.emitReceivedMessage(buffer) 247 | } else { 248 | // It may happen that the eventListener is not yet set, so store the lost messages. 249 | self.lostMessages.append(buffer) 250 | } 251 | } else { 252 | NSLog("iRTCDataChannel | binary message received") 253 | 254 | if self.eventListenerForBinaryMessage != nil { 255 | self.emitReceivedMessage(buffer) 256 | } else { 257 | // It may happen that the eventListener is not yet set, so store the lost messages. 258 | self.lostMessages.append(buffer) 259 | } 260 | } 261 | } 262 | 263 | /** The data channel's |bufferedAmount| changed. */ 264 | func dataChannel(_ channel: RTCDataChannel, didChangeBufferedAmount amount: UInt64) { 265 | NSLog("iRTCDataChannel | didChangeBufferedAmount %d", amount) 266 | 267 | self.eventListener!([ 268 | "type": "bufferedamount", 269 | "bufferedAmount": amount 270 | ]) 271 | 272 | } 273 | 274 | func emitReceivedMessage(_ buffer: RTCDataBuffer) { 275 | if !buffer.isBinary { 276 | let message = NSString( 277 | data: buffer.data, 278 | encoding: String.Encoding.utf8.rawValue 279 | ) 280 | 281 | self.eventListener!([ 282 | "type": "message", 283 | "message": message! as String 284 | ]) 285 | } else { 286 | self.eventListenerForBinaryMessage!(buffer.data) 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/MediaStreamRenderer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cordova-plugin-iosrtc v6.0.17 3 | * Cordova iOS plugin exposing the ̶f̶u̶l̶l̶ WebRTC W3C JavaScript APIs. 4 | * Copyright 2015-2017 eFace2Face, Inc. (https://eface2face.com) 5 | * Copyright 2015-2019 BasqueVoIPMafia (https://github.com/BasqueVoIPMafia) 6 | * Copyright 2019 Cordova-RTC (https://github.com/cordova-rtc) 7 | * The MIT License (MIT) 8 | */ 9 | 10 | /** 11 | * Expose the MediaStreamRenderer class. 12 | */ 13 | module.exports = MediaStreamRenderer; 14 | 15 | 16 | /** 17 | * Dependencies. 18 | */ 19 | var 20 | debug = require('debug')('iosrtc:MediaStreamRenderer'), 21 | exec = require('./IOSExec'), 22 | randomNumber = require('random-number').generator({min: 10000, max: 99999, integer: true}), 23 | EventTarget = require('./EventTarget'), 24 | MediaStream = require('./MediaStream'); 25 | 26 | 27 | function MediaStreamRenderer(element) { 28 | debug('new() | [element:"%s"]', element); 29 | 30 | var self = this; 31 | 32 | // Make this an EventTarget. 33 | EventTarget.call(this); 34 | 35 | if (!(element instanceof HTMLElement)) { 36 | throw new Error('a valid HTMLElement is required'); 37 | } 38 | 39 | // Public atributes. 40 | this.element = element; 41 | this.stream = undefined; 42 | this.videoWidth = undefined; 43 | this.videoHeight = undefined; 44 | 45 | // Private attributes. 46 | this.id = randomNumber(); 47 | 48 | function onResultOK(data) { 49 | onEvent.call(self, data); 50 | } 51 | 52 | exec.execNative(onResultOK, null, 'WKWebViewRTC', 'new_MediaStreamRenderer', [this.id]); 53 | 54 | this.refresh(); 55 | 56 | // TODO cause video resizing jiggling add semaphore 57 | //this.refreshInterval = setInterval(function () { 58 | // self.refresh(self); 59 | //}, 500); 60 | 61 | element.render = this; 62 | } 63 | 64 | MediaStreamRenderer.prototype = Object.create(EventTarget.prototype); 65 | MediaStreamRenderer.prototype.constructor = MediaStreamRenderer; 66 | 67 | MediaStreamRenderer.prototype.render = function (stream) { 68 | debug('render() [stream:%o]', stream); 69 | 70 | var self = this; 71 | 72 | if (!(stream instanceof MediaStream.originalMediaStream)) { 73 | throw new Error('render() requires a MediaStream instance as argument'); 74 | } 75 | 76 | self.stream = stream; 77 | 78 | exec.execNative(null, null, 'WKWebViewRTC', 'MediaStreamRenderer_render', [self.id, stream.id]); 79 | 80 | // Subscribe to 'update' event so we call native mediaStreamChanged() on it. 81 | stream.addEventListener('update', function () { 82 | if (self.stream !== stream) { 83 | return; 84 | } 85 | 86 | debug('MediaStream emits "update", calling native mediaStreamChanged()'); 87 | 88 | exec.execNative(null, null, 'WKWebViewRTC', 'MediaStreamRenderer_mediaStreamChanged', [self.id]); 89 | }); 90 | 91 | // Subscribe to 'inactive' event and emit "close" so the video element can react. 92 | stream.addEventListener('inactive', function () { 93 | if (self.stream !== stream) { 94 | return; 95 | } 96 | 97 | debug('MediaStream emits "inactive", emiting "close" and closing this MediaStreamRenderer'); 98 | 99 | self.dispatchEvent(new Event('close')); 100 | self.close(); 101 | }); 102 | 103 | if (stream.connected) { 104 | connected(); 105 | // Otherwise subscribe to 'connected' event to emulate video elements events. 106 | } else { 107 | stream.addEventListener('connected', function () { 108 | if (self.stream !== stream) { 109 | return; 110 | } 111 | 112 | connected(); 113 | }); 114 | } 115 | 116 | function connected() { 117 | // Emit video events. 118 | self.element.dispatchEvent(new Event('loadedmetadata')); 119 | self.element.dispatchEvent(new Event('loadeddata')); 120 | self.element.dispatchEvent(new Event('canplay')); 121 | self.element.dispatchEvent(new Event('canplaythrough')); 122 | } 123 | }; 124 | 125 | MediaStreamRenderer.prototype.save = function (callback) { 126 | debug('save()'); 127 | 128 | if (!this.stream) { 129 | callback(null); 130 | return; 131 | } 132 | 133 | function onResultOK(data) { 134 | callback(data); 135 | } 136 | 137 | function onResultError() { 138 | callback(null); 139 | } 140 | 141 | exec.execNative(onResultOK, onResultError, 'WKWebViewRTC', 'MediaStreamRenderer_save', [this.id]); 142 | }; 143 | 144 | MediaStreamRenderer.prototype.refresh = function () { 145 | debug('refresh()'); 146 | 147 | var elementPositionAndSize = getElementPositionAndSize.call(this), 148 | computedStyle, 149 | videoRatio, 150 | elementRatio, 151 | elementLeft = elementPositionAndSize.left, 152 | elementTop = elementPositionAndSize.top, 153 | elementWidth = elementPositionAndSize.width, 154 | elementHeight = elementPositionAndSize.height, 155 | videoViewWidth, 156 | videoViewHeight, 157 | visible, 158 | opacity, 159 | zIndex, 160 | mirrored, 161 | objectFit, 162 | clip, 163 | borderRadius, 164 | paddingTop, 165 | paddingBottom, 166 | paddingLeft, 167 | paddingRight, 168 | backgroundColorRgba, 169 | self = this; 170 | 171 | computedStyle = window.getComputedStyle(this.element); 172 | 173 | // get background color 174 | backgroundColorRgba = computedStyle.backgroundColor 175 | .replace(/rgba?\((.*)\)/, '$1') 176 | .split(',') 177 | .map(function (x) { 178 | return x.trim(); 179 | }); 180 | backgroundColorRgba[3] = '0'; 181 | this.element.style.backgroundColor = 'rgba(' + backgroundColorRgba.join(',') + ')'; 182 | backgroundColorRgba.length = 3; 183 | 184 | // get padding values 185 | paddingTop = parseInt(computedStyle.paddingTop) | 0; 186 | paddingBottom = parseInt(computedStyle.paddingBottom) | 0; 187 | paddingLeft = parseInt(computedStyle.paddingLeft) | 0; 188 | paddingRight = parseInt(computedStyle.paddingRight) | 0; 189 | 190 | // fix position according to padding 191 | elementLeft += paddingLeft; 192 | elementTop += paddingTop; 193 | 194 | // fix width and height according to padding 195 | elementWidth -= paddingLeft + paddingRight; 196 | elementHeight -= paddingTop + paddingBottom; 197 | 198 | videoViewWidth = elementWidth; 199 | videoViewHeight = elementHeight; 200 | 201 | // visible 202 | if (computedStyle.visibility === 'hidden') { 203 | visible = false; 204 | } else { 205 | visible = !!this.element.offsetHeight; // Returns 0 if element or any parent is hidden. 206 | } 207 | 208 | // opacity 209 | opacity = parseFloat(computedStyle.opacity); 210 | 211 | // zIndex 212 | zIndex = parseFloat(computedStyle.zIndex) || parseFloat(this.element.style.zIndex) || 0; 213 | 214 | // mirrored (detect "-webkit-transform: scaleX(-1);" or equivalent) 215 | if (computedStyle.transform === 'matrix(-1, 0, 0, 1, 0, 0)' || 216 | computedStyle['-webkit-transform'] === 'matrix(-1, 0, 0, 1, 0, 0)') { 217 | mirrored = true; 218 | } else { 219 | mirrored = false; 220 | } 221 | 222 | // objectFit ('contain' is set as default value) 223 | objectFit = computedStyle.objectFit || 'contain'; 224 | 225 | // clip 226 | if (objectFit === 'none') { 227 | clip = false; 228 | } else { 229 | clip = true; 230 | } 231 | 232 | // borderRadius 233 | borderRadius = parseFloat(computedStyle.borderRadius); 234 | if (/%$/.test(borderRadius)) { 235 | borderRadius = Math.min(elementHeight, elementWidth) * borderRadius; 236 | } 237 | 238 | /** 239 | * No video yet, so just update the UIView with the element settings. 240 | */ 241 | 242 | if (!this.videoWidth || !this.videoHeight) { 243 | debug('refresh() | no video track yet'); 244 | 245 | nativeRefresh.call(this); 246 | return; 247 | } 248 | 249 | videoRatio = this.videoWidth / this.videoHeight; 250 | 251 | /** 252 | * Element has no width and/or no height. 253 | */ 254 | 255 | if (!elementWidth || !elementHeight) { 256 | debug('refresh() | video element has 0 width and/or 0 height'); 257 | 258 | nativeRefresh.call(this); 259 | return; 260 | } 261 | 262 | /** 263 | * Set video view position and size. 264 | */ 265 | 266 | elementRatio = elementWidth / elementHeight; 267 | 268 | switch (objectFit) { 269 | case 'cover': 270 | // The element has higher or equal width/height ratio than the video. 271 | if (elementRatio >= videoRatio) { 272 | videoViewWidth = elementWidth; 273 | videoViewHeight = videoViewWidth / videoRatio; 274 | // The element has lower width/height ratio than the video. 275 | } else if (elementRatio < videoRatio) { 276 | videoViewHeight = elementHeight; 277 | videoViewWidth = videoViewHeight * videoRatio; 278 | } 279 | break; 280 | 281 | case 'fill': 282 | videoViewHeight = elementHeight; 283 | videoViewWidth = elementWidth; 284 | break; 285 | 286 | case 'none': 287 | videoViewHeight = this.videoHeight; 288 | videoViewWidth = this.videoWidth; 289 | break; 290 | 291 | case 'scale-down': 292 | // Same as 'none'. 293 | if (this.videoWidth <= elementWidth && this.videoHeight <= elementHeight) { 294 | videoViewHeight = this.videoHeight; 295 | videoViewWidth = this.videoWidth; 296 | // Same as 'contain'. 297 | } else { 298 | // The element has higher or equal width/height ratio than the video. 299 | if (elementRatio >= videoRatio) { 300 | videoViewHeight = elementHeight; 301 | videoViewWidth = videoViewHeight * videoRatio; 302 | // The element has lower width/height ratio than the video. 303 | } else if (elementRatio < videoRatio) { 304 | videoViewWidth = elementWidth; 305 | videoViewHeight = videoViewWidth / videoRatio; 306 | } 307 | } 308 | break; 309 | 310 | // 'contain'. 311 | default: 312 | objectFit = 'contain'; 313 | // The element has higher or equal width/height ratio than the video. 314 | if (elementRatio >= videoRatio) { 315 | videoViewHeight = elementHeight; 316 | videoViewWidth = videoViewHeight * videoRatio; 317 | // The element has lower width/height ratio than the video. 318 | } else if (elementRatio < videoRatio) { 319 | videoViewWidth = elementWidth; 320 | videoViewHeight = videoViewWidth / videoRatio; 321 | } 322 | break; 323 | } 324 | 325 | nativeRefresh.call(this); 326 | 327 | function hash(str) { 328 | var hash = 5381, 329 | i = str.length; 330 | 331 | while (i) { 332 | hash = (hash * 33) ^ str.charCodeAt(--i); 333 | } 334 | 335 | return hash >>> 0; 336 | } 337 | 338 | function nativeRefresh() { 339 | var data = { 340 | elementLeft: Math.round(elementLeft), 341 | elementTop: Math.round(elementTop), 342 | elementWidth: Math.round(elementWidth), 343 | elementHeight: Math.round(elementHeight), 344 | videoViewWidth: Math.round(videoViewWidth), 345 | videoViewHeight: Math.round(videoViewHeight), 346 | visible: visible, 347 | backgroundColor: backgroundColorRgba.join(','), 348 | opacity: opacity, 349 | zIndex: zIndex, 350 | mirrored: mirrored, 351 | objectFit: objectFit, 352 | clip: clip, 353 | borderRadius: borderRadius 354 | }, 355 | newRefreshCached = hash(JSON.stringify(data)); 356 | 357 | if (newRefreshCached === self.refreshCached) { 358 | return; 359 | } 360 | 361 | self.refreshCached = newRefreshCached; 362 | 363 | debug('refresh() | [data:%o]', data); 364 | 365 | exec.execNative(null, null, 'WKWebViewRTC', 'MediaStreamRenderer_refresh', [this.id, data]); 366 | } 367 | }; 368 | 369 | 370 | MediaStreamRenderer.prototype.close = function () { 371 | debug('close()'); 372 | 373 | if (!this.stream) { 374 | return; 375 | } 376 | this.stream = undefined; 377 | 378 | exec.execNative(null, null, 'WKWebViewRTC', 'MediaStreamRenderer_close', [this.id]); 379 | if (this.refreshInterval) { 380 | clearInterval(this.refreshInterval); 381 | delete this.refreshInterval; 382 | } 383 | }; 384 | 385 | 386 | /** 387 | * Private API. 388 | */ 389 | 390 | 391 | function onEvent(data) { 392 | var type = data.type, 393 | event; 394 | 395 | debug('onEvent() | [type:%s, data:%o]', type, data); 396 | 397 | switch (type) { 398 | case 'videoresize': 399 | this.videoWidth = data.size.width; 400 | this.videoHeight = data.size.height; 401 | this.refresh(); 402 | 403 | event = new Event(type); 404 | event.videoWidth = data.size.width; 405 | event.videoHeight = data.size.height; 406 | this.dispatchEvent(event); 407 | 408 | break; 409 | } 410 | } 411 | 412 | 413 | function getElementPositionAndSize() { 414 | var rect = this.element.getBoundingClientRect(); 415 | 416 | return { 417 | left: rect.left + this.element.clientLeft, 418 | top: rect.top + this.element.clientTop, 419 | width: this.element.clientWidth, 420 | height: this.element.clientHeight 421 | }; 422 | } 423 | -------------------------------------------------------------------------------- /WKWebViewRTC/Js/src/videoElementsHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose a function that must be called when the library is loaded. 3 | * And also a helper function. 4 | */ 5 | module.exports = videoElementsHandler; 6 | module.exports.observeVideo = observeVideo; 7 | module.exports.refreshVideos = refreshVideos; 8 | 9 | 10 | /** 11 | * Dependencies. 12 | */ 13 | var 14 | debug = require('debug')('iosrtc:videoElementsHandler'), 15 | MediaStreamRenderer = require('./MediaStreamRenderer'), 16 | 17 | 18 | /** 19 | * Local variables. 20 | */ 21 | 22 | // RegExp for Blob URI. 23 | BLOB_INTERNAL_URI_REGEX = new RegExp(/^blob:/), 24 | 25 | // Dictionary of MediaStreamRenderers (provided via module argument). 26 | // - key: MediaStreamRenderer id. 27 | // - value: MediaStreamRenderer. 28 | mediaStreamRenderers, 29 | 30 | // Dictionary of MediaStreams (provided via module argument). 31 | // - key: MediaStream blobId. 32 | // - value: MediaStream. 33 | mediaStreams, 34 | 35 | // Video element mutation observer. 36 | videoObserver = new MutationObserver(function (mutations) { 37 | var i, numMutations, mutation, video; 38 | 39 | for (i = 0, numMutations = mutations.length; i < numMutations; i++) { 40 | mutation = mutations[i]; 41 | 42 | // HTML video element. 43 | video = mutation.target; 44 | 45 | // .srcObject removed. 46 | if (!video.srcObject && !video.src) { 47 | // If this video element was previously handling a MediaStreamRenderer, release it. 48 | releaseMediaStreamRenderer(video); 49 | continue; 50 | } 51 | 52 | handleVideo(video); 53 | } 54 | }), 55 | 56 | // DOM mutation observer. 57 | domObserver = new MutationObserver(function (mutations) { 58 | var 59 | i, numMutations, mutation, 60 | j, numNodes, node; 61 | 62 | for (i = 0, numMutations = mutations.length; i < numMutations; i++) { 63 | mutation = mutations[i]; 64 | 65 | // Check if there has been addition or deletion of nodes. 66 | if (mutation.type !== 'childList') { 67 | continue; 68 | } 69 | 70 | // Check added nodes. 71 | for (j = 0, numNodes = mutation.addedNodes.length; j < numNodes; j++) { 72 | node = mutation.addedNodes[j]; 73 | 74 | checkNewNode(node); 75 | } 76 | 77 | // Check removed nodes. 78 | for (j = 0, numNodes = mutation.removedNodes.length; j < numNodes; j++) { 79 | node = mutation.removedNodes[j]; 80 | 81 | checkRemovedNode(node); 82 | } 83 | } 84 | 85 | function checkNewNode(node) { 86 | var j, childNode; 87 | 88 | if (node.nodeName === 'VIDEO') { 89 | debug('new video element added'); 90 | 91 | // Avoid same node firing more than once (really, may happen in some cases). 92 | if (node._iosrtcVideoHandled) { 93 | return; 94 | } 95 | node._iosrtcVideoHandled = true; 96 | 97 | // Observe changes in the video element. 98 | observeVideo(node); 99 | } else { 100 | for (j = 0; j < node.childNodes.length; j++) { 101 | childNode = node.childNodes.item(j); 102 | 103 | checkNewNode(childNode); 104 | } 105 | } 106 | } 107 | 108 | function checkRemovedNode(node) { 109 | var j, childNode; 110 | 111 | if (node.nodeName === 'VIDEO') { 112 | debug('video element removed'); 113 | 114 | // If this video element was previously handling a MediaStreamRenderer, release it. 115 | releaseMediaStreamRenderer(node); 116 | delete node._iosrtcVideoHandled; 117 | } else { 118 | for (j = 0; j < node.childNodes.length; j++) { 119 | childNode = node.childNodes.item(j); 120 | 121 | checkRemovedNode(childNode); 122 | } 123 | } 124 | } 125 | }); 126 | 127 | function refreshVideos() { 128 | debug('refreshVideos()'); 129 | 130 | var id; 131 | 132 | for (id in mediaStreamRenderers) { 133 | if (mediaStreamRenderers.hasOwnProperty(id)) { 134 | mediaStreamRenderers[id].refresh(); 135 | } 136 | } 137 | } 138 | 139 | function videoElementsHandler(_mediaStreams, _mediaStreamRenderers) { 140 | var 141 | existingVideos = document.querySelectorAll('video'), 142 | i, len, video; 143 | 144 | mediaStreams = _mediaStreams; 145 | mediaStreamRenderers = _mediaStreamRenderers; 146 | 147 | // Search the whole document for already existing HTML video elements and observe them. 148 | for (i = 0, len = existingVideos.length; i < len; i++) { 149 | video = existingVideos.item(i); 150 | 151 | debug('video element found'); 152 | 153 | observeVideo(video); 154 | } 155 | 156 | // Observe the whole document for additions of new HTML video elements and observe them. 157 | domObserver.observe(document, { 158 | // Set to true if additions and removals of the target node's child elements (including text nodes) are to 159 | // be observed. 160 | childList: true, 161 | // Set to true if mutations to target's attributes are to be observed. 162 | attributes: false, 163 | // Set to true if mutations to target's data are to be observed. 164 | characterData: false, 165 | // Set to true if mutations to not just target, but also target's descendants are to be observed. 166 | subtree: true, 167 | // Set to true if attributes is set to true and target's attribute value before the mutation needs to be 168 | // recorded. 169 | attributeOldValue: false, 170 | // Set to true if characterData is set to true and target's data before the mutation needs to be recorded. 171 | characterDataOldValue: false 172 | // Set to an array of attribute local names (without namespace) if not all attribute mutations need to be 173 | // observed. 174 | // attributeFilter: 175 | }); 176 | } 177 | 178 | 179 | function observeVideo(video) { 180 | debug('observeVideo()'); 181 | 182 | // If the video already has a srcObject property but is not yet handled by the plugin 183 | // then handle it now. 184 | var hasStream = video.srcObject || video.src; 185 | if (hasStream && !video._iosrtcMediaStreamRendererId) { 186 | handleVideo(video); 187 | } 188 | 189 | // Add .srcObject observer to the video element. 190 | videoObserver.observe(video, { 191 | // Set to true if additions and removals of the target node's child elements (including text 192 | // nodes) are to be observed. 193 | childList: false, 194 | // Set to true if mutations to target's attributes are to be observed. 195 | attributes: true, 196 | // Set to true if mutations to target's data are to be observed. 197 | characterData: false, 198 | // Set to true if mutations to not just target, but also target's descendants are to be observed. 199 | subtree: false, 200 | // Set to true if attributes is set to true and target's attribute value before the mutation 201 | // needs to be recorded. 202 | attributeOldValue: false, 203 | // Set to true if characterData is set to true and target's data before the mutation needs to be 204 | // recorded. 205 | characterDataOldValue: false, 206 | // Set to an array of attribute local names (without namespace) if not all attribute mutations 207 | // need to be observed. 208 | // srcObject DO not trigger MutationObserver 209 | attributeFilter: ['srcObject', 'src'] 210 | }); 211 | 212 | // MutationObserver fail to trigger when using srcObject on ony tested browser. 213 | // But video.srcObject = new MediaStream() will trigger onloadstart and 214 | // video.srcObject = null will trigger onemptied events. 215 | 216 | video.addEventListener('loadstart', function () { 217 | 218 | var hasStream = video.srcObject || video.src; 219 | 220 | if (hasStream && !video._iosrtcMediaStreamRendererId) { 221 | // If this video element was NOT previously handling a MediaStreamRenderer, release it. 222 | handleVideo(video); 223 | } else if (hasStream && video._iosrtcMediaStreamRendererId) { 224 | // The video element has received a new srcObject. 225 | var stream = video.srcObject; 226 | if (stream && typeof stream.getBlobId === 'function') { 227 | // Release previous renderer 228 | releaseMediaStreamRenderer(video); 229 | // Install new renderer 230 | provideMediaStreamRenderer(video, stream.getBlobId()); 231 | } 232 | } 233 | }); 234 | 235 | video.addEventListener('emptied', function () { 236 | var hasStream = video.srcObject || video.src; 237 | if (!hasStream && video._iosrtcMediaStreamRendererId) { 238 | // If this video element was previously handling a MediaStreamRenderer, release it. 239 | releaseMediaStreamRenderer(video); 240 | } 241 | }); 242 | 243 | // Intercept video 'error' events if it's due to the attached MediaStream. 244 | video.addEventListener('error', function (event) { 245 | if (video.error.code === window.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED && BLOB_INTERNAL_URI_REGEX.test(video.src)) { 246 | debug('stopping "error" event propagation for video element'); 247 | 248 | event.stopImmediatePropagation(); 249 | } 250 | }); 251 | } 252 | 253 | 254 | /** 255 | * Private API. 256 | */ 257 | 258 | function handleVideo(video) { 259 | var 260 | stream; 261 | 262 | // The app has set video.srcObject. 263 | if (video.srcObject) { 264 | stream = video.srcObject; 265 | if (stream && typeof stream.getBlobId === 'function') { 266 | 267 | if (!stream.getBlobId()) { 268 | // If this video element was previously handling a MediaStreamRenderer, release it. 269 | releaseMediaStreamRenderer(video); 270 | 271 | return; 272 | } 273 | 274 | provideMediaStreamRenderer(video, stream.getBlobId()); 275 | } 276 | } else if (video.src) { 277 | 278 | var xhr = new XMLHttpRequest(); 279 | xhr.open('GET', video.src, true); 280 | xhr.responseType = 'blob'; 281 | xhr.onload = function () { 282 | if (xhr.status !== 200) { 283 | // If this video element was previously handling a MediaStreamRenderer, release it. 284 | releaseMediaStreamRenderer(video); 285 | 286 | return; 287 | } 288 | 289 | var reader = new FileReader(); 290 | 291 | // Some versions of Safari fail to set onloadend property, some others do not react 292 | // on 'loadend' event. Try everything here. 293 | try { 294 | reader.onloadend = onloadend; 295 | } catch (error) { 296 | reader.addEventListener('loadend', onloadend); 297 | } 298 | reader.readAsText(xhr.response); 299 | 300 | function onloadend() { 301 | var mediaStreamBlobId = reader.result; 302 | 303 | // The retrieved URL does not point to a MediaStream. 304 | if (!mediaStreamBlobId || typeof mediaStreamBlobId !== 'string') { 305 | // If this video element was previously handling a MediaStreamRenderer, release it. 306 | releaseMediaStreamRenderer(video); 307 | 308 | return; 309 | } 310 | 311 | provideMediaStreamRenderer(video, mediaStreamBlobId); 312 | } 313 | }; 314 | xhr.send(); 315 | } 316 | } 317 | 318 | 319 | function provideMediaStreamRenderer(video, mediaStreamBlobId) { 320 | var 321 | mediaStream = mediaStreams[mediaStreamBlobId], 322 | mediaStreamRenderer = mediaStreamRenderers[video._iosrtcMediaStreamRendererId]; 323 | 324 | if (!mediaStream) { 325 | releaseMediaStreamRenderer(video); 326 | 327 | return; 328 | } 329 | 330 | if (mediaStreamRenderer) { 331 | mediaStreamRenderer.render(mediaStream); 332 | } else { 333 | mediaStreamRenderer = new MediaStreamRenderer(video); 334 | mediaStreamRenderer.render(mediaStream); 335 | 336 | mediaStreamRenderers[mediaStreamRenderer.id] = mediaStreamRenderer; 337 | video._iosrtcMediaStreamRendererId = mediaStreamRenderer.id; 338 | } 339 | 340 | // Close the MediaStreamRenderer of this video if it emits "close" event. 341 | mediaStreamRenderer.addEventListener('close', function () { 342 | if (mediaStreamRenderers[video._iosrtcMediaStreamRendererId] !== mediaStreamRenderer) { 343 | return; 344 | } 345 | 346 | releaseMediaStreamRenderer(video); 347 | }); 348 | 349 | // Override some