【无标题】

简单说下,前端实现共享屏幕主要用到 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打开

接收者创建连接

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

好了,完结撒花
谢谢包子们的观看
————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_52072601/article/details/139160041

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值