手把手带你从零开始写一个视频通话功能

开始

屁股们,我又来整花活了~~~

因为上次用go写了websockt中间件后,我就在想文章里说的那些websocket适用场景是不是扯的有点远了?

【带你用golang手撸一个websocket中间件】

这不,为了让你们学会用推送,这里又整了视频通话的示例~

是不是很奇怪?

不是说websocket是用来做推送的吗?和视频通话有什么关系呢?

没想到吧,websocket不仅能退字符串消息还能当 WebRTC 信令服务器呢😁?

先来看看最终效果 👇🏻

关键是,这样实现的视频通话完全不用依托其他任何第三库SDK哦,各位屁股可以根据下面的h5示例代码任意定制你的语音、视频通话功能!

(怎么样?是不是又有和老板提加薪的底气了 O(∩_∩)O~)

代码示例

【带你用golang手撸一个websocket中间件】

运行示例以前,大家需要先根据上面的文章启动好服务端中间件,这里就不重复说了~

前端代码:

xml 复制代码
<!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: 100px;
		}
		.video-box {
			position: relative;
			width: 800px;
			height: 400px;
		}
		#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: 240px;
			height: 120px;
			object-fit: cover;
			border: 1px solid #eee;
			background-color: #EBEEF5;
		}
		.start-button {
			position: absolute;
			left: 50%;
			top: 50%;
			width: 100px;
			line-height: 40px;
			outline: none;
			color: #fff;
			background-color: #409eff;
			border: none;
			border-radius: 4px;
			cursor: pointer;
			transform: translate(-50%, -50%);
		}
		.logger {
			width: 100%;
			position: fixed;
			bottom: 0;
			height: 150px;
			padding: 14px;
			overflow-y: auto;
			line-height: 1.5;
			color: #4fbf40;
			background-color: #272727;
		}
		.logger .error, .error .time {
			background-color: #DD4A68 !important;
			color: #fff;
		}
		.logger .time {
			background-color: yellow;
			padding-left: 10px;
			margin-right: 5px;
		}
		.token {
			width: 100%;
			color: #4fbf40;
			background-color: #272727;
			font-size: 18px;
			padding: 10px;
		}
		.token .col-12{
			display: inline-block;
		}
		.token input {
			border-radius: 5px;
			height: 30px;
			width: 300px;
			padding: 5px;
		}
	</style>
</head>
<body>
	<div class="token">
		<div class="col-12">
			<label for="name">我本次的clientID</label>
			<input class="my-client" type="text" readonly>
		</div>
		<div class="col-12">
			<label for="name">对方本次的clientID</label>
			<input class="to-client" type="text">
		</div>
	</div>
	<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>
	<div class="logger"></div>
    <script src="/static/js/jquery-3.0.0.min.js"></script>
	<script>

		// 角色标识(answer = 应答方,offer = 发起方)
		let role = "answer"
		
		const dump = {
			
			el: document.querySelector('.logger'),
			log(msg) {
				this.addLog(msg);
			},
			error(msg) {
				this.addLog(msg, 'error');
			},
			addLog(msg, type = '') {
				this.el.innerHTML += `<span ${ type ? `class="${type}"` : ''}><a class='time'>${new Date().toLocaleTimeString()} > </a> ${msg}</span><br/>`;
				this.scrollToBottom();
			},
			scrollToBottom() {
				this.el.scrollTop = this.el.scrollHeight;
			}
		};

		let stream;
		const localVideo = document.querySelector('#local-video');
		const remoteVideo = document.querySelector('#remote-video');

		localVideo.onloadeddata = function() {
			dump.log('播放本地视频');
			localVideo.play();
		}

		remoteVideo.onloadeddata = function() {
			dump.log('播放对方视频');
			remoteVideo.play();
		}

        dump.log('本次测试角色为:' + (role == 'offer' ? '发起方' : '接收方'))

		dump.log('信令通道(WebSocket)创建中......');
                // 这里改成websocket中间件运行的端口
		const ws = new WebSocket('ws://localhost:8282');

		ws.onopen = function() {

			dump.log('信令通道创建成功!');
		}

		ws.onerror = function() {

			dump.error('信令通道创建失败!');
		} 

		ws.onclose = function(event) {

			dump.error('信令通道已断开!')
		}

		ws.onmessage = function(e) {

            try {

                const { type, sdp, iceCandidate } = JSON.parse(e.data)
                if (type === 'answer') { // 应答请求

                    peer.setRemoteDescription(new RTCSessionDescription({type, sdp}))

                } else if (type === 'answer_ice') { // 应答请求ice

                    peer.addIceCandidate(iceCandidate)

                } else if (type === 'offer') {	// 发起请求

                    startLive(new RTCSessionDescription({type, sdp}))

                } else if (type === 'offer_ice') { // 发起请求ice

                    peer.addIceCandidate(iceCandidate);
                }

            } catch {

				$('.my-client').val(e.data)
            }
		};

		const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;

		if (!PeerConnection) {

			dump.error('浏览器不支持WebRTC!');
		}
		
		const peer = new PeerConnection();

		/**
		* @description: 接受视频流信息
		* @param {*} e
		* @return {*}
		*/		
		peer.ontrack = function(e) {

			if (e && e.streams) {

				dump.log('收到对方音频/视频流数据...');
				remoteVideo.srcObject = e.streams[0];
			}
		}

		/**
		* @description: 当对方出现断线时
		* @param {*} e
		* @return {*}
		*/		
		peer.oniceconnectionstatechange = function(e) {

			const iceConnectionState = peer.iceConnectionState;

			dump.log('ICE连接状态: ' + iceConnectionState);
		
			if (iceConnectionState === 'disconnected' || iceConnectionState === 'failed') {

				// 处理连接断开的逻辑
				localVideo.srcObject = null; // 停止播放本地视频
      			remoteVideo.srcObject = null; // 停止播放远程视频
				dump.error('连接断开!');

				// 回收摄像头/麦克风权限
				stream.getTracks().forEach(track => {
					track.stop();
				});

				$('.video-box button').show()
			}
		}

		/**
		* @description: 接收到视频通话请求
		* @param {*} e
		* @return {*}
		*/		
		peer.onicecandidate = function(e) {
			
			if (e.candidate) {
				dump.log('搜集并发送候选人');
                $.post('send', {id: $('.to-client').val(), msg: JSON.stringify({type: `${role}_ice`, iceCandidate: e.candidate})})
			} else {
				dump.log('候选人收集完成!');
			}
		};

		/**
		* @description: 发起视频通话请求
		* @param {*} offerSdp
		* @return {*}
		*/		
		async function startLive (offerSdp) {
			
			// 将状态切换至发起请求方
			role = "offer"

			$('.video-box button').hide()
			
			dump.log('正在尝试调取本地摄像头/麦克风...');

			try {
				
				stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
				
				dump.log('摄像头/麦克风权限获取成功!');
				localVideo.srcObject = stream;

			} catch {

				dump.error('摄像头/麦克风权限获取失败!');
				return;
			}

			dump.log(`------ 视频通话流程开始 ------`);
			dump.log('将媒体轨道添加到轨道集');

			stream.getTracks().forEach(track => {
				peer.addTrack(track, stream);
			});

			if (!offerSdp) {
				// 发起方
				dump.log('作为发起方创建本地SDP');
				const offer = await peer.createOffer();
				await peer.setLocalDescription(offer);
				
				dump.log(`传输发起方本地SDP`);

                $.post('send', {id: $('.to-client').val(), msg: JSON.stringify(offer)})

			} else {

				// 应答方
				dump.log('作为应答方收到对方SDP');
				await peer.setRemoteDescription(offerSdp);

				dump.log('创建应答方SDP');
				const answer = await peer.createAnswer();
				dump.log(`传输应答方SDP`);
				
                $.post('send', {id: $('.to-client').val(), msg: JSON.stringify(answer)})

				await peer.setLocalDescription(answer);
			}
		}
	</script>
</body>
</html>

后端代码(PHP):

php 复制代码
<?php
/*
 * @Author: psq
 * @Date: 2024-03-12 10:34:29
 * @LastEditors: psq
 * @LastEditTime: 2024-03-12 17:45:23
 */


namespace app\controller;

use think\facade\View;

use app\BaseController;

class Rtc extends BaseController {

    public function page() 
    {   
        return View::fetch();    
    }
    
    public function send()
    {
        $grpc = new \Proto\SendMessageToClientRequest();
        $grpc->setClientid(input('post.id'));
        $grpc->setMessage(input('post.msg'));
        // 这里改成你的GRPC端口
        list($response, $status) = (new \Clients('192.168.1.3:8182'))->SendMessageToClient($grpc)->wait();

        return json([
            'result'    =>  $response->getResult(),
            'id'        =>  input('post.id'),
            'msg'       =>  input('post.msg')
        ]);
    }
}

最好

好了,搞定~,就这么简单!

相关推荐
百万蹄蹄向前冲6 分钟前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
The Open Group24 分钟前
英特尔公司Darren Pulsipher 博士:以架构之力推动政府数字化转型
大数据·人工智能·架构
追逐时光者32 分钟前
.NET 使用 MethodTimer 进行运行耗时统计提升代码的整洁性与可维护性!
后端·.net
朝阳5811 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路1 小时前
GeoTools 读取影像元数据
前端
ssshooter1 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友1 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
曼岛_2 小时前
[系统架构设计师]系统质量属性与架构评估(八)
架构·系统架构
Jerry2 小时前
Jetpack Compose 中的状态
前端
AlbertZein3 小时前
HarmonyOS5 凭什么学鸿蒙—— GetContext
架构·harmonyos