Question or problem in the Swift programming language:
I have a mobile-optimized web app that makes use of getUserMedia to access webcam and mic resources.
I’m wrapping this app in a WKWebView as I want to offer a native app experience. I’m aware that iOS doesn’t allow camera access via browsers – however is there any way to gain permissions to the webcam/mic with native code (alongside the wrapper) and feed this to the web app – perhaps by somehow pointing getUserMedia to a local stream source?
How to solve the problem:
Yes, take a look at cordova-plugin-iosrtc and cordova-plugin-wkwebview-engine. The idea behind the plugin is as follows:
1. Create a JavaScript file (WebRTC.js) that defines the various WebRTC classes and functions, and passes the calls to the WKWebView, for example:
(function() { if (!window.navigator) window.navigator = {}; window.navigator.getUserMedia = function() { webkit.messageHandlers.callbackHandler.postMessage(arguments); } })();
2. In the WKWebView, inject the script at the document start:
let contentController = WKUserContentController(); contentController.add(self, name: "callbackHandler") let script = try! String(contentsOf: Bundle.main.url(forResource: "WebRTC", withExtension: "js")!, encoding: String.Encoding.utf8) contentController.addUserScript(WKUserScript(source: script, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: true)) let config = WKWebViewConfiguration() config.userContentController = contentController webView = WKWebView(frame: CGRect.zero, configuration: config)
3. Listen for messages sent from the JavaScript:
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler { var webView: WKWebView! func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if message.name == "callbackHandler" { print(message.body) // make native calls to the WebRTC framework here } } }
4. If success or failure callbacks need to be performed back in JavaScript-land, evaluate the function call directly within the WKWebView:
webView.evaluateJavaScript("callback({id: \(id), status: 'success', args: ...})", completionHandler: nil)
These callbacks need to be stored in a hash in the JavaScript before calling postMessage
, then the hash key must be sent to the WKWebView. This is the commandId
in the plugins.
int exec_id = 0; function exec(success, failure, ...) { // store the callbacks for later if (typeof success == 'function' || typeof failure == 'function') { exec_id++; exec_callbacks[exec_id] = { success: success, failure: failure }; var commandId = exec_id; } webkit.messageHandlers.callbackHandler.postMessage({id: commandId, args: ...}) } // the native code calls this directly with the same commandId, so the callbacks can be performed and released function callback(opts) { if (opts.status == "success") { if (typeof exec_callbacks[opts.id].success == 'function') exec_callbacks[opts.id].success(opts.args); } else { if (typeof exec_callbacks[opts.id].failure == 'function') exec_callbacks[opts.id].failure(opts.args); } // some WebRTC functions invoke the callbacks multiple times // the native Cordova plugin uses setKeepCallbackAs(true) if (!opts.keepalive) delete exec_callbacks[opts.id]; }
5. Of course add the NSCameraUsageDescription
and NSMicrophoneUsageDescription
permissions to the Info.plist
for your project.
Keep in mind this is a non-trivial task, but that’s the general idea behind bridging JavaScript, WKWebView, and native framework code with asynchronous callbacks.