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

news2025/1/23 17:30:59

原文首发链接: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 可以实现点对点的音视频通信。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1600938.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

4.Labview簇、变体与类(上)

在Labview中&#xff0c;何为簇与变体&#xff0c;何为类&#xff1f;应该如何理解&#xff1f;具体有什么应用场景&#xff1f; 本文基于Labview软件&#xff0c;独到的讲解了簇与变体与类函数的使用方法和场景&#xff0c;从理论上讲解其数据流的底层概念&#xff0c;从实践上…

CSS基础之伪元素选择器(如果想知道CSS的伪元素选择器知识点,那么只看这一篇就足够了!)

前言&#xff1a;我们已经知道了在CSS中&#xff0c;选择器有基本选择器、复合选择器、伪类选择器、那么选择器学习完了吗&#xff1f;显然是没有的&#xff0c;这篇文章讲解最后一种选择器——伪元素选择器。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解更多内容可以访问我…

如何把npm切换成yarn管理项目

1.删掉项目中package-lock.json和依赖包 这一步手动删掉就好 2.全局安装yarn npm install -g yarn 3.可以开始执行yarn install安装依赖 1&#xff09;执行yarn init 这一步是修改npm生成的package.json文件&#xff0c;可能会遇到这个问题&#xff1a; 这个查了一下是有…

cesium primitive 移动 缩放 旋转 矩阵

旋转参考&#xff1a;cesium 指定点旋转rectangle Primitive方式 矩阵篇-CSDN博客 平移参考&#xff1a;cesium 调整3dtiles的位置 世界坐标下 相对坐标下 平移矩阵-CSDN博客 一、primitive方式添加polygon let polygonInstance new Cesium.GeometryInstance({geometry: Ce…

智能商品计划系统如何提升鞋服零售品牌的竞争力

国内鞋服零售企业经过多年的发展&#xff0c;已经形成了众多知名品牌&#xff0c;然而近年来一些企业频频受到库存问题的困扰&#xff0c;这一问题不仅影响了品牌商自身&#xff0c;也给长期合作的经销商带来了困扰。订货会制度在初期曾经有效地解决了盲目生产的问题&#xff0…

【python】flask结合SQLAlchemy,在视图函数中实现对数据库的增删改查

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

C++11(下篇)

文章目录 C111. 模版的可变参数1.1 模版参数包的使用 2. lambda表达式2.1 Lambda表达式语法捕获列表说明 2.2 lambda的底层 3. 包装器3.1 function包装器3.2 bind 4. 线程库4.1 thread类4.2 mutex类4.3 atomic类4.4 condition_variable类 C11 1. 模版的可变参数 C11支持模版的…

初学若依笔记

初学若依 下载ruoyi(以前后端分离板为例) https://ruoyi.vip/ 部署 安装mysql安装redis将数据库和redis配置到若依 配置文件为 ruoyi-admin\src\main\resource\application-druid.yml 运行 略 开发自己的功能 创建模块 为了不影响原有功能&#xff0c;创建一个模块写自…

[AI]-(第0期):认知深度学习

深度学习是一种人工智能&#xff08;AI&#xff09;方法&#xff0c;用于教计算机以受人脑启发的方式处理数据。 深度学习模型可以识别图片、文本、声音和其他数据中的复杂模式&#xff0c;从而生成准确的见解和预测。 您可以使用深度学习方法自动执行通常需要人工智能完成的…

深入理解大语言模型微调技术

一、概念解析 1、什么是微调&#xff08;Fine-tuning&#xff09;&#xff1f; 大模型微调&#xff0c;也称为Fine-tuning&#xff0c;是指在已经预训练好的大型语言模型基础上&#xff08;一般称为“基座模型”&#xff09;&#xff0c;使用特定的数据集进行进一步的训练&am…

pandas基本用法

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas的数据结构1、一维数组pd.Series1.1 pd.Series&#xff08;data,index,dtype&#xff09;示例1&#xff1a;不定义index示例2&#xff1a;自定义inde…

【C++学习】C++IO流

这里写目录标题 &#x1f680;C语言的输入与输出&#x1f680;什么是流&#x1f680;CIO流&#x1f680;C标准IO流&#x1f680;C文件IO流 &#x1f680;C语言的输入与输出 C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键盘)读取…

14.C++常用的算法_排序算法

文章目录 遍历算法1. sort()代码工程运行结果 2. random_shuffle()代码工程运行结果第一次运行结果第二次运行结果第三次运行结果 3. merge()代码工程运行结果 4. reverse()代码工程运行结果 遍历算法 1. sort() 代码工程 sort()函数默认是升序排列&#xff0c;如果想要降序…

Jenkins配置windows/linux从节点

背景&#xff1a; 环境&#xff1a;jenkins环境&#xff08;Ubuntu&#xff09; 节点机器&#xff1a;Linux、Windows 前置条件&#xff1a; 节点机器&#xff1a;安装java、allure、python 1 Linux节点管理机器添加 1.1 系统管理->节点列表->New Node 1.2 节点配置…

基于微信小程序投票评选系统的设计与实现(论文+源码)_kaic

摘 要 社会发展日新月异&#xff0c;用计算机应用实现数据管理功能已经算是很完善的了&#xff0c;但是随着移动互联网的到来&#xff0c;处理信息不再受制于地理位置的限制&#xff0c;处理信息及时高效&#xff0c;备受人们的喜爱。所以各大互联网厂商都瞄准移动互联网这个潮…

Weblogic 数据源无法解析错误的解决方法

问题现象 javax.naming.NameNotFoundException: Unable to resolve datasource1. Resolved ; remaining name datasource1at weblogic.jndi.internal.BasicNamingNode.newNameNotFoundException(BasicNamingNode.java:1292)at weblogic.jndi.internal.BasicNamingNode.lookupH…

leetcode-合并两个有序链表

目录 题目 图解 方法一 方法二 代码(解析在注释中) 方法一 ​编辑方法二 题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1…

电机控制器电路板布局布线参考指导(五)

电机控制器电路板布局布线参考指导&#xff08;五&#xff09;大容量电容和旁路电容的放置 1.大容量电容的放置2.电荷泵电容器3.旁路电容/去耦电容的放置3.1 靠近电源3.2 靠近功率器件3.3 靠近开关电流源3.4 靠近电流感测放大器3.5 靠近稳压器 tips&#xff1a;资料主要来自网络…

Spring Boot 多环境配置:YML 文件的三种高效方法

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…