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 协议》,转载必须注明作者和本文链接
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

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