Swoole 实践篇之结合 WebRTC 实现音视频实时通信方案

原文首发链接:Swoole 实践篇之结合 WebRTC 实现音视频实时通信方案
大家好,我是码农先森。

引言

这次实现音视频实时通信的方案是基于 WebRTC 技术的,它是一种点对点的通信技术,通过浏览器之间建立对等连接,实现音频和视频流数据的传输。

在 WebRTC 技术中通常使用 WebSocket 服务来协调浏览器之间的通信,建立 WebRTC 通信的信道,传输通信所需的元数据信息,如:SDP、ICE 候选项等。

WebRTC 技术在实时通信领域中得到了广泛应用,包括在线会议、视频聊天、远程协作等,例如:腾讯在线会议就是基于此技术实现的。

技术实现

index.html 作为首页,这里提供了发起方、接收方的操作入口。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>p2p webrtc</title>
    <style>
    .container {
        width: 250px;
        margin: 100px auto;
        padding: 10px 30px;
        border-radius: 4px;
    border: 1px solid #ebeef5;
    box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
    color: #303133;
    }
    </style>
</head>
<body>
    <div class="container">
        <p>流程:</p>
        <ul>
            <li>打开<a href="/p2p?type=answer" target="_blank">接收方页面</a></li>
            <li>打开<a href="/p2p?type=offer" target="_blank">发起方页面</a></li>
            <li>确认双方都已建立 WebSocket 连接;</li>
            <li>发起方点击 开始 按钮。</li>
        </ul>
    </div>
</body>
</html>

p2p.html 作为视频展示页面,且实现了调取摄像头及音频权限的功能,再将连接数据推送到 WebSocket 服务端,最后渲染远程端的音视频数据到本地。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title></title>
    <style>
        * {
            padding: 0;
            margin: 0;
            box-sizing: border-box;
        }
        .container {
            width: 100%;
            display: flex;
            display: -webkit-flex;
            justify-content: space-around;
            padding-top: 20px;
        }
        .video-box {
            position: relative;
            width: 330px;
            height: 550px;
        }
        #remote-video {
            width: 100%;
            height: 100%;
            display: block;
            object-fit: cover;
            border: 1px solid #eee;
            background-color: #F2F6FC;
        }
        #local-video {
            position: absolute;
            right: 0;
            bottom: 0;
            width: 140px;
            height: 200px;
            object-fit: cover;
            border: 1px solid #eee;
            background-color: #EBEEF5;
        }
        .start-button {
            position: absolute;
            left: 50%;
            top: 50%;
            width: 100px;
            display: none;
            line-height: 40px;
            outline: none;
            color: #fff;
            background-color: #409eff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transform: translate(-50%, -50%);
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="video-box">
            <video id="remote-video"></video>
            <video id="local-video" muted></video>
            <button class="start-button" onclick="startLive()">开始</button>
        </div>
    </div>
    <script>
        const target = location.search.slice(6);
        const localVideo = document.querySelector('#local-video');
        const remoteVideo = document.querySelector('#remote-video');
        const button = document.querySelector('.start-button');

        localVideo.onloadeddata = () => {
            console.log('播放本地视频');
            localVideo.play();
        }
        remoteVideo.onloadeddata = () => {
            console.log('播放对方视频');
            remoteVideo.play();
        }

        document.title = target === 'offer' ? '发起方' : '接收方';

        console.log('信令通道(WebSocket)创建中......');
        const socket = new WebSocket('ws://127.0.0.1:9502');
        socket.onopen = () => {
            console.log('信令通道创建成功!');
            target === 'offer' && (button.style.display = 'block');
        }
        socket.onerror = () => console.error('信令通道创建失败!');
        socket.onmessage = e => {
            const { type, sdp, iceCandidate } = JSON.parse(e.data)
            if (type === 'answer') {
                peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }));
            } else if (type === 'answer_ice') {
                peer.addIceCandidate(iceCandidate);
            } else if (type === 'offer') {
                startLive(new RTCSessionDescription({ type, sdp }));
            } else if (type === 'offer_ice') {
                peer.addIceCandidate(iceCandidate);
            }
        };

        const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
        !PeerConnection && console.error('浏览器不支持WebRTC!');
        const peer = new PeerConnection();

        peer.ontrack = e => {
            if (e && e.streams) {
                console.log('收到对方音频/视频流数据...');
                remoteVideo.srcObject = e.streams[0];
            }
        };

        peer.onicecandidate = e => {
            if (e.candidate) {
                console.log('搜集并发送候选人');
                socket.send(JSON.stringify({
                    type: `${target}_ice`,
                    iceCandidate: e.candidate
                }));
            } else {
                console.log('候选人收集完成!');
            }
        };

        async function startLive (offerSdp) {
            target === 'offer' && (button.style.display = 'none');
            let stream;
            try {
                console.log('尝试调取本地摄像头/麦克风');
                stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
                console.log('摄像头/麦克风获取成功!');
                localVideo.srcObject = stream;
            } catch {
                console.error('摄像头/麦克风获取失败!');
                return;
            }

            console.log(`------ WebRTC ${target === 'offer' ? '发起方' : '接收方'}流程开始 ------`);
            console.log('将媒体轨道添加到轨道集');
            stream.getTracks().forEach(track => {
                peer.addTrack(track, stream);
            });

            if (!offerSdp) {
                console.log('创建本地SDP');
                const offer = await peer.createOffer();
                await peer.setLocalDescription(offer);

                console.log(`传输发起方本地SDP`);
                socket.send(JSON.stringify(offer));
            } else {
                console.log('接收到发送方SDP');
                await peer.setRemoteDescription(offerSdp);

                console.log('创建接收方(应答)SDP');
                const answer = await peer.createAnswer();
                console.log(`传输接收方(应答)SDP`);
                socket.send(JSON.stringify(answer));
                await peer.setLocalDescription(answer);
            }
        }
    </script>
</body>
</html>

在 http_server.php 文件中实现了一个 Web 服务,并根据不同的路由返回对应的 HTML 页面服务,主要是用于提供视频页面的展示。

<?php

// 创建一个 HTTP 服务
$http = new Swoole\Http\Server("0.0.0.0", 9501);

// 监听客户端请求
$http->on('request', function ($request, $response) {
    $path = $request->server['request_uri'];
    switch ($path) {
        case '/':
            $html = file_get_contents("index.html");
            $response->header("Content-Type", "text/html");
            $response->end($html);
            break;

        case '/p2p':
            $html = file_get_contents("p2p.html");
            $response->header("Content-Type", "text/html");
            $response->end($html);
            break;
        default:
            $response->status(404);
            $response->end("Page Not Found");
            break;
    }
});

// 启动 HTTP 服务
$http->start();

在 websocket_server.php 文件中实现了一个 WebSocket 服务,并设置了 onOpen、onMessage 和 onClose 回调函数。在 onMessage 回调函数中,遍历所有连接,将消息发送给除当前连接外的其他连接。

<?php

// 创建 WebSocket 服务
$server = new Swoole\WebSocket\Server("0.0.0.0", 9502);

// 监听 WebSocket 连接事件
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
    echo "新的客户端连接: {$request->fd}\n";
});

// 监听 WebSocket 消息事件
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
    echo "收到消息来自 {$frame->fd}: {$frame->data}, 广播给其他的客户端\n";

    // 广播给其他的客户端
    foreach ($server->connections as $fd) {
        if ($fd != $frame->fd) {
            $server->push($fd, $frame->data);
        }
    }
});

// 监听 WebSocket 关闭事件
$server->on('close', function ($ser, $fd) {
    echo "客户端 {$fd} 关闭连接\n";
});

// 启 WebSocket 服务
$server->start();

总结

音视频通信技术方案是基于 WebRTC 实现的,Swoole 在其中的作用是提供了页面的 Web 服务及协调浏览器之间通信的 WebSocket 服务。
WebRTC 是一项重要的技术,它使得实时音视频通信变得简单而高效。通过基于浏览器的 API,WebRTC 可以实现点对点的音视频通信。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!