范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

WebRTC学习笔记一简单示例

  一、捕获本地媒体流getUserMedia1.index.html                      Document                                                        
  部署本地服务器后,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                    Realtime communication with WebRTC                                 

Realtime communication with WebRTC

2.main.js   【腾讯文档】FFmpegWebRTCRTMPRTSPHLSRTP播放器-音视频流媒体高级开发-资料领取FFmpegWebRTCRTMPRTSPHLSRTP鎾 斁鍣 -闊宠 棰戞祦濯掍綋楂樼骇寮 鍙 -璧勬枡棰嗗彇"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: ` + `${event.candidate.candidate}.`); } } function handleConnectionSuccess(peerConnection) { trace(`${getPeerName(peerConnection)} addIceCandidate success.`); }; function handleConnectionFailure(peerConnection, error) { trace(`${getPeerName(peerConnection)} failed to add ICE Candidate: `+ `${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: ${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: ${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."); }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中得到验证。   image.png 4.来张流程图,转自https://segmentfault.com/a/1190000037513346   image.png   (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:   image.png 三、网络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; }

没了mini,小杯还有青春么?iPhoneSE4将满足小屏党所有需求iPhone14系列似乎预告了小屏时代的结束。从爆料看,整个系列最低版本为6。1英寸屏幕的iPhone14,上代的mini消失无踪也因此小屏党将注意力放在了iPhoneSE4上,希盘点iPhone图标里的小秘密,你知道几个?1。地图苹果地图的图标位置是位于洲际公路280号的苹果公司总部,在最新的系统中,右上角露出的为ApplePark的一角2。时钟苹果时钟的logo是实时转动的,一直与现实时间相符合32022年词典笔推荐新一代词典笔优学派P6标配的智能眼词典笔智能学习时代,孩子学习也需要更加智能的学习工具,词典笔作为一款轻量级的英语学习神器,成为了很多孩子必不可少的学习工具。但词典笔品牌众多,各种词典笔特色也不同,家长们选购时也不自觉挑从12999元跌至4888元,华为旗舰二手价,12GB512GB鸿蒙OSIP68花费五六千元购买4G手机听起来就有点吃亏,而这也是限制华为旗舰销售的一个主要原因,华为目前主要销售的旗舰华为P50系列尽管售价不菲,但它只是4G手机,所以尽管许多愿意消费者支持华为JDG2比0战胜V5,队员表现引发热议,赛后米勒的一番话很真实不知不觉本次的英雄联盟LPL春季赛也已进行到白热化阶段,在刚刚结束的JDG对阵V5的比赛中,JDG以2比0的比分战胜V5赢下比赛,此战过后JDG本次春季赛的战绩也来到了121,对于散文丨甘建华遥远的金银滩金银滩草原上萨耶卓玛塑像。赵元海摄遥远的金银滩文甘建华遥远的金银滩将会铭记这一天。2021年6月8日下午,习近平总书记来到海拔3200多米的青海湖仙女湾,实地考察这里的生态环境保护曝小米MIUI14发布时间还早小米新机仍搭载MIUI137月26日消息,今天数码博主关于小米MIUI14进行了爆料,该博主表示,离MIUI14发布还早,8月份发布的智能手机新品还将继续搭载MIUI13系统,虽然现在MIUI13内测版UX价格亲民的3款旗舰手机,不发烫续航长,买对了能用好长时间如果你也在乎旗舰手机的价格温控和续航,那今天的内容看到就是赚到了。今天我们要聊的是3款性价比极高,并且也可以用很长时间的旗舰手机。OPPOFindX3Pro发布于去年3月的Oppo同样是音箱,高保真音箱贵在什么地方?现在市面上的HiFi音响动则上万,那么为什么HiFi音响的价格会如此高昂呢?和HiFi一起来看看HiFi音响究竟贵在什么地方吧。首先,也是最重要的,我们要明白HiFi设备就是服务于长安高端新能源电车,深蓝SL03你确定不来看一看?20万级电轿王炸在新能源B级车市场中,特斯拉Model3一直是诸多自主品牌车型想要翻越的一座大山。而消费者选择Model3的理由有很多,比如特斯拉是最早进入新能源车市场的品牌之一,7。26山东地炼成品油报价,下一轮油价调整时间今日山东地炼柴汽油报价普遍上调,上调幅度在3050之间。部分炼厂下午仍在二调涨价。17。26山东地炼成品油报价鲁清石化今日92汽油报价7220,95汽油报价7370。汽油均上调了5
天空瑞典的一项研究表明,球员患痴呆的风险是普通人的一倍半直播吧3月17日讯根据天空体育消息,瑞典的一项研究表明,球员患神经退行性疾病(主要有脑萎缩老年痴呆症帕金森综合征以及肌肉萎缩侧索硬化等)的风险是普通人的一倍半。瑞典卡罗林斯卡学院和牛了!张志磊上门踢馆世界拳王!4月15日,中国拳击名将大爆炸张志磊将再战擂台,对阵英国拳手剑圣乔伊斯,乔伊斯是WBO重量级(91公斤以上)临时冠军,这一战也是张志磊首次争夺世界拳王金腰带的比赛。乔伊斯(左)张志巴黎奥运门票第二阶段开售篮球决赛最贵980欧元篮球决赛最贵980欧元巴黎奥运门票第二阶段开售广州日报讯(全媒体记者杨敏)随着2024年巴黎奥运会迎来开幕倒计时500天,第二阶段门票销售也在本月中旬正式开启,更多热门金牌项目的决国足受邀参加中亚杯可能性不大按照中亚足球联盟的计划,他们将从今年开始恢复举办中亚杯足球锦标赛,共有8支球队参赛。据外媒透露,除中亚足联下属6个会员协会代表队外,另有两支持外卡球队受邀参赛。目前,俄罗斯队泰国队西部第一锁定季后赛,猛龙助力湖人升至第九!篮网恐掉出前六北京时间3月17日,NBA常规赛继续进行,今天的比赛不多,但看点十足。之前四连败的西部第一掘金,面对东部摆烂倒数第一的活塞,前三节打得还非常被动,看起来首发根本没有做好赢球准备,不全英羽毛球公开赛安赛龙无缘八强雅思组合涉险过关新华社伯明翰3月16日电(记者张薇)全英羽毛球公开赛16日决出全部5个单项的八强,丹麦名将安赛龙爆冷不敌马来西亚小将黄智勇,止步男单16强。中国队的郑思维黄雅琼在先失一局的情况下逆讽刺官场骂奸商怼领导,赵本山这个时代的春晚小品,可真敢说在阅读此文之前,麻烦您点击一下关注,既方便您进行讨论和分享,又能给您带来不一样的参与感,感谢您的支持。文日斤编辑日斤引言宫廷玉液酒,一百八一杯。这酒怎么样啊,听我跟你吹!就这就18AI试驾员智能捏脸品牌中国元宇宙实验室精选案例发布!中新经纬3月17日电(马静闫淑鑫)将黄帝祠宇搬进数字世界真身复刻央视主持人王冠和数万观众在元宇宙里过春节当元宇宙走进现实,将产生哪些新的变革和碰撞?17日,第九届财经中国V论坛暨元中国计划今年底发射爱因斯坦探针,探索宇宙更暗更远的地方爱因斯坦探针卫星构想图。新华社发(中国科学院微小卫星创新研究院供图)中国计划于2023年底发射一颗新的X射线天文卫星爱因斯坦探针,有望捕捉超新星爆发出的第一缕光,帮助搜寻和精确定位重磅!来凤荣获中国天然氧吧称号3月16日在第四届氧吧产业发展大会暨2022年中国天然氧吧媒体推介会上来凤县荣获中国天然氧吧称号授牌仪式来凤县来凤县一脚踏三省,南邻湘西,西接渝东,境内武陵山绵延,酉水河纵贯,山川中国五大名茶是哪些?你喝过哪几种,最喜欢哪一种呢?中国是世界茶文化的源头,但我们已经很难查清饮茶这个习惯最早是起源于什么年代了,但是大致还是有那么几个传说能够说明茶大概什么时候出现的。我们不得不说世界上很多地区的饮茶风俗是中国带过