WebRTC学习笔记一 简单示例

news2024/11/25 13:00:22

一、捕获本地媒体流getUserMedia

1.index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div>
        <button id="start">开始录制</button>
        <button id="stop">停止录制</button>
    </div>
    <div>
        <video autoplay controls id="stream"></video>
    </div>
    <script>
        // 只获取视频
        let constraints = {audio: false, video: true}; 
        let startBtn = document.getElementById('start')
        let stopBtn = document.getElementById('stop')
        let video = document.getElementById('stream')
        startBtn.onclick = function() {
            navigator.getUserMedia(constraints, function(stream) {
                video.srcObject = stream;
                window.stream = stream;
            }, function(err) {
                console.log(err)
            })
        }
        stopBtn.onclick = function() {
            video.pause();
        }
    </script>
</body>
</html>

部署本地服务器后,chrome中访问192.168.11.129:9050会报错:navigator.getUserMedia is not a function。原因是,Chrome 47以后,getUserMedia API只能允许来自“安全可信”的客户端的视频音频请求,如HTTPS和本地的Localhost。所以将访问地址改成localhost:9050即可。

二、同网页示例

例子来源:https://codelabs.developers.google.com/codelabs/webrtc-web/#4

1.index.html

 <!DOCTYPE html>
 <html>
​
     <head>
         <title>Realtime communication with WebRTC</title>
         <style>
             body {
                 font-family: sans-serif;
             }
     
             video {
                 max-width: 100%;
                 width: 320px;
             }
         </style>
     </head>
​
     <body>
         <h1>Realtime communication with WebRTC</h1>
     
         <video id="localVideo" autoplay playsinline></video>
         <video id="remoteVideo" autoplay playsinline></video>
     
         <div>
             <button id="startButton">开始</button>
             <button id="callButton">调用</button>
             <button id="hangupButton">挂断</button>
         </div>
        <!-- 适配各浏览器 API 不统一的脚本 -->
        <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
         </script>
 </body>
​
</html>

2.main.js

'use strict';
​
//log
function trace(text) {
  text = text.trim();
  const now = (window.performance.now() / 1000).toFixed(3);
  console.log(now, text);
}
​
// 设置两个video,分别显示本地视频流和远端视频流
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
​
localVideo.addEventListener('loadedmetadata', logVideoLoaded);
remoteVideo.addEventListener('loadedmetadata', logVideoLoaded);
remoteVideo.addEventListener('onresize', logResizedVideo);
​
function logVideoLoaded(event) {
  const video = event.target;
  trace(`${video.id} videoWidth: ${video.videoWidth}px, ` +
        `videoHeight: ${video.videoHeight}px.`);
}
​
function logResizedVideo(event) {
  logVideoLoaded(event);
  if (startTime) {
    const elapsedTime = window.performance.now() - startTime;
    startTime = null;
    trace(`Setup time: ${elapsedTime.toFixed(3)}ms.`);
  }
}
​
let startTime = null;
let localStream;
let remoteStream;
// 建立两个对等连接对象,分表代表本地和远端
let localPeerConnection;
let remotePeerConnection;
​
const startButton = document.getElementById('startButton');
const callButton = document.getElementById('callButton');
const hangupButton = document.getElementById('hangupButton');
callButton.disabled = true;
hangupButton.disabled = true;
startButton.addEventListener('click', startAction);
callButton.addEventListener('click', callAction);
hangupButton.addEventListener('click', hangupAction);
​
// 传输视频,不传输音频
const mediaStreamConstraints = {
  video: true,
  audio: false
};
//开始事件,采集摄像头到本地
function startAction() {
  startButton.disabled = true;
  navigator.getUserMedia(mediaStreamConstraints, gotLocalMediaStream, handleLocalMediaStreamError)
  trace('Requesting local stream.');
}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false; 
}
function handleLocalMediaStreamError(error) {
  trace(`navigator.getUserMedia error: ${error.toString()}.`);
}
​
// 设置只交换视频
const offerOptions = {
  offerToReceiveVideo: 1,
};
​
// 创建对等连接
function callAction() {
  callButton.disabled = true;
  hangupButton.disabled = false;
​
  trace('Starting call.');
  startTime = window.performance.now();
​
  const videoTracks = localStream.getVideoTracks();
  const audioTracks = localStream.getAudioTracks();
  if (videoTracks.length > 0) {
    trace(`Using video device: ${videoTracks[0].label}.`);
  }
  if (audioTracks.length > 0) {
    trace(`Using audio device: ${audioTracks[0].label}.`);
  }
​
  // 服务器配置
  const servers = null; 
  localPeerConnection = new RTCPeerConnection(servers);
  trace('Created local peer connection object localPeerConnection.');
  localPeerConnection.addEventListener('icecandidate', handleConnection);
  localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
​
  remotePeerConnection = new RTCPeerConnection(servers);
  trace('Created remote peer connection object remotePeerConnection.');
  remotePeerConnection.addEventListener('icecandidate', handleConnection);
  remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
  remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);
​
  localPeerConnection.addStream(localStream);
  trace('Added local stream to localPeerConnection.');
​
  trace('localPeerConnection createOffer start.');
  localPeerConnection.createOffer(offerOptions)
    .then(createdOffer).catch(setSessionDescriptionError);
}
​
function getOtherPeer(peerConnection) {
  return (peerConnection === localPeerConnection) ?
      remotePeerConnection : localPeerConnection;
}
​
function getPeerName(peerConnection) {
  return (peerConnection === localPeerConnection) ?
      'localPeerConnection' : 'remotePeerConnection';
}
​
function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;
​
  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);
​
    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });
​
    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}
​
function handleConnectionSuccess(peerConnection) {
  trace(`${getPeerName(peerConnection)} addIceCandidate success.`);
};
​
function handleConnectionFailure(peerConnection, error) {
  trace(`${getPeerName(peerConnection)} failed to add ICE Candidate:\n`+
        `${error.toString()}.`);
}
​
function handleConnectionChange(event) {
  const peerConnection = event.target;
  console.log('ICE state change event: ', event);
  trace(`${getPeerName(peerConnection)} ICE state: ` +
        `${peerConnection.iceConnectionState}.`);
}
​
function gotRemoteMediaStream(event) {
  const mediaStream = event.stream;
  remoteVideo.srcObject = mediaStream;
  remoteStream = mediaStream;
  trace('Remote peer connection received remote stream.');
}
​
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);
​
  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
​
  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);
​
  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);
}
​
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);
​
  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);
​
  trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}
​
function setSessionDescriptionError(error) {
  trace(`Failed to create session description: ${error.toString()}.`);
}
​
​
function setLocalDescriptionSuccess(peerConnection) {
  setDescriptionSuccess(peerConnection, 'setLocalDescription');
}
​
function setRemoteDescriptionSuccess(peerConnection) {
  setDescriptionSuccess(peerConnection, 'setRemoteDescription');
}
​
function setDescriptionSuccess(peerConnection, functionName) {
  const peerName = getPeerName(peerConnection);
  trace(`${peerName} ${functionName} complete.`);
}
​
//断掉
function hangupAction() {
  localPeerConnection.close();
  remotePeerConnection.close();
  localPeerConnection = null;
  remotePeerConnection = null;
  hangupButton.disabled = true;
  callButton.disabled = false;
  trace('Ending call.');
}

【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:

免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~

  

3.源码分析

点击开始,触发startAction没什么好说的。点击调用,直接看callAction: (1)首先使用new RTCPeerConnection创建了两个connection

  const servers = null; 
  localPeerConnection = new RTCPeerConnection(servers);

servers在这个例子中并没有用,是用来配置STUN and TURN s服务器的,先忽略。

(2)添加事件侦听,先忽略

//也可以使用onicecandidate这种写法
addEventListener('icecandidate', handleConnection);
addEventListener('iceconnectionstatechange', handleConnectionChange);

(3)然后就是addStream和createOffer

  localPeerConnection.addStream(localStream);
  trace('Added local stream to localPeerConnection.');
​
  trace('localPeerConnection createOffer start.');
  localPeerConnection.createOffer(offerOptions)
    .then(createdOffer).catch(setSessionDescriptionError);

其中createOffer需要一个Options

// 设置只交换视频
const offerOptions = {
  offerToReceiveVideo: 1,
};

这里我的理解是,createOffer为了产生SDP描述,要先使用addStream把视频流加载进去才能解析。

  • A创建一个RTCPeerConnection对象。

  • A使用RTCPeerConnection .createOffer()方法产生一个offer(一个SDP会话描述)。

  • A用生成的offer调用setLocalDescription(),设置成自己的本地会话描述。

  • A将offer通过信令机制发送给B。

  • B用A的offer调用setRemoteDescription(),设置成自己的远端会话描述,以便他的RTCPeerConnection知道A的设置。

  • B调用createAnswer()生成answer

  • B通过调用setLocalDescription()将其answer设置为本地会话描述。

  • B然后使用信令机制将他的answer发回给A。

  • A使用setRemoteDescription()将B的应答设置为远端会话描述。

上述过程可以在源码createOffer和createAnser中看到。

(4)icecandidate事件 参考

https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/icecandidate_event

当 RTCPeerConnection通过RTCPeerConnection.setLocalDescription() (en-US)方法更改本地描述之后,该RTCPeerConnection会抛出icecandidate事件。该事件的监听器需要将更改后的描述信息传送给远端RTCPeerConnection,以更新远端的备选源。

意思就是setLocalDescription被调用后,触发icecandidate事件,这一点可以在示例的console中得到验证。

4.来张流程图,转自https://segmentfault.com/a/1190000037513346

(5)addTrack,addTransceiver addStream() 已过时,官方不推荐使用.将一个MediaStream音频或视频的本地源,添加到WebRTC对等连接流对象中。官方推荐我们使用另外一个方法addTrack

  remotePeerConnection.ontrack = function(evt) {
      const mediaStream = evt.streams[0];
    remoteVideo.srcObject = mediaStream;
    remoteStream = mediaStream;
    trace('Remote peer connection received remote stream.');
}
​
localStream.getTracks().forEach(track => {
    localPeerConnection.addTrack(track, localStream);
    // localPeerConnection.addTransceiver(track, {streams: [localStream]}); // 这个也可以
});

如果你是做音视频聊天相关的产品,那么addTrack 刚好能满足你的需求,毕竟需要使用到用户的摄像头、麦克风(浏览器会询问用户是否授权)。但是你只想建立音视频轨道,并不需要使用摄像头、麦克风,那我们应该怎么去做呢?

addTransceiver创建一个新的RTCRtpTransceiver并将其添加到与关联的收发器集中RTCPeerConnection。每个收发器都代表一个双向流,并带有RTCRtpSender和RTCRtpReceiver。

let rtcTransceiver = RTCPeerConnection .addTransceiver(trackOrKind,init);

(a)trackOrKind: MediaStreamTrack以与所述收发器相关联,或者一个DOMString被用作kind接收器的的track。这里视频轨道就传"video",音频轨道就传"audio" (b)init: 可选参数。如下:

  • direction:收发器的首选方向性。此值用于初始化新RTCRtpTransceiver对象的*RTCRtpTransceiver.direction属性。

  • sendEncodings:从中发送RTP媒体时允许的编码列表RTCRtpSender。每个条目都是类型RTCRtpEncodingParameters。

  • streams: MediaStream要添加到收发器的对象列表RTCRtpReceiver;当远程对等方RTCPeerConnection的track事件发生时,这些是将由该事件指定的流。

举个例子: 添加一个单向的音视频流收发器

            this.rtcPeerConnection.addTransceiver("video", {
                direction: "recvonly"
            });
            this.rtcPeerConnection.addTransceiver("audio", {
                direction: "recvonly"
            });

上述代码只会接收对端发过来的音视频流,不会将自己的音视频流传输给对端。direction:

三、网络1V1示例

源码参见https://github.com/wuyawei/webrtc-stream

这个例子不再是同一个网页,所以需要借助socket.io通讯。 房间相关逻辑暂时忽略,看一下创建offer部分:

socket.on('apply', data => { // 你点同意的地方
    ...
    this.$confirm(data.self + ' 向你请求视频通话, 是否同意?', '提示', {
        confirmButtonText: '同意',
        cancelButtonText: '拒绝',
        type: 'warning'
    }).then(async () => {
        await this.createP2P(data); // 同意之后创建自己的 peer 等待对方的 offer
        ... // 这里不发 offer
    })
    ...
});
socket.on('reply', async data =>{ // 对方知道你点了同意的地方
    switch (data.type) {
        case '1': // 只有这里发 offer
            await this.createP2P(data); // 对方同意之后创建自己的 peer
            this.createOffer(data); // 并给对方发送 offer
            break;
        ...
    }
});

本例采取的是呼叫方发送 Offer,这个地方一定得注意,只要有一方创建 Offer 就可以了,因为一旦连接就是双向的。

和微信等视频通话一样,双方都需要进行媒体流输出,因为你们都要看见对方。所以这里和之前本地对等连接的区别就是都需要给自己的 RTCPeerConnection 实例添加媒体流,然后连接后各自都能拿到对方的视频流。在 初始化 RTCPeerConnection 时,记得加上 onicecandidate 函数,用以给对方发送 ICE 候选。

async createP2P(data) {
    this.loading = true; // loading动画
    this.loadingText = '正在建立通话连接';
    await this.createMedia(data);
},
async createMedia(data) {
    ... // 获取并将本地流赋值给 video  同之前
    this.initPeer(data); // 获取到媒体流后,调用函数初始化 RTCPeerConnection
},
initPeer(data) {
    // 创建输出端 PeerConnection
    ...
    this.peer.addStream(this.localstream); // 都需要添加本地流
    this.peer.onicecandidate = (event) => {
    // 监听ICE候选信息 如果收集到,就发送给对方
        if (event.candidate) { // 发送 ICE 候选
            socket.emit('1v1ICE',
            {account: data.self, self: this.account, sdp: event.candidate});
        }
    };
    this.peer.onaddstream = (event) => {
    // 监听是否有媒体流接入,如果有就赋值给 rtcB 的 src,改变相应loading状态,赋值省略
        this.isToPeer = true;
        this.loading = false;
        ...
    };
}

createOffer 等信息交换和之前一样,只是需要通过 Socket 转发给对应的客户端。然后各自接收到消息后分别采取对应的措施。

socket.on('1v1answer', (data) =>{ // 接收到 answer
    this.onAnswer(data);
});
socket.on('1v1ICE', (data) =>{ // 接收到 ICE
    this.onIce(data);
});
socket.on('1v1offer', (data) =>{ // 接收到 offer
    this.onOffer(data);
});
​
async createOffer(data) { // 创建并发送 offer
    try {
        // 创建offer
        let offer = await this.peer.createOffer(this.offerOption);
        // 呼叫端设置本地 offer 描述
        await this.peer.setLocalDescription(offer);
        // 给对方发送 offer
        socket.emit('1v1offer', {account: data.self, self: this.account, sdp: offer});
    } catch (e) {
        console.log('createOffer: ', e);
    }
},
async onOffer(data) { // 接收offer并发送 answer
    try {
        // 接收端设置远程 offer 描述
        await this.peer.setRemoteDescription(data.sdp);
        // 接收端创建 answer
        let answer = await this.peer.createAnswer();
        // 接收端设置本地 answer 描述
        await this.peer.setLocalDescription(answer);
        // 给对方发送 answer
        socket.emit('1v1answer', {account: data.self, self: this.account, sdp: answer});
    } catch (e) {
        console.log('onOffer: ', e);
    }
},
async onAnswer(data) { // 接收answer
    try {
        await this.peer.setRemoteDescription(data.sdp); // 呼叫端设置远程 answer 描述
    } catch (e) {
        console.log('onAnswer: ', e);
    }
},
async onIce(data) { // 接收 ICE 候选
    try {
        await this.peer.addIceCandidate(data.sdp); // 设置远程 ICE
    } catch (e) {
        console.log('onAnswer: ', e);
    }
}

挂断的思路依然是将各自的 peer 关闭,但是这里挂断方还需要借助 Socket 告诉对方,你已经挂电话了,不然对方还在痴痴地等。

hangup() { // 挂断通话 并做相应处理 对方收到消息后一样需要关闭连接
    socket.emit('1v1hangup', {account: this.isCall, self: this.account});
    this.peer.close();
    this.peer = null;
    this.isToPeer = false;
    this.isCall = false;
}

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

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

相关文章

基于预训练模型的Unet【超级简单】【懒人版】【Pytorch版】

基于预训练模型的Unet【超级简单】【懒人版】【Pytorch版】 在本项目开始前&#xff0c;首先给大家保证&#xff0c;本次项目只是一个最简单的Unet实现&#xff0c;使用现成的代码&#xff0c;不需要手写代码&#xff0c;使用预训练模型&#xff0c;不需要标注数据集和训练。所…

NTFS及文件共享

一&#xff0c;NTFS安全权限概述 1、给文件和文件夹设置权限&#xff0c;通过设置权限&#xff0c;实现不同的用户访问不同文件和文件夹的权限。 2、分配了正确的访问权限后&#xff0c;用户才能访问对应资源。 3、设置权限防止资源被篡改、删除。 二、文件系统概述 文件系统…

[附源码]Python计算机毕业设计SSM旅游服务平台(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Whistle 前端抓包

whistle文档&#xff1a;http://wproxy.org/whistle/install.html 1.确保电脑安装了node node -v如果能正常输出Node的版本号&#xff0c;表示Node已安装成功 2.安装whistle npm install -g whistlewhistle安装完成后&#xff0c;执行命令 whistle help 或 w2 help&#xf…

Spring——三级缓存解决循环依赖详解

三级缓存解决循环依赖详解一、什么是三级缓存二、三级缓存详解Bean实例化前属性赋值/注入前初始化后总结三、怎么解决的循环依赖四、不用三级缓存不行吗五、总结一、什么是三级缓存 就是在Bean生成流程中保存Bean对象三种形态的三个Map集合&#xff0c;如下&#xff1a; // 一…

IPv6进阶:IPv6 过渡技术之 NAT64(IPv6 节点主动访问 IPv4 节点-地址池方式)

实验拓扑 PC1是IPv4网络的一个节点&#xff0c;处于Trust安全域&#xff1b;PC2是IPv6网络的一个节点&#xff0c;处于Untrust安全域。 实验需求 完成防火墙IPv4、IPv6接口的配置&#xff0c;并将接口添加到相应的安全域&#xff1b;在防火墙上配置NAT64的IPv6前缀3001::/64&…

cpu设计和实现(数据访问)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在cpu设计当中&#xff0c;数据访问是比较重要的一个环节。一般认为&#xff0c;数据访问就是内存访问。其实不然。我们都知道&#xff0c;cpu访问…

【微服务】SpringCloud中Ribbon的轮询(RoundRobinRule)与重试(RetryRule)策略

💖 Spring家族及微服务系列文章 ✨【微服务】SpringCloud中Ribbon集成Eureka实现负载均衡 ✨【微服务】SpringCloud轮询拉取注册表及服务发现源码解析 ✨【微服务】SpringCloud微服务续约源码解析 ✨【微服务】SpringCloud微服务注册源码解析 ✨

Nginx的操作

一、什么是nginx。 Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器 , 其特点是占有内存少&#xff0c;并发能力强&#xff0c;事实上nginx的并发能力在同类型的网页服务器中表现较好。 Nginx代码完全用C语言从头写成 . 能够支持高达 50,000 个并发连接数的响应. 现在…

【pen200-lab】10.11.1.72

pen200-lab 学习笔记 【pen200-lab】10.11.1.72 &#x1f525;系列专栏&#xff1a;pen200-lab &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月27日&#x1f334; &#x1f36d;作…

aws beanstalk 使用eb cli配置和启动环境

Elastic Beanstalk 不额外收费&#xff0c;只需为存储和运行应用程序所需的 AWS 资源付费 EB CLI 是 Amazon Elastic Beanstalk 的命令行界面&#xff0c;它提供了可简化从本地存储库创建、更新和监控环境的交互式命令 安装eb cli $ pip install virtualenv $ virtualenv ebve…

2023年考研数学测试卷(预测)

2023年考研数学测试卷 原题再现&#xff1a; 多的我也不说了&#xff0c;直接把预测的2023年考研数学卷子分享给大家好吧&#xff0c;准确详细全面是我的宗旨&#xff0c;我的博客创立初衷和发展方向肯定不应该只是"考试有用"&#xff0c;而是面对社会生产生活的有用…

Day815.数据库参数设置优化 -Java 性能调优实战

数据库参数设置优化 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于数据库参数设置优化。 MySQL 是一个灵活性比较强的数据库系统&#xff0c;提供了很多可配置参数&#xff0c;便于根据应用和服务器硬件来做定制化数据库服务。 数据库主要是用来存取数据的&#…

视频编解码 — DCT变换和量化

目录 视频编码流程 DCT变换 Hadamard变换 量化 H264中的DCT变换和量化 H264各模式的DCT变换和量化过程 1、亮度16x16帧内预测块 2&#xff0c;其它模式亮度块 3&#xff0c;色度块 小结 视频编码流程 DCT变换 离散余弦变换 它能将空域信号转换到频率上表示&#xff0…

建造者模式

文章目录定义优点使用场景代码实现定义 将一个复杂对象的构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 4个角色&#xff1a; Product产品类&#xff1a;通常是实现了模板方法模式&#xff0c;也就是有模板方法和基本方法Builder抽象建造者&#xf…

PyQt5可视化编程-事件、信号和对话框

一、概述: 所有的应用都是事件驱动的。事件大部分都是由用户的行为产生的&#xff0c;当然也有其他的事件产生方式&#xff0c;比如网络的连接&#xff0c;窗口管理器或者定时器等。调用应用的exec_()方法时&#xff0c;应用会进入主循环&#xff0c;主循环会监听和分发事件。…

算法题:整数除法

一.题目描述以及来源 给定两个整数 a 和 b &#xff0c;求它们的除法的商 a/b &#xff0c;要求不得使用乘号 *、除号 / 以及求余符号 % 。 注意&#xff1a; 整数除法的结果应当截去&#xff08;truncate&#xff09;其小数部分&#xff0c;例如&#xff1a;truncate(8.345…

MP157-2-TF-A移植:

MP157-2-TF-A移植&#xff1a;1. TF-A移植&#xff1a;1.1 新建开发板的设备树1.2 修改设备树电源管理1.3修改TF卡和EMMC设备树1.4 修改USBOTG设备树2 编译测试2.1 Makefile.sdk 修改内容&#xff1a;2.2 编译命令&#xff1a;正点原子第九章内容&#xff1a;自己记的笔记&…

SpringBoot(One·上)

SpringBoot一、简介概述Spring Boot特性SpringBoot四大核心二、SpringBoot项目分析1、创建第一个案例结构目录和pom文件2、Springboot集成mvcSpringboot核心配置文件application.propertiesSpringboot核心配置文件application.yml或者application.yamlapplication.ymlapplicati…

Allegro削铜皮详细操作指导

Allegro削铜皮详细操作指导 Allegro可以编辑任意形状的铜皮,下面介绍几种削铜皮的方式 任意形状,shape-manual Void/cavity-Polygon 鼠标左键点击铜皮,铜皮会被亮起来 画出需要的形状 完成后如下图 方形shape-manual Void/cavity-Rectangular 同样的选择铜皮,画出需要…