WebRTC 应用开发 - 实现前后摄像头同时预览

Tips

仅部分手机支持同时打开前后摄像头,比如 小米10 以上机型等。

前文 基础上实现同时打开前后摄像头,并且模拟视频通话的界面和功能,主界面显示前置摄像头的内容,当发起通话,并且接通之后,在小窗口显示后置摄像头的内容

贴个图大概就是这样:

Image title
前后双摄预览

实现逻辑也比较清晰,首先要创建并打开前后摄像头:

// 创建前后摄像头采集器
val frontCapturer = createCameraCapturer(context, true)
val backCapturer = createCameraCapturer(context, false)

// 创建视频源和轨道
val frontSource = factory.createVideoSource(false)
val backSource = factory.createVideoSource(false)

// 初始化采集器
frontCapturer?.initialize(
    SurfaceTextureHelper.create("FrontCaptureThread", eglBase.eglBaseContext),
    context,
    frontSource.capturerObserver
)
backCapturer?.initialize(
    SurfaceTextureHelper.create("BackCaptureThread", eglBase.eglBaseContext),
    context,
    backSource.capturerObserver
)

// 开始采集
frontCapturer?.startCapture(1280, 720, 30)
backCapturer?.startCapture(640, 480, 15)

这时候摄像头还没有和 SurfaceView 进行关联,只有相机在疯狂输出,但却是看不到画面的。

作为视频通话功能,可以先让前置摄像头的画面和 SurfaceView 进行关联,点击按键进行通信后再关联后置摄像头的画面。

1
2
3
4
// 创建视频轨道
val frontTrack = factory.createVideoTrack("frontTrack", frontSource)
// 前置摄像头画面显示
frontTrack.addSink(frontRenderer)

接下来点击 发起通话 让前后摄像头进行通信,用 ICE 协商建立的过程模拟,当前置摄像头都收到对方应答后就可以把后置摄像头的画面显示在小窗口上。

Tips

可以把 前置摄像头 当成本地,后置摄像头 当成远端,收到远端回话,就表示电话打通了,可以显示画面了。

ICE 协商建立需要创建 PeerConnection ,它是 WebRTC 中的核心接口,代表两个通信端之间的点对点连接通道,负责管理音视频数据传输、网络穿透、编解码协商等全链路流程。

一般流程如下:

首先通过 PeerConnection 向远端服务器发送一个 Offer 信号,服务器处理分发后,让远端设备进行处理。

远端设备接收 Offer 信号后,会向服务端返回一个 Answer 信号,服务端分发处理后,让本地设备进行处理。

本地设备接收 Answer 信号进行处理,表示二者连接已经建立,可以交换 ​ICE Candidates​​ 数据了。

Tips

简要流程

本地设备 -> 发送 Offer 信号 -> 远端服务器 -> 远端设备

远端设备 -> 接收 Offer 信号 -> 发送 Answer 信号 -> 远端服务器

本地设备 -> 接收 Answer 信号 -> 连接建立成功

互相交换 ICE Candidates​​ 数据

因为是本地模拟不需要网络,流程可以简化很多。

当本地设备成功创建 Offer 信号,忽略网络请求发送以及远端接收的过程,默认远端设备可以收到,并且创建 Answer 信号返回。

同样也默认本地设备可以接受到 Offerr 信号。

frontPeer = factory.createPeerConnection(rtcConfig, object : SimplePeerConnectionObserver() {
    override fun onIceCandidate(p0: IceCandidate?) {
        backPeer.addIceCandidate(p0)
    }

    override fun onTrack(transceiver: RtpTransceiver?) {
        super.onTrack(transceiver)
        val videoTrack = transceiver?.receiver?.track() as? VideoTrack
        Handler(Looper.getMainLooper()).post {
            videoTrack?.addSink(backRenderer)
        }
    }
})!!

frontPeer.createOffer(
  object : SimpleSdpObserver() {
    override fun onCreateSuccess(offer: SessionDescription?) {
        frontPeer.setLocalDescription(object : SimpleSdpObserver() {}, offer)
        backPeer.setRemoteDescription(object : SimpleSdpObserver() {}, offer)

        // Offer 创建成功跳过服务端处理,让远端创建一个 Answer 过来
        backPeer.createAnswer(object : SimpleSdpObserver() {
            override fun onCreateSuccess(p0: SessionDescription?) {
                backPeer.setLocalDescription(object : SimpleSdpObserver() {}, p0)
                frontPeer.setRemoteDescription(object : SimpleSdpObserver() {}, p0)
                // 协商完成后显示小窗口
                showSmallWindow = true
            }
        }, MediaConstraints())
    }
  }, MediaConstraints()
)

以上代码除了实现 Offer 信号和 Answer 信号的创建和处理,还有关键步骤是 setLocalDescriptionsetRemoteDescription 操作。

本地设备首先要通过 setLocalDescription 把创建的 SDP 信息保存起来,并且通过 setRemoteDescription 把远端设备的 SDP 信息保存。

setLocalDescription 和 setRemoteDescription 会触发 PeerConnection 的 onIceCandidate 方法,对于远端设备同样如此。

Tips

如果没有 setLocalDescription 就直接 setRemoteDescription ,会报错 Failed to set remote answer sdp: Called in wrong state: stable 。

PeerConnection 有回调接口 PeerConnection.Observer ,在 setLocalDescription/setRemoteDescription 时也有回调接口 SdpObserver,一般顺序先回调 PeerConnection.Observer 的方法,比如回调 onTrack、onAddTrack 后才回调 SdpObserver 的 onSetSuccess 方法。

在 ICE 连接建立成功之后,PeerConnection 的 onTrack 方法会接收到远端设备的视频轨道,把视频轨道和 SurfaceView 关联,就可以看到远端画面了,也就是后置摄像头的内容。

OK ,到这里就实现了前后摄像头同时预览,具体的代码可以在 知识星球 里面去获取,相关答疑也在 知识星球 里面开展 ~~

📢 欢迎关注微信公众号:音视频开发进阶

评论