前端实现多人共享屏幕
云也有苦恼 2024-06-29 17:03:09 阅读 50
上班需要啊啊啊啊啊啊啊!!!!!!!
简单说下,前端实现共享屏幕主要用到 websocket 、 RTCPeerConnection、navigator.mediaDevices.getDisplayMedia({ video: true })这三个API
先看最终效果
websocket
这个必须实现,因为共享屏幕需要交换协议,通过websocket发送协议到服务器,然后转发给远程,交换双发协议,才能实现共享屏幕。其他方式发送也可以,不过考虑到共享屏幕伴随着聊天室,所以还是用websocket吧,学就完事儿了。
RTCPeerConnection
这个是实现共享屏幕的重点,就是用于前端点对点网络传输的,想要实时传输视频流,这个是重点。因为我试过用canvas获取视频帧,然后toImageDate,然后上传图片信息。确实可以实现,但是非常卡!巨卡!因为图片信息太大了!!!
还有就是要注意,这个是一对一的,就是一个发送者只能服务一个接受者,但是这难不住前端人!我直接把发送者装进数组,连接成功一个,就新建一个空闲的发送者,这样就可以一对多了。
navigator.mediaDevices.getDisplayMedia({ video: true })
这个是前端共享屏幕的API,它会返回一个视频流,通过RTCPeerConnection交换视频流,实现共享屏幕。
第一步 后端
首先要连接websocket ,后端的话各有不同,就只能麻烦各位自行百度了,我这里是用的django(上班用的)框架,就是python后端
这三个得安装一下
pip install channels
pip install daphne
pip install dwebsocket
settings.py
INSTALLED_APPS = [
'daphne',
'django.contrib.staticfiles',
'channels',
]
# 顺序不能错 有依赖关系
# 这个最好放在后面
ASGI_APPLICATION = 'wiseHorse.asgi.application'
注意啊注意,这个 wiseHorse 是我的项目根目录,因人而异,切记,不然后面找不到路径。
添加文件
在根目录下添加 asgi.py 和 routing.py 这两个文件.
asgi.py
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from wiseHorse import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bug_Project2.settings')
# application = get_asgi_application()
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": URLRouter(routing.websoctet_urlpatterns)
})
routing.py
from django.urls import re_path
# 这个是我的 infomation 目录下的 chat.py 文件,因人而异哈
from information import chat
websoctet_urlpatterns = [
# chat.ChatConsumer.as_asgi() 这个同理哈 .as_asgi()不能变
re_path('ws/room/', chat.ChatConsumer.as_asgi()),
]
chat.py
这里我的处理比较多,因人而异哈
import json
from channels.generic.websocket import WebsocketConsumer
# 记录连接人数
count = 0
# 存储连接websocket的用户
# 格式 { room_name :[user_1,user_2,...] }
meeting_info = {}
# 保存用户的room_name 和 self 连接断开时从 meeting_info 中移除 并通知其他用户
user_info_list = {}
# 储存会议的 offer 格式{ room_name : [offer, self] }
meeting_offer = {}
# 储存会议的 ice 格式{ room_name : [ice, self] }
meeting_ice = {}
class ChatConsumer(WebsocketConsumer):
# 当用户连接时
def websocket_connect(self, message):
self.accept()
# 当用户发送消息时
def websocket_receive(self, message):
# 浏览器基于websocket向后端发送数据,自动触发接受消息
data = json.loads(message['text'])
# 加入房间
if data.get('type') == 'join':
# 如果房间存在
if data.get('room_name') in meeting_info:
meeting_info[data.get('room_name')].append(self)
# 将用户的 self_name self room_name 保存到 user_list 中
user_info_list[self] = data.get('room_name')
self.send('{"type":"success","msg":"joined the room"}')
# 房间不存在
else:
self.send('{"type":"error","msg":"room is not exits"}')
# 创建房间
elif data.get('type') == 'create':
# 如果房间已存在 则创建失败
if data.get('room_name') in meeting_info:
self.send('{"type":"error","msg":"room is exits"}')
# 房间不存在 创建成功
else:
# 创建房间
meeting_info[data.get('room_name')] = []
meeting_info[data.get('room_name')].append(self)
# 将用户的 self_name self room_name 保存到 user_list 中
user_info_list[self] = data.get('room_name')
self.send('{"type":"success","msg":"room created"}')
# 离开房间
elif data.get('type') == 'leave':
meeting_info[data.get('room_name')].remove(self)
self.send('{"type":"success","msg":"leaved the room"}')
# 发送消息
elif data.get('type') == 'chat':
# 先检查是否加入了房间
if self not in meeting_info[data.get('room_name')]:
msg = 'not joined room ' + data.get('room_name')
self.send(json.dumps({"type": "chat_error", "msg": msg}))
else:
for room in meeting_info[data.get('room_name')]:
room.send(json.dumps({"type": "chat_success", "msg": data.get('content')}))
# 关闭房间
elif data.get('type') == "close":
# 通知所有人 房间已经解散
for room in meeting_info[data.get('room_name')]:
room.send('{"type":"success","msg":"The room has been disbanded"}')
room.close()
# 删除房间信息
meeting_info.pop(data.get('room_name'))
# ice
elif data.get('type') == 'ice':
# 发起者的ice
if data.get('is_sender'):
for index, room in enumerate(meeting_info[data.get('room_name')]):
if index != 0:
room.send(json.dumps({"type": "ice_success", "msg": data.get('ice')}))
# 接收者的ice
elif data.get('is_receiver'):
meeting_info[data.get('room_name')][0].send(json.dumps({"type": "ice_success", "msg": data.get('ice')}))
# offer
elif data.get('type') == 'offer':
# 如果 offer已经存在 就删除 因为是旧的 offer
if data.get('room_name') in meeting_offer:
meeting_offer.pop(data.get('room_name'))
# 设置 offer
meeting_offer[data.get('room_name')] = []
meeting_offer[data.get('room_name')].append(data.get('offer'))
meeting_offer[data.get('room_name')].append(self)
self.send(json.dumps({"type": "offer_success", "msg": "the room has added an offer"}))
# 获取 offer
elif data.get('type') == 'getoffer':
offer = meeting_offer[data.get('room_name')][0]
self.send(json.dumps({"type": "getoffer_success", "msg": offer}))
# answer
elif data.get('type') == 'answer':
# 房间存在 向房主发送answer
if data.get('room_name') in meeting_info:
meeting_offer[data.get('room_name')][1].send(json.dumps({"type": "answer_success", "msg": data.get('answer')}))
else:
self.send('{"type":"answer_error","msg":"room is not exits"}')
# 当用户断开连接时
def websocket_disconnect(self, message):
# 客户端与服务端断开连接,从meeting_info / user_info_list / meeting_ice / meeting_offer找到该用户
# 从会议中查找
try:
meeting_info[user_info_list[self]].remove(self)
except KeyError:
print('meeting_info not found')
# 从用户列表中查找
try:
user_info_list.pop(self)
except KeyError:
print('user_info_list not found')
# 对于发起共享屏幕者 移除offer 和 ice
try:
# 移除 offer
for item in meeting_offer:
if item[1] == self:
meeting_offer.pop(item)
break
# 移除ice
for item in meeting_ice:
if item[1] == self:
meeting_ice.pop(item)
break
except KeyError:
print('meeting_offer and meeting_ice not found')
第二步 前端
连接websocket 一定要打开控制台看看有没有连接成功!!!
let socket = new WebSocket('ws://localhost:8888/ws/room/');
// 连接 websocket 成功
socket.onopen = function () {
console.log('连接成功');
}
代码太多了,直接全上吧,注释也挺全的,各位慢慢消化
发送者 html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
position: relative;
}
#localVideo {
position: absolute;
top: 100px;
left: 0;
background-color: #fff;
padding: 0;
}
</style>
</head>
<body>
<h1>发送者</h1>
<video id="localVideo" width="600" height="400"></video>
</body>
<script>
// 连接 websocket
// let socket = new WebSocket(`ws://${window.location.host}/ws/room`)
let socket = new WebSocket('ws://localhost:8888/ws/room/');
// 获取本地video 也就是共享屏幕的视频数据容器
let localVideo = document.getElementById('localVideo');
// 视频流
let stream = null;
// 本地 peer
let pc = new RTCPeerConnection();
// peer数组 实现一对多
let pcArr = [];
// 当前已连接人数 默认1个人
let connectNum = 1;
let count = 0;
// 将 pc 添加到数组
pcArr.push(pc);
// 我是发起者(sender) 还是 接收者(recipients)
// 默认接收者
let my_indefent = 'recipients';
// 连接 websocket 成功
socket.onopen = function () {
console.log('连接成功');
}
// 创建房间
const createRoom = () => {
socket.send(JSON.stringify({
'type': 'create',
'room_name': '123',
}));
// 开始共享屏幕
startScreenSharing();
}
// 聊天
const chat = (str) => {
socket.send(JSON.stringify({
'type': 'chat',
'room_name': '123',
'content': str
}));
}
// 服务器发来消息
socket.onmessage = function (e) {
try {
data = JSON.parse(e.data);
if (data.type == 'success') {
console.log('success', data.msg);
}
if (data.type == 'error') {
console.log('error', data.msg);
}
if (data.type == 'chat_success') {
console.log('chat_success', data.msg);
}
if (data.type == 'chat_error') {
console.log('chat_error', data.msg);
}
if (data.type == 'ice_success') {
console.log('我是发起者,收到了ice');
count += 1;
if (count == 3) {
// 连接数 +1
connectNum += 1;
// 新建一个pc 对象 添加到数组
let pc = new RTCPeerConnection();
pcArr.push(pc);
// 重置
count = 0;
// 初始化
RTCInit();
}
pcArr[connectNum - 1].addIceCandidate(data.msg);
// pc.addIceCandidate(data.msg);
}
if (data.type == 'ice_error') {
console.log('ice_error', data.msg);
}
if (data.type == 'offer_success') {
console.log('offer_success', data.msg);
}
if (data.type == 'offer_error') {
console.log('offer_error', data.msg);
}
if (data.type == 'answer_success') {
// 设置远程描述
pcArr[connectNum - 1].setRemoteDescription(data.msg);
// ice
pcArr[connectNum - 1].onicecandidate = function (e) {
// 发送 ice 给对方
socket.send(JSON.stringify({
'type': 'ice',
'is_sender': true,
'room_name': '123',
'ice': e.candidate
}))
}
console.log('我是发起者,收到了answer');
}
if (data.type == 'answer_error') {
console.log('answer_error', data.msg);
}
}
catch (err) {
console.log(err);
}
}
// 获取屏幕共享流
async function startScreenSharing() {
// 共享屏幕 身份改为发起者 sender
my_indefent = 'sender';
try {
stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
localVideo.srcObject = stream;
localVideo.play();
// 设置 pc
RTCInit();
} catch (error) {
console.error('Error accessing screen stream:', error);
}
}
// 初始化 RTC
async function RTCInit() {
// 将屏幕流传送出去
stream.getTracks().forEach(track => pcArr[connectNum - 1].addTrack(track, stream));
// offer
const offer = await pcArr[connectNum - 1].createOffer();
await pcArr[connectNum - 1].setLocalDescription(offer);
// 发送 offer
socket.send(JSON.stringify({
'type': 'offer',
'room_name': '123',
'offer': offer
}))
// 发起者不用设置track
// track
pcArr[connectNum - 1].ontrack = function (e) {
}
}
</script>
</html>
接收者 html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>接收者</h1>
<video src="" id="ramoteVideo" width="600" height="600" autoplay muted></video>
<!-- <script src="./screen_2.js"></script> -->
</body>
<script>
// 连接 websocket
// let socket = new WebSocket(`ws://${window.location.host}/ws/room`)
let socket = new WebSocket('ws://localhost:8888/ws/room/');
// 获取本地video 也就是共享屏幕的视频数据容器
let ramoteVideo = document.getElementById('ramoteVideo');
// 本地 peer
let pc = new RTCPeerConnection();
// 我是发起者(sender) 还是 接收者(recipients)
// 默认接收者
let my_indefent = 'recipients';
// 连接 websocket 成功
socket.onopen = function () {
console.log('连接成功');
}
// 加入房间
const joinRoom = () => {
socket.send(JSON.stringify({
'type': 'join',
'room_name': '123',
}));
// 获取offer
socket.send(JSON.stringify({
'type': 'getoffer',
'room_name': '123',
}))
}
// 聊天
const chat = (str) => {
socket.send(JSON.stringify({
'type': 'chat',
'room_name': '123',
'content': str
}));
}
// 服务器发来消息
socket.onmessage = function (e) {
try {
data = JSON.parse(e.data);
if (data.type == 'success') {
console.log('success', data.msg);
}
if (data.type == 'error') {
console.log('error', data.msg);
}
if (data.type == 'chat_success') {
console.log('chat_success', data.msg);
}
if (data.type == 'chat_error') {
console.log('chat_error', data.msg);
}
if (data.type == 'ice_success') {
console.log('我是接收者,收到了 ice');
pc.addIceCandidate(data.msg);
}
if (data.type == 'ice_error') {
console.log('ice_error', data.msg);
}
if (data.type == 'offer_success') {
console.log('offer_success', data.msg);
}
if (data.type == 'offer_error') {
console.log('offer_error', data.msg);
}
if (data.type == 'answer_success') {
console.log('answer_success', data.msg);
}
if (data.type == 'answer_error') {
console.log('answer_error', data.msg);
}
if (data.type == 'getoffer_success') {
// 接收到 offer 设置远程描述
pc.setRemoteDescription(data.msg);
// 创建应答 answer
pc.createAnswer().then(answer => {
// 设置本地描述
pc.setLocalDescription(answer);
// 将answer发送给 发起者
socket.send(JSON.stringify({
'type': 'answer',
'room_name': '123',
"answer": answer
}))
})
// 接收到视频流 开始播放
pc.ontrack = event => {
if (ramoteVideo.srcObject !== event.streams[0]) {
ramoteVideo.srcObject = event.streams[0];
console.log('我是远端,收到了视频流 = >', event.streams[0]);
}
};
pc.onicecandidate = function (e) {
// 发送 ice 给发起者
socket.send(JSON.stringify({
'type': 'ice',
'is_receiver': true,
'room_name': '123',
'ice': e.candidate
}))
}
}
}
catch (err) {
console.log(err);
}
}
</script>
</html>
第三步
先打开 发送者 html 文件
控制台输入
createRoom()
ok 到这里发送者已经就绪 等待接收者连接配对
接收者 html打开
随后应该就可以看到共享屏幕成功啦,可以多开哈
好了,完结撒花
谢谢包子们的观看
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。