利用webRtc实现前端调用移动端摄像头

1,678 阅读5分钟

背景

移动端某项目的内嵌webview前端页面需要调用手机的摄像头功能。

前期调研

要实现这个功能,其实也有已经实践的方式。但会有缺陷。

方案:

ios手机:ios14.3以上手机直接调用webrtc中方法getUserMedia; ios14.3以下设备手机用jsbridge传递base64图片,前端canvas绘制图片帧。

安卓手机:前端直接调用webrtc中方法getUserMedia即可

缺陷:

  1. 每次调用getUserMedia都会有开启授权的弹窗;

  2. 并且高频传输base64图片,会导致手机内存泄漏,需要把图片转blob的方法防止内存溢出

探索新方案

在看了webRtc相关知识后发现,前端的video标签可以直接接收stream流数据展示。如果移动端可以直接把摄像头流传给前端就可以直接展示摄像头内容了。

类似于直播业务的推拉流操作。

那么在具体实践中就会存在几个问题:

  1. 如何创建连接?如何建立信令服务器?
  2. 视频流数据应该如何传递?
  3. 如何控制摄像头打开关闭?

实践

如何创建连接?如何建立信令服务器?

一段推流,要想另一端拉到流就需要建立连接。也就是信令。

信令是在两个设备之间发送控制信息以确定通信协议、信道、媒体编解码器和格式以及数据传输方法以及任何所需的路由信息的过程。关于 WebRTC 的信令流程最重要的一点是:信令在规范中并没有定义。所以开发者需要自己决定如何实现这个过程。开发者可以为应用程序引擎选择任意的信息协议(如 SIP 或 XMPP),任意双向通信信道(如 WebSocket 或 XMLHttpRequest) 与持久连接服务器的 API(如Google Channel API)一起工作。

为了交换信令信息,前端通常通过 WebSocket 连接来回发送 JSON 对象,或者可以通过 HTTPS 使用 XMLHttpRequest 进行轮询。甚至可以使用电子邮件作为信号通道。

还值得注意的是,用于执行信令的信道甚至不需要通过网络。一个 Peer 可以输出一个数据对象,这个数据对象可以被打印出来,然后物理携带(步行或由信鸽)直到进入另一个设备,然后由该设备输出响应,并以同种方式返回,直到 WebRTC 对等连接打开。这将带来非常高的延迟,但也是可以做到的。

最初的想法是移动端通过websocket创建一个本地服务来和前端建立连接并传递流数据。

但是后来发现我们根本不需要通过websocket来建立整个信令服务器。移动端内嵌webview本身就可以直接和前端进行双向通信。

所以不需要额外的创建连接的过程。

视频流数据应该如何传递?

image.png

以上是通常的直播推拉流,两端通信流程。而针对于移动端和前端直接通信的流程,就是如下图:

截屏2024-06-19 16.09.24.png

  1. 移动端创建RTCPeerConnection,并把本地视频利用addTrack传入。

  2. 移动端将 createoffersdp发送给h5;

  3. h5 收到offer后,h5创建RTCSessionDescription,获取会话描answer sdp信息;

  4. h5 将 answer sdp 发送给 移动端;

  5. h5向安卓发送answer:Android.sendAnswer(answer);h5向IOS发送answer: window.webkit.messageHandlers.sendAnswer.postMessage(answerString);

  6. 移动端 和 h5 开始收集并 交换 ice 信息;候选交换后,一旦 ICE 层满足要求,媒体数据就开始流动。所有这些都是在幕后处理端。我们的任务就是简单地通过信令服务器来回发送候选。

  7. h5 收到track可以获取媒体流信息并通过video渲染

这中间的一系列的信息的传递:off,iceCandidate都是通过移动端和前端互相触发方法来传递的。 因为用jsbridge数据会被转义,格式就不匹配了。

移动端调用前端方法(安卓为例):

移动端可以通过webview来调用前端挂载到Window上的方法。

//前端代码
  const initializePeerConnection = () => {
    const peerConnection = new RTCPeerConnection();
    peerConnection.ontrack = handleOnTrack;
    window.onIceCandidate = handleOnIceCandidate;
    window.onOffer = handleOnOffer;
    pc.value = peerConnection;
  };
//移动端代码(安卓)
   private class RtcHandler(webView: WebView): Handler(Looper.getMainLooper()){
        private val weakWebView = WeakReference(webView)
        override fun handleMessage(msg: Message) {
            when(msg.what){
                1->{
                    weakWebView.get()?.evaluateJavascript("javascript:onIceCandidate('${msg.obj as String}')", null)
                }
                2->{
                    weakWebView.get()?.evaluateJavascript("javascript:onOffer('${msg.obj as String}')", null)
                }
            }
        }
    }

前端如何调用移动端方法(ios为例)

Step 1: 创建一个继承自NSObject并实现WKScriptMessageHandler协议的类


classWebAppInterface: NSObject, WKScriptMessageHandler{  
  
funcuserContentController(_userContentController: WKUserContentController, didReceivemessage: WKScriptMessage) {  
if message.name =="ios" {  
iflet messageBody = message.body as?String {  
showToast(messageBody)  
}  
}  
}  
  
funcshowToast(_message: String) {  
*// 显示Toast或执行其他操作*  
print("Message from H5: \(message)")  
}  
} 

Step 2: 将该类实例绑定到WKWebView的配置中,并指定接口名称


let contentController =WKUserContentController()  
let webAppInterface =WebAppInterface()  
contentController.add(webAppInterface, name: "ios")  
  
let config =WKWebViewConfiguration()  
config.userContentController = contentController  
  
let webView =WKWebView(frame: .zero, configuration: config)  

Step 3: 在H5页面中调用iOS端方法


<buttononclick="callIOSFunction()">Call iOS Function</button>  
  
<script type="text/javascript">  
functioncallIOSFunction() {  
window.webkit.messageHandlers.ios.postMessage("Hello from H5!");  
}  
</script>

如何控制摄像头打开关闭?

通过jsbridge进行通信。

打开:前端调用后通知端上开始摄像头通信。

关闭:前端根据时机主动使用peerConnection.close()关闭连接;同时移动端要监听peerConnection状态和webview状态去关闭连接;

流数据为何不会造成内存泄漏

因为在js中,流数据被分块加入队列处理。同时会有积压系统判断数据缓存是否超出highWaterMark。如果超出了将暂停把数据流加入队列。一旦数据流清空了,就会继续接受下一批数据。 所以我们不需要手动的去清理。