11┃音视频直播系统之 WebRTC 进行文本聊天并实时传输文件

11┃音视频直播系统之 WebRTC 进行文本聊天并实时传输文件

学会文本聊天并传输文件

一、RTCDataChannel

  • WebRTC不但可以让你进行音视频通话,而且还可以用它传输普通的二进制数据,比如说可以利用它实现文本聊天、文件的传输等
  • WebRTC 的数据通道 (RTCDataChannel)是专门用来传输除了音视频数据之外的任何数据,模仿了 WebSocket的实现
  • RTCDataChannel支持的数据类型也非常多,包括: 字符串BlobArrayBuffer以及 ArrayBufferView
  • WebRTCRTCDataChannel使用的传输协议为 SCTP,即 Stream Control Transport Protocol
  • RTCDataChannel既可以在可靠的、有序的模式下工作,也可在不可靠的、无序的模式下工作
  • 可靠有序模式(TCP 模式):在这种模式下,消息可以有序到达,但同时也带来了额外的开销,所以在这种模式下消息传输会比较慢
  • 不可靠无序模式(UDP 模式):在此种模式下,不保证消息可达,也不保证消息有序,但在这种模式下没有什么额外开销,所以它非常快
  • 部分可靠模式(SCTP 模式):在这种模式下,消息的可达性和有序性可以根据业务需求进行配置
  • RTCDataChannel对象是由 RTCPeerConnection对象创建,其中包含两个参数:
  • 第一个参数:是一个标签(字符串),相当于给 RTCDataChannel 起了一个名字
  • 第二个参数:是 options,包含很多配置,其中就可以设置上面说的模式,重试次数等
// 创建 RTCPeerConnection 对象
var pc = new RTCPeerConnection();

// 创建 RTCDataChannel 对象
var dc = pc.createDataChannel("dc", {
    ordered: true // 保证到达顺序
});

// options参数详解, 前三项是经常使用的:
// ordered:消息的传递是否有序
// maxPacketLifeTime:重传消息失败的最长时间
// maxRetransmits:重传消息失败的最大次数
// protocol:用户自定义的子协议, 默认为空
// negotiated:如果为 true,则会删除另一方数据通道的自动设置
// id:当 negotiated 为 true 时,允许你提供自己的 ID 与 channel 进行绑定

// dc的事件处理与 WebSocket 的事件处理非常相似
dc.onerror = (error) => {
    // 出错的处理
};
dc.onopen = () => {
    // 打开的处理
};
dc.onclose = () => {
    // 关闭的处理
};
dc.onmessage = (event) => {
    // 收到消息的处理
    var msg = event.data;
};

二、文本聊天

  • 点击 Start 按钮时,会调用 start方法获取视频流然后 调用 conn 方法
  • 然后调用 io.connect() 连接信令服务器,然后再根据信令服务器下发的消息做不同的处理
  • 数据的发送非常简单,当用户点击 Send按钮后,文本数据就会通过 RTCDataChannel传输到远端
  • 对于接收数据,则是通过 RTCDataChannelonmessage事件实现的
  • RTCDataChannel对象的创建要在媒体协商 (offer/answer) 之前创建,否则 WebRTC就会一直处于 connecting状态,从而导致数据无法进行传输
  • RTCDataChannel对象是可以双向传输数据的,所以接收与发送使用一个 RTCDataChannel对象即可,而不需要为发送和接收单独创建 RTCDataChannel对象
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .preview {
            display: flex;
        }

        .remote {
            margin-left: 20px;
        }

        .text_chat {
            display: flex;
        }

        .text_chat textarea {
            width: 350px;
            height: 350px;
        }

        .send {
            margin-top: 20px;
        }
    </style>
</head>

<body>
    <div>
        <div>
            <button onclick="start()">&#x8FDE;&#x63A5;&#x4FE1;&#x4EE4;&#x670D;&#x52A1;&#x5668;</button>
            <button onclick="leave()" disabled>&#x65AD;&#x5F00;&#x8FDE;&#x63A5;</button>
        </div>

        <div class="preview">
            <div>
                <h2>&#x672C;&#x5730;:</h2>
                <video id="localvideo" autoplay playsinline></video>
            </div>
            <div class="remote">
                <h2>&#x8FDC;&#x7AEF;:</h2>
                <video id="remotevideo" autoplay playsinline></video>
            </div>
        </div>
        <!--文本聊天-->
        <h2>&#x804A;&#x5929;:</h2>

        <div class="send">
            <button onclick="send()" disabled>&#x53D1;&#x9001;</button>
        </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
</body>
<script>
    'use strict'

    var localVideo = document.querySelector('video#localvideo');
    var remoteVideo = document.querySelector('video#remotevideo');

    // 文本聊天
    var chat = document.querySelector('textarea#chat');
    var send_txt = document.querySelector('textarea#sendtext');

    var localStream = null;

    var roomid = '44444';
    var socket = null;

    var state = 'init';

    var pc = null;
    var dc = null;

    function sendMessage(roomid, data) {
        socket.emit('message', roomid, data);
    }

    function getAnswer(desc) {
        pc.setLocalDescription(desc);
        // 发送信息
        socket.emit('message', roomid, desc);
    }

    function handleAnswerError(err) {
        console.error('Failed to get Answer!', err);
    }

    //接收远端流通道
    function call() {
        if (state === 'joined_conn') {
            if (pc) {
                var options = {
                    offerToReceiveAudio: 1,
                    offerToReceiveVideo: 1
                }
                pc.createOffer(options)
                    .then(function (desc) {
                        pc.setLocalDescription(desc);
                        socket.emit('message', roomid, desc);
                    })
                    .catch(function (err) {
                        console.error('Failed to get Offer!', err);
                    });
            }
        }
    }

    //文本对方传过来的数据
    function reveivemsg(e) {
        var msg = e.data;
        console.log('recreived msg is :' + e.data);
        if (msg) {
            chat.value += '->' + msg + '\r\n';
        } else {
            console.error('recreived msg is null');
        }
    }

    function dataChannelStateChange() {
        var readyState = dc.readyState;
        if (readyState === 'open') {
            send_txt.disabled = false;
            btnSend.disabled = false;
        } else {
            send_txt.disabled = true;
            btnSend.disabled = true;
        }
    }

    function dataChannelError(error) {
        console.log("Data Channel Error:", error);
    }

    function conn() {
        //1 触发socke连接
        socket = io.connect();

        //2 加入房间后的回调
        socket.on('joined', (roomid, id) => {

            state = 'joined';

            createPeerConnection();

            btnConn.disabled = true;
            btnLeave.disabled = false;

            console.log("reveive joined message:state=", state);
        });
        socket.on('otherjoin', (roomid, id) => {

            if (state === 'joined_unbind') {
                createPeerConnection();
            }

            var dataChannelOptions = {
                ordered: true, //保证到达顺序
            };
            //文本聊天
            dc = pc.createDataChannel('dataChannel', dataChannelOptions);
            dc.onmessage = reveivemsg;
            dc.onopen = dataChannelStateChange;
            dc.onclose = dataChannelStateChange;
            dc.onerror = dataChannelError;

            state = 'joined_conn';

            //媒体协商
            call();
            console.log("reveive otherjoin message:state=", state);
        });
        socket.on('full', (roomid, id) => {
            console.log('receive full message ', roomid, id);

            closePeerConnection();
            closeLocalMedia();

            state = 'leaved';

            btnConn.disabled = false;
            btnLeave.disabled = true;
            console.log("reveive full message:state=", state);
            alert("the room is full!");
        });

        socket.on('leaved', (roomid, id) => {

            state = 'leaved';
            socket.disconnect();
            btnConn.disabled = false;
            btnLeave.disabled = true;
            console.log("reveive leaved message:state=", state);
        });

        socket.on('bye', (roomid, id) => {

            state = 'joined_unbind';
            closePeerConnection();
            console.log("reveive bye message:state=", state);
        });
        socket.on('disconnect', (socket) => {
            console.log('receive disconnect message!', roomid);
            if (!(state === 'leaved')) {
                closePeerConnection();
                closeLocalMedia();
            }
            state = 'leaved';

        });
        socket.on('message', (roomid, id, data) => {
            console.log(" message=====>", data);
            //媒体协商
            if (data) {
                if (data.type === 'offer') {
                    pc.setRemoteDescription(new RTCSessionDescription(data));
                    pc.createAnswer()
                        .then(getAnswer)
                        .catch(handleAnswerError);
                } else if (data.type === 'answer') {
                    console.log("reveive client message=====>", data);
                    pc.setRemoteDescription(new RTCSessionDescription(data));
                } else if (data.type === 'candidate') {
                    var candidate = new RTCIceCandidate({
                        sdpMLineIndex: data.label,
                        candidate: data.candidate
                    });
                    pc.addIceCandidate(candidate);

                } else {
                    console.error('the message is invalid!', data)
                }
            }

            console.log("reveive client message", roomid, id, data);
        });

        socket.emit('join', roomid);
        return;
    }

    function start() {
        if (!navigator.mediaDevices ||
            !navigator.mediaDevices.getUserMedia) {
            console.log("getUserMedia is not supported!")
            return;
        }

        navigator.mediaDevices.getUserMedia({
            video: true,
            audio: false
        })
            .then(function (stream) {
                localStream = stream;
                localVideo.srcObject = localStream;
                conn();
            })
            .catch(function (err) {
                console.error("getUserMedia  error:", err);
            })
    }

    function leave() {
        if (socket) {
            socket.emit('leave', roomid);
        }

        //释放资源
        closePeerConnection();
        closeLocalMedia();

        btnConn.disabled = false;
        btnLeave.disabled = true;
    }

    //关闭流通道
    function closeLocalMedia() {
        if (localStream && localStream.getTracks()) {
            localStream.getTracks().forEach((track) => {
                track.stop();
            });
        }
        localStream = null;
    }

    //关闭本地媒体流链接
    function closePeerConnection() {
        console.log('close RTCPeerConnection!');
        if (pc) {
            pc.close();
            pc = null;
        }
    }

    //创建本地流媒体链接
    function createPeerConnection() {
        console.log('create RTCPeerConnection!');
        if (!pc) {
            pc = new RTCPeerConnection({
                'iceServers': [{
                    'urls': 'turn:127.0.0.1:8000',
                    'credential': '123456',
                    'username': 'autofelix'
                }]
            });

            pc.onicecandidate = (e) => {
                if (e.candidate) {
                    sendMessage(roomid, {
                        type: 'candidate',
                        label: e.candidate.sdpMLineIndex,
                        id: e.candidate.sdpMid,
                        candidate: e.candidate.candidate
                    });
                }
            }

            //文本聊天
            pc.ondatachannel = e => {
                dc = e.channel;
                dc.onmessage = reveivemsg;
                dc.onopen = dataChannelStateChange;
                dc.onclose = dataChannelStateChange;
                dc.onerror = dataChannelError;
            }

            pc.ontrack = (e) => {
                remoteVideo.srcObject = e.streams[0];
            }
        }

        if (pc === null || pc === undefined) {
            console.error('pc is null or undefined!');
            return;
        }

        if (localStream === null || localStream === undefined) {
            console.error('localStream is null or undefined!');
            return;
        }

        if (localStream) {
            localStream.getTracks().forEach((track) => {
                pc.addTrack(track, localStream);
            })
        }
    }

    //发送文本
    function send() {
        var data = send_txt.value;
        if (data) {
            dc.send(data);
        }
        send_txt.value = "";
        chat.value += '<-' + data + '\r\n';
    }
</script>

</html>

三、文件传输

  • 实时文件的传输与实时文本消息传输的基本原理是一样的,都是使用 RTCDataChannel 对象进行传输
  • 它们的区别一方面是传输数据的类型不一样,另一方面是数据的大小不一样
  • 在传输文件的时候,必须要保证文件传输的有序性和完整性,所以需要设置 ordered 和 maxRetransmits 选项
  • 发送数据如下:
// &#x521B;&#x5EFA; RTCDataChannel &#x5BF9;&#x8C61;&#x7684;&#x9009;&#x9879;
var options = {
    ordered: true,
    maxRetransmits: 30 // &#x6700;&#x591A;&#x5C1D;&#x8BD5;&#x91CD;&#x4F20; 30 &#x6B21;
};

// &#x521B;&#x5EFA; RTCPeerConnection &#x5BF9;&#x8C61;
var pc = new RTCPeerConnection();

// &#x65B9;&#x6CD5;&#x4E00;&#xFF1A;&#x901A;&#x8FC7;&#x901A;&#x9053;&#x53D1;&#x9001;
sendChannel = pc.createDataChannel(name, options)&#xFF1B;
sendChannel.addEventListener('open', onSendChannelStateChange); //&#x6253;&#x5F00;&#x4E4B;&#x540E;&#x624D;&#x53EF;&#x4EE5;&#x4F20;&#x8F93;&#x6570;&#x636E;
sendChannel.addEventListener('close', onSendChannelStateChange);
sendChannel.send(JSON.stringify({
    // &#x5C06;&#x6587;&#x4EF6;&#x4FE1;&#x606F;&#x4EE5; JSON &#x683C;&#x5F0F;&#x53D1;&#x78C5;
    type: 'fileinfo',
    name: file.name,
    size: file.size,
    filetype: file.type,
    lastmodify: file.lastModified
}));

// &#x65B9;&#x6CD5;&#x4E8C;&#xFF1A;&#x901A;&#x8FC7;arraybuffer&#x53D1;&#x9001;
var offset = 0; // &#x504F;&#x79FB;&#x91CF;
var chunkSize = 16384; // &#x6BCF;&#x6B21;&#x4F20;&#x8F93;&#x7684;&#x5757;&#x5927;&#x5C0F;
var file = fileInput.files[0]; // &#x8981;&#x4F20;&#x8F93;&#x7684;&#x6587;&#x4EF6;&#xFF0C;&#x5B83;&#x662F;&#x901A;&#x8FC7; HTML &#x4E2D;&#x7684; file &#x83B7;&#x53D6;&#x7684;

// &#x521B;&#x5EFA; fileReader &#x6765;&#x8BFB;&#x53D6;&#x6587;&#x4EF6;
fileReader = new FileReader();

// &#x5F53;&#x6570;&#x636E;&#x88AB;&#x52A0;&#x8F7D;&#x65F6;&#x89E6;&#x53D1;&#x8BE5;&#x4E8B;&#x4EF6;
fileReader.onload = e => {
    // &#x53D1;&#x9001;&#x6570;&#x636E;
    dc.send(e.target.result);
    offset += e.target.result.byteLength; // &#x66F4;&#x6539;&#x5DF2;&#x8BFB;&#x6570;&#x636E;&#x7684;&#x504F;&#x79FB;&#x91CF;

    if (offset < file.size) { // &#x5982;&#x679C;&#x6587;&#x4EF6;&#x6CA1;&#x6709;&#x88AB;&#x8BFB;&#x5B8C;
        readSlice(offset); // &#x8BFB;&#x53D6;&#x6570;&#x636E;
    }
}

var readSlice = o => {
    const slice = file.slice(offset, o + chunkSize); // &#x8BA1;&#x7B97;&#x6570;&#x636E;&#x4F4D;&#x7F6E;
    fileReader.readAsArrayBuffer(slice); // &#x8BFB;&#x53D6; 16K &#x6570;&#x636E;
};
readSlice(0); // &#x5F00;&#x59CB;&#x8BFB;&#x53D6;&#x6570;&#x636E;
  • 接收数据如下:
  • 当有数据到达时就会触发该事件就会触发 onmessage 事件
  • 只需要简单地将收到的这块数据 push 到 receiveBuffer 数组中即可
var receiveBuffer = []; // &#x5B58;&#x653E;&#x6570;&#x636E;&#x7684;&#x6570;&#x7EC4;
var receiveSize = 0; // &#x6570;&#x636E;&#x5927;&#x5C0F;

onmessage = (event) => {
    // &#x6BCF;&#x6B21;&#x4E8B;&#x4EF6;&#x88AB;&#x89E6;&#x53D1;&#x65F6;&#xFF0C;&#x8BF4;&#x660E;&#x6709;&#x6570;&#x636E;&#x6765;&#x4E86;&#xFF0C;&#x5C06;&#x6536;&#x5230;&#x7684;&#x6570;&#x636E;&#x653E;&#x5230;&#x6570;&#x7EC4;&#x4E2D;
    receiveBuffer.push(event.data);
    // &#x66F4;&#x65B0;&#x5DF2;&#x7ECF;&#x6536;&#x5230;&#x7684;&#x6570;&#x636E;&#x7684;&#x957F;&#x5EA6;
    receivedSize += event.data.byteLength;
    // &#x5982;&#x679C;&#x63A5;&#x6536;&#x5230;&#x7684;&#x5B57;&#x8282;&#x6570;&#x4E0E;&#x6587;&#x4EF6;&#x5927;&#x5C0F;&#x76F8;&#x540C;&#xFF0C;&#x5219;&#x521B;&#x5EFA;&#x6587;&#x4EF6;
    if (receivedSize === fileSize) { //fileSize &#x662F;&#x901A;&#x8FC7;&#x4FE1;&#x4EE4;&#x4F20;&#x8FC7;&#x6765;&#x7684;
        // &#x521B;&#x5EFA;&#x6587;&#x4EF6;
        var received = new Blob(receiveBuffer, { type: 'application/octet-stream' });
        // &#x5C06; buffer &#x548C; size &#x6E05;&#x7A7A;&#xFF0C;&#x4E3A;&#x4E0B;&#x4E00;&#x6B21;&#x4F20;&#x6587;&#x4EF6;&#x505A;&#x51C6;&#x5907;
        receiveBuffer = [];
        receiveSize = 0;
        // &#x751F;&#x6210;&#x4E0B;&#x8F7D;&#x5730;&#x5740;
        downloadAnchor.href = URL.createObjectURL(received);
        downloadAnchor.download = fileName;
        downloadAnchor.textContent = Click to download '${fileName}' (${fileSize} bytes);
        downloadAnchor.style.display = 'block';
    }
}

Original: https://www.cnblogs.com/sunnyeden/p/16292181.html
Author: sunnyeden
Title: 11┃音视频直播系统之 WebRTC 进行文本聊天并实时传输文件

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/597043/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球