0


前端实现多人共享屏幕

上班需要啊啊啊啊啊啊啊!!!!!!!

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

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

好了,完结撒花

谢谢包子们的观看

标签: 前端

本文转载自: https://blog.csdn.net/qq_52072601/article/details/139160041
版权归原作者 云也有苦恼 所有, 如有侵权,请联系我们删除。

“前端实现多人共享屏幕”的评论:

还没有评论