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

开始

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

因为上次用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')
        ]);
    }
}

最好

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

相关推荐
qq_251836457几秒前
基于springboot vue3 在线考试系统设计与实现 源码数据库 文档
数据库·spring boot·后端
2401_8581205321 分钟前
古典舞在线交流平台:SpringBoot设计与实现详解
java·spring boot·后端
赐你岁月如歌33 分钟前
如何使用ssm实现基于web的网站的设计与实现+vue
java·后端·ssm
时清云34 分钟前
【算法】合并两个有序链表
前端·算法·面试
小爱丨同学42 分钟前
宏队列和微队列
前端·javascript
持久的棒棒君1 小时前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_857297911 小时前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋2 小时前
Web和UE5像素流送、通信教程
前端·ue5
潘多编程3 小时前
Spring Boot微服务架构设计与实战
spring boot·后端·微服务
2402_857589363 小时前
新闻推荐系统:Spring Boot框架详解
java·spring boot·后端