前端实现多人共享屏幕

云也有苦恼 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打开

随后应该就可以看到共享屏幕成功啦,可以多开哈

好了,完结撒花

谢谢包子们的观看



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。