WebRTC音视频开发读书笔记(六)

news2024/11/18 3:32:12

数据通道不仅可以发送文本消息, 还可以发送图片、二进制文件,将其类型binaryType属性设置成arraybuffer类型即可.

九\、文件传输

1、文件传输流程

(1)使用表单file打开本地文件

(2)使用FileReader读取文件的二进制数据

 (3)创建对等连接,本地连接及远端连接

  (4)创建发送数据通道

  (5)创建接收数据通道

   (6)将读取的二进制数据 切割成一个个切片,然后使用数据通道的send方法发送数据 。

  (7)使用接收数据通道二进制数据 将其放入数据缓存

  (8)根据数据缓存生成Blob 文件

   (10)生成文件连接并提供下载。

读取流程中文件二进制数据使用FileReader对象,还需要对数据进行切片处理,在接收方法中使用缓存将接收的数据缓存起来,最后生成blob下载文件。处理流程如下所示:

2、FileReader

FileReader对象允许Web应用程序异步读取存储在用户计算机上的文件内容。常用事件如下所示:

使用File或Blob对象指定要读取的文件或数据,当监听到load事件后,即可通过事件的结果属性拿到数据,然后使用发送通道的send方法将数据发送出去。代码如下:

sendChannel.send(e.target.result)

读取二进制数据可以使用FileReader的readAsArrayBuffer方法,结果为ArrayBuffer对象。还有其它处理方法,如下所示:

读取数据 时需要将数据分成一段一段的,然后发送,接收时再按顺序读取到数据缓存中,数据流的走向如图所示:

上图中数据块叫做Chunk,DataChannel每次发送的数据即为Chunk,  使用对象的slice方法按顺序切割 出数据块。如下面代码所示:

let slice=file.slice(offset,offset+chunksize)

假设chunksize的大小为16348,整个文件的数据块如下所示:

3、发送文件示例

本示例为文件具休步骤如下:

(1)首先在界面上添加input,类型为file,用于选择并读取文件,progress两个,分别用于发送及接收文件进度展示。

 (2)创建本地和远端连接,发送和接收数据通道,发送和接收通道的数据类型设置为arraybuffer。

  (3)建立本地和远端连接

  (4)实例化FileReader对象,并添加如下事件:

                      error:  读取文件错误

                     abort:  读取文件取消

                      load:  文件加载完成

         load事件表示数据已经准备好,可以进行切割发送,具体处理逻辑如下所示:

//监听load事件
fileReader.addEventListener('load',(e)=>{
    ...
   //发送文件数据
   sendChannel.send(e.target.result);
    //偏移量
   offer+=e.target.result.byteLength;
   ...
    //判断偏移量是否小于文件大小
   if(offer<file.size){
     //继续读取
     readSlice(offser)
  

   }
});
 //读取切片大小
let readSlice=(o)=>{
  ...
  //开始切片
  let slice=file.slice(offset ,o+chunksize);
 //读取二进制数据  
 fileReader.readAsArrayBuffer(slice);

}

   (5)数据接收处理,将收到第一个数据放入receiveBuffer缓存,处理逻辑如下所示:

//接收消息处理
onReceiveMessageCallback=(event)=>{
//将接收的数据添加到接收缓存里
    receiveBuffer.push(event.data);
//设置当前接收文件的大小
   receivedSize+=event.data.byteLength;
   ...
   const file=fileInput.files[0];
//判断当前接收的文件大小是否等于文件的大小
if(receivedSize===file.size){
   //根据缓存生成Blob文件
   const received=new Blob(receiveBuffer)
  //将缓存数据设置为空
  receiveBuffer=[];
  ...
 //创建下载文件对象及连接
  ...

    }
}

完整代码如下:

import React from "react";
import { Button } from "antd";

//本地连接对象
let localConnection;
//远端连接对象
let remoteConnection;
//发送通道
let sendChannel;
//接收通道
let receiveChannel;
//文件读取
let fileReader;
//接收数据缓存
let receiveBuffer = [];
//接收到的数据大小
let receivedSize = 0;
//文件选择
let fileInput;
//发送进度条
let sendProgress;
//接收进度条
let receiveProgress;

/**
 * 数据通道发送文件示例
 */
class DataChannelFile extends React.Component {

    componentDidMount() {

        sendProgress =document.getElementById('sendProgress') 
        receiveProgress =document.getElementById('receiveProgress')
        fileInput = document.getElementById('fileInput');
        //监听change事件,判断文件是否选择
        fileInput.addEventListener('change', async () => {
            const file = fileInput.files[0];
            if (!file) {
                console.log('没有选择文件');
            } else {
                console.log('选择的文件是:' + file.name);
            }
        });
    }

    //建立对等连接并发送文件
    startSendFile = async () => {
    
        //创建RTCPeerConnection对象
        localConnection = new RTCPeerConnection();
        console.log('创建本地PeerConnection成功:localConnection');
        //监听返回的Candidate信息
        localConnection.addEventListener('icecandidate', this.onLocalIceCandidate);

        //实例化发送通道
        sendChannel = localConnection.createDataChannel('webrtc-datachannel');
        //数据类型为二进制
        sendChannel.binaryType = 'arraybuffer';

        //onopen事件监听
        sendChannel.addEventListener('open', this.onSendChannelStateChange);
        //onclose事件监听
        sendChannel.addEventListener('close', this.onSendChannelStateChange);

        //创建RTCPeerConnection对象
        remoteConnection = new RTCPeerConnection();
        console.log('创建本地PeerConnection成功:remoteConnection');
        //监听返回的Candidate信息
        remoteConnection.addEventListener('icecandidate', this.onRemoteIceCandidate);

        //远端连接数据到达事件监听
        remoteConnection.addEventListener('datachannel', this.receiveChannelCallback);

        //监听ICE状态变化
        localConnection.addEventListener('iceconnectionstatechange', this.onLocalIceStateChange);
        //监听ICE状态变化
        remoteConnection.addEventListener('iceconnectionstatechange', this.onRemoteIceStateChange);

        try {
            console.log('localConnection创建提议Offer开始');
            //创建提议Offer
            const offer = await localConnection.createOffer();
            //创建Offer成功
            await this.onCreateOfferSuccess(offer);
        } catch (e) {
            //创建Offer失败
            this.onCreateSessionDescriptionError(e);
        }
    }

    //创建会话描述错误
    onCreateSessionDescriptionError = (error) => {
        console.log(`创建会话描述SD错误: ${error.toString()}`);
    }

    //创建提议Offer成功
    onCreateOfferSuccess = async (desc) => {
        //localConnection创建Offer返回的SDP信息
        console.log(`localConnection创建Offer返回的SDP信息\n${desc.sdp}`);
        console.log('设置localConnection的本地描述start');
        try {
            //设置localConnection的本地描述
            await localConnection.setLocalDescription(desc);
            this.onSetLocalSuccess(localConnection);
        } catch (e) {
            this.onSetSessionDescriptionError();
        }

        console.log('remoteConnection开始设置远端描述');
        try {
            //设置remoteConnection的远端描述
            await remoteConnection.setRemoteDescription(desc);
            this.onSetRemoteSuccess(remoteConnection);
        } catch (e) {
            //创建会话描述错误
            this.onSetSessionDescriptionError();
        }

        console.log('remoteConnection开始创建应答Answer');
        try {
            //创建应答Answer
            const answer = await remoteConnection.createAnswer();
            //创建应答成功
            await this.onCreateAnswerSuccess(answer);
        } catch (e) {
            //创建会话描述错误
            this.onCreateSessionDescriptionError(e);
        }
    }

    //设置本地描述完成
    onSetLocalSuccess = (pc) => {
        console.log(`${this.getName(pc)}设置本地描述完成:setLocalDescription`);
    }

    //设置远端描述完成
    onSetRemoteSuccess = (pc) => {
        console.log(`${this.getName(pc)}设置远端描述完成:setRemoteDescription`);
    }

    //设置描述SD错误
    onSetSessionDescriptionError = (error) => {
        console.log(`设置描述SD错误: ${error.toString()}`);
    }

    getName = (pc) => {
        return (pc === localConnection) ? 'localConnection' : 'remoteConnection';
    }

    //创建应答成功
    onCreateAnswerSuccess = async (desc) => {
        //输出SDP信息
        console.log(`remoteConnection的应答Answer数据:\n${desc.sdp}`);
        console.log('remoteConnection设置本地描述开始:setLocalDescription');
        try {
            //设置remoteConnection的本地描述信息
            await remoteConnection.setLocalDescription(desc);
            this.onSetLocalSuccess(remoteConnection);
        } catch (e) {
            this.onSetSessionDescriptionError(e);
        }
        console.log('localConnection设置远端描述开始:setRemoteDescription');
        try {
            //设置localConnection的远端描述,即remoteConnection的应答信息
            await localConnection.setRemoteDescription(desc);
            this.onSetRemoteSuccess(localConnection);
        } catch (e) {
            this.onSetSessionDescriptionError(e);
        }
    }

    //Candidate事件回调方法
    onLocalIceCandidate = async (event) => {
        try {
            if (event.candidate) {
                //将会localConnection的Candidate添加至remoteConnection里
                await remoteConnection.addIceCandidate(event.candidate);
                this.onAddIceCandidateSuccess(remoteConnection);
            }
        } catch (e) {
            this.onAddIceCandidateError(remoteConnection, e);
        }
        console.log(`IceCandidate数据:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
    }

    //Candidate事件回调方法
    onRemoteIceCandidate = async (event) => {
        try {
            if (event.candidate) {
                //将会remoteConnection的Candidate添加至localConnection里
                await localConnection.addIceCandidate(event.candidate);
                this.onAddIceCandidateSuccess(localConnection);
            }
        } catch (e) {
            this.onAddIceCandidateError(localConnection, e);
        }
        console.log(`IceCandidate数据:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
    }

    //添加Candidate成功
    onAddIceCandidateSuccess = (pc) => {
        console.log(`${this.getName(pc)}添加IceCandidate成功`);
    }

    //添加Candidate失败
    onAddIceCandidateError = (pc, error) => {
        console.log(`${this.getName(pc)}添加IceCandidate失败: ${error.toString()}`);
    }

    //监听ICE状态变化事件回调方法
    onLocalIceStateChange = (event) => {
        console.log(`localConnection连接的ICE状态: ${localConnection.iceConnectionState}`);
        console.log('ICE状态改变事件: ', event);
    }

    //监听ICE状态变化事件回调方法
    onRemoteIceStateChange = (event) => {
        console.log(`remoteConnection连接的ICE状态: ${remoteConnection.iceConnectionState}`);
        console.log('ICE状态改变事件: ', event);
    }

    //关闭数据通道
    closeChannel = () => {
        console.log('关闭数据通道');
        sendChannel.close();
        if (receiveChannel) {
            receiveChannel.close();
        }
        //关闭localConnection
        localConnection.close();
        //关闭remoteConnection
        remoteConnection.close();
        //localConnection置为空
        localConnection = null;
        //remoteConnection置为空
        remoteConnection = null;
    }

    //发送数据
    sendData = () => {
        let file = fileInput.files[0];
        console.log(`文件是: ${[file.name, file.size, file.type].join(' ')}`);

        //设置发送进度条的最大值
        sendProgress.max = file.size;
        //设置接收进度条的最大值
        receiveProgress.max = file.size;

        //文件切片大小,即每次读取的文件大小
        let chunkSize = 16384;
        //实例化文件读取对象
        fileReader = new FileReader();
        //偏移量可用于表示进度
        let offset = 0;
        //监听error事件
        fileReader.addEventListener('error', (error) => {
            console.error('读取文件出错:', error)
        });
        //监听abort事件
        fileReader.addEventListener('abort', (event) => {
            console.log('读取文件取消:', event)
        });
        //监听load事件
        fileReader.addEventListener('load', (e) => {
            console.log('文件加载完成 ', e);
            //使用发送通道开始发送文件数据
            sendChannel.send(e.target.result);
            //使用文件二进制数据长度作为偏移量
            offset += e.target.result.byteLength;
            //使用偏移量作为发送进度
            sendProgress.value = offset;
            console.log('当前文件发送进度为:', offset);
            //判断偏移量是否小于文件大小
            if (offset < file.size) {
                //继续读取
                readSlice(offset);
            }
        });
        //读取切片大小
        let readSlice = (o) => {
            console.log('readSlice ', o);
            //将文件的某一段切割下来,从offset到offset + chunkSize位置切下
            let slice = file.slice(offset, o + chunkSize);
            //读取切片的二进制数据
            fileReader.readAsArrayBuffer(slice);
        };
        //首次读取0到chunkSize大小的切片数据
        readSlice(0);
    }

    //接收通道数据到达回调方法
    receiveChannelCallback = (event) => {
        //实例化接收通道
        receiveChannel = event.channel;
        //数据类型为二进制
        receiveChannel.binaryType = 'arraybuffer';
        //接收消息事件监听
        receiveChannel.onmessage = this.onReceiveMessageCallback;
        //onopen事件监听
        receiveChannel.onopen = this.onReceiveChannelStateChange;
        //onclose事件监听
        receiveChannel.onclose = this.onReceiveChannelStateChange;

        receivedSize = 0;
    }

    //接收消息处理
    onReceiveMessageCallback = (event) => {
        console.log(`接收的数据 ${event.data.byteLength}`);
        //将接收到的数据添加到接收缓存里
        receiveBuffer.push(event.data);
        //设置当前接收文件的大小
        receivedSize += event.data.byteLength;
        //使用接收文件的大小表示当前接收进度
        receiveProgress.value = receivedSize;

        const file = fileInput.files[0];
        //判断当前接收的文件大小是否等于文件的大小
        if (receivedSize === file.size) {
            //根据缓存数据生成Blob文件
            const received = new Blob(receiveBuffer);
            //将缓存数据置为空
            receiveBuffer = [];
            
            //获取下载连接对象
            let download = document.getElementById('download');
            //创建下载文件对象及链接
            download.href = URL.createObjectURL(received);
            download.download = file.name;
            download.textContent = `点击下载'${file.name}'(${file.size} bytes)`;
            download.style.display = 'block';
        }
    }

    //发送通道状态变化
    onSendChannelStateChange = () => {
        const readyState = sendChannel.readyState;
        console.log('发送通道状态: ' + readyState);
        if (readyState === 'open') {
            this.sendData();
        }
    }

    //接收通道状态变化
    onReceiveChannelStateChange = () => {
        const readyState = receiveChannel.readyState;
        console.log('接收通道状态:' + readyState);
    }

    //取消发送文件
    cancleSendFile = () => {
        if (fileReader && fileReader.readyState === 1) {
          console.log('取消读取文件');
          fileReader.abort();
        }
      }

    render() {
        return (
            <div className="container">
                <div>
                    <form id="fileInfo">
                        <input type="file" id="fileInput" name="files" />
                    </form>
                    <div>
                        <h2>发送</h2>
                        <progress id="sendProgress" max="0" value="0" style={{width:'500px'}}></progress>
                    </div>
                    <div>
                        <h2>接收</h2>
                        <progress id="receiveProgress" max="0" value="0" style={{width:'500px'}}></progress>
                    </div>
                </div>

                <a id="download"></a>
                <div>
                    <Button onClick={this.startSendFile} style={{ marginRight: "10px" }}>发送</Button>
                    <Button onClick={this.cancleSendFile} style={{ marginRight: "10px" }}>取消</Button>
                    <Button onClick={this.closeChannel} style={{ marginRight: "10px" }}>关闭</Button>
                </div>
            </div>
        );
    }
}
//导出组件
export default DataChannelFile;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2052528.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【PyCharm】配置“清华镜像”地址

文章目录 前言一、清华镜像是什么&#xff1f;二、pip是什么&#xff1f;三、具体步骤1.复制镜像地址2.打开PyCharm&#xff0c;然后点击下图红框的选项3.在弹出的新窗口点击下图红框的选项进行添加4.在URL输入框中粘贴第一步复制的地址&#xff0c;名字可以不更改&#xff0c;…

电力调度控制台作为智能电网的中枢大脑,引领能源高效调度新时代

在当今这个能源需求日益增长、电力网络日益复杂的时代&#xff0c;电力调度控制台作为智能电网的核心组成部分&#xff0c;正扮演着至关重要的角色。它不仅是电力系统中信息汇聚与决策输出的中枢大脑&#xff0c;更是实现电力资源优化配置、保障电网安全稳定运行的关键所在。 智…

MYSQL查询规范:索引

前言 工作有段时间了&#xff0c;现在看以前写的代码、sql之类的&#xff0c;实属辣眼睛。 这里将给出一些目前遇到的MYSQL查询规范&#xff08;索引&#xff09;&#xff0c;并长期更新 索引 众所周知&#xff0c;索引能提高数据查询效率&#xff08;前提是该字段被用在WHERE、…

Springboot邮件发送:如何配置SMTP服务器?

Springboot邮件发送集成方法&#xff1f;如何提升邮件发送性能&#xff1f; 对于使用Springboot的开发者来说&#xff0c;配置SMTP服务器来实现邮件发送并不是一件复杂的事情。AokSend将详细介绍如何通过配置SMTP服务器来实现Springboot邮件发送。 Springboot邮件发送&#x…

【SpringBoot】SpringBoot中的异常处理和异常跳转页面

目录 1.异常跳转页面 1.1 概念 1.2 使用 2.异常处理 2.1 概念 2.2 通过ExceptionHandler 注解处理异常&#xff08;局部处理&#xff09; 2.3 通过ControllerAdvice 注解处理异常&#xff08;全局处理&#xff09; 2.4 通过 SimpleMappingExceptionResolver 对象处理…

AI大模型达人秘籍:豆瓣9.2分推荐必读!

有很多程序员正在AIGC赛道中默默发财&#xff0c;有通过短视频做内容爆火&#xff0c;接广告的&#xff1b;有卖AI解决方案的&#xff1b;有卖AI课程的&#xff1b;也有卖AI产品&#xff0c;慢慢做大做强的…更不必说&#xff0c;那些拿下“人均年薪100万”大模型相关岗位的“赢…

多模态 AI 是零售业的未来吗?使用 GPT-4 Vision 和 MongoDB 矢量搜索探索智能产品发现

生成式人工智能如何重新定义零售盈利能力 欢迎来到雲闪世界。想象一下这样的购物体验&#xff1a;您上传了一张心仪服装或商品的照片。片刻之后&#xff0c;您便会收到来自您喜爱的商店的个性化、AI 驱动的类似商品推荐。这是一种革命性的零售体验&#xff0c;由一款创新应用实…

从零到一,数据恢复不求人!2024年四款全免费神器,轻松搞定

活在这个电脑手机满天飞的时代&#xff0c;我们天天跟数据打交道。工作文件、家庭照片、视频&#xff0c;这些都是我们的宝贝&#xff0c;一旦没了或者出问题&#xff0c;那可真够呛。好在我们有高科技帮忙&#xff0c;数据恢复现在也不是啥大问题。今儿个&#xff0c;我要给你…

数说故事 | 2024巴黎奥运会,“谷子文化”出圈了

全红婵金牌&#xff0c;全网沸腾。 摘金之后全妹的痛包&#xff08;itabag&#xff0c;是指挂满人物徽章和玩偶等周边的包包&#xff0c;因为这样的包会让人感觉“奇怪和夸张&#xff0c;日语的“痛”有此含义&#xff0c;所以被称为“痛包”&#xff09;&#xff0c;也让二次…

多模态大模型(MLLM):架构篇

**【导读】**多模态大模型主要是以LLM作为核心决策模块&#xff0c;主流架构有两种&#xff1a;LLM as Discrete Scheduler/Controller和LLM as joint part of system&#xff0c;第一种LLM充当任务调度的作用&#xff0c;第二种LLM通过Encoder-LLM-Decoder结构作为系统的关键连…

探索Python性能优化的神秘力量:Line Profiler

文章目录 探索Python性能优化的神秘力量&#xff1a;Line Profiler第一部分&#xff1a;背景第二部分&#xff1a;库简介第三部分&#xff1a;安装指南第四部分&#xff1a;基本使用方法第五部分&#xff1a;实际应用场景场景1&#xff1a;数据分析场景2&#xff1a;机器学习模…

Docker 离线下载镜像(本地)

最近很多离线下载镜像的网站挂了&#xff0c;可以选择 docker hub 下载上传到服务器 Docker desktop image-tools ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/04174b8cefe3445596e09b1e82d9253b.png 保存后&#xff0c;就可以上传服务器&#xff0c;通过 dock…

音频合成剪辑工具推荐哪个?5款高效工具不容小觑

在探索音乐和声音的世界中&#xff0c;我发现了几款令人兴奋的音频剪辑软件&#xff0c;它们不仅功能强大&#xff0c;而且完全免费。作为一个热衷于音频制作的爱好者&#xff0c;我深知找到一款既经济又高效的工具是多么重要。 今天&#xff0c;我想跟大家分享我的一些发现&a…

jeecg部署后端到ubuntu

安装java17 java -versionsudo apt updatesudo apt install openjdk-17-jre-headless安装tomcat 更新源 sudo apt update在apt中搜索tomcat sudo apt search tomcat安装tomcat10和tomcat10-admin sudo apt install tomcat9 tomcat9-admin启动tomcat sudo systemctl start…

C语言小练习(伍)

练习&#xff1a; 利用指针变量将一个数组中的数据反向输出。 void renew(int *p,int len) {for(int i len - 1;i > 0;i--){printf("%d",*(pi));} }int main() {int a[5] {1,2,3,4,5};int len1 sizeof(a) / sizeof(int);renew(a,len1);return 0; }运行结果: …

AI文生图新纪元:FLUX.1领衔,多款顶尖模型汇聚趋动云社区

前言 近期&#xff0c;SD(Stable Diffusion)原班人马(跳槽后新成立团队——Black Forest Lab)开源模型 FLUX.1&#xff0c;或成文生图模型新霸主&#xff01; FLUX.1 系列包含 pro、dev、schnell 3 个模型。主要在文字生成、复杂指令遵循和人手生成上具备优势。 FLUX.1pro&a…

爆学C++之类和对象(上)

1.类的定义 类定义格式 • class为定义类的关键字&#xff0c;Stack为类的名字&#xff0c;{}中为类的主体&#xff0c;注意类定义结束时后⾯分号不能省略。类体中内容称为类的成员&#xff1a;类中的变量称为类的属性或成员变量; 类中的函数称为类的⽅法或者成员函数。 • …

IDEA中设置类和方法的注释

分两步设置&#xff1a; 第一个设置是创建类的时候自动加的注解 第二个设置是快捷键为方法增加的注解 类的时候自动加的注解设置 注释模版 #if (${PACKAGE_NAME} && ${PACKAGE_NAME} ! "")package ${PACKAGE_NAME};#end /** * Description: TODO * Auth…

IOPaint部署,在服务器Ubuntu22.04系统下——点动科技

这里写目录标题 一、ubuntu22.04基本环境配置1.1 更换清华Ubuntu镜像源1.2 更新包列表&#xff1a;2. 安装英伟达显卡驱动2.1 使用wget在命令行下载驱动包2.2 更新软件列表和安装必要软件、依赖2.2 卸载原有驱动2.3 安装驱动2.4 安装CUDA2.5 环境变量配置 二、安装miniconda环境…

2024年第九届机器人与自动化工程国际会议(ICRAE 2024)即将召开!

2024年第九届机器人与自动化工程国际会议(ICRAE 2024)将于11月15-17日在新加坡举办。ICRAE 2024会议旨在为机器人与自动化工程等领域的专家学者建立一个广泛有效的学术交流平台&#xff0c;以便让参会人员及时了解行业发展动态、掌握最新技术&#xff0c;促使我们快速、准确地解…