开始
屁股们,我又来整花活了~~~
因为上次用go写了websockt中间件后,我就在想文章里说的那些websocket适用场景是不是扯的有点远了?
这不,为了让你们学会用推送,这里又整了视频通话的示例~
是不是很奇怪?
不是说websocket是用来做推送的吗?和视频通话有什么关系呢?
没想到吧,websocket不仅能退字符串消息还能当 WebRTC 信令服务器呢😁?
先来看看最终效果 👇🏻

关键是,这样实现的视频通话完全不用依托其他任何第三库SDK哦,各位屁股可以根据下面的h5示例代码任意定制你的语音、视频通话功能!
(怎么样?是不是又有和老板提加薪的底气了 O(∩_∩)O~)
代码示例
运行示例以前,大家需要先根据上面的文章启动好服务端中间件,这里就不重复说了~
前端代码:
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')
]);
}
}
最好
好了,搞定~,就这么简单!
