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; }
散文中年之后,人生追寻的最美风景,就是内心的安静 作者子墨二月,尽管春天来得迟了一些,即便春天的美丽,还在山水有相逢的路上,当季节变暖的时候,邂逅的依旧是美好。当春风拂过,山水有了生机,心也会变得柔软起来。而这一份喜悦,无关风月,
中华文脉经典围读会穿越中华文脉,走进一个遍地都是英雄史诗的时代 中国人之所以被称为汉人汉民族,正是因为汉代。秦汉,一个文字统一文学繁荣,遍地都是英雄史诗的时代。2月14日,由五粮液独家支持南方周末与南瓜视业联合打造的中华文脉经典围读会第二场泱泱
2512!263!加盟湖人大放异彩,能突能投能传,老詹果然赌赢了 洛杉矶湖人队大幅调整了球员阵容之后,球队开始慢慢好转了起来,特别是新援拉塞尔和范德比尔特的首秀效果令人惊艳,湖人队整体进攻变得更加流畅了起来,浓眉哥八村塁范德比尔特的前场有高度,有
天赋易昺(bng),创造历史! 中国网坛历史性的一刻当地时间2月12日在ATP250达拉斯男单决赛中中国球员吴易昺(bng)延续出色状态顶住对手44记ACE的猛烈冲击化解四个赛点后以67(4)76(3)76(12
安徽肥西春来农事忙 2月14日,肥西县紫蓬山旅游开发区张老圩村风之谷家庭农场内,农民正在采摘荠菜。陈家乐摄2月14日,肥西县紫蓬山旅游开发区张老圩村风之谷家庭农场内,农民正在加紧采摘红菜苔。陈家乐摄2
赠人玫瑰,手留余香 传说在南山上有一座庙,庙里供奉着一尊佛祖,这尊佛祖十分灵验,只要是信徒诚心诚意的许愿,佛祖都会帮他实现。一位信徒听说了此事,决心要去向佛祖许个愿,希望转变他十年考功名不得的命运。他
平静的心态 今天看到东莞发生的一处悲剧。据说是因为中介介绍工作,结果工资比所介绍的耍低,嫌疑人殷某努而犯事,造成三死的悲惨结局。我认为现代人都很浮燥。需要保持一颗宁静安详的心!心灵的平静,是智
从0100京东云数智供应链服务滨州高质量发展 在时光里扎根,好似希声无形,5年却足以孕育从0到1的成长京东黄河三角洲云计算大数据产业基地拔地而起京东(滨州)数字经济产业园集聚起产业链条滨州湖滨产业智能供应链冷链园项目开工建设2
ABC三类人,你在哪一层? 年啊,就这么过完了,莽莽苍苍潦潦草草。今天有个小网友给我发信息阿姐,你怎么没来呀?其实是有点后悔的。虽然我知道过年期间的景德镇没什么可看的,可也比待在家里发呆泡在网上强。人的一生究
(随笔)说韩城 这两天,陕西省韩城市又火了,又上央视新闻了。因为,在陕西韩城陶渠遗址发现了西周和东周时期的8座甲字型大墓。考古人员初步推断,此处墓地为商周时期京国所在地。作为陕西人看到这则新闻还是
梅州大埔县洲瑞镇武装部组织开展义务植树活动 春潮涌动日,绿化正当时。在第45个植树节到来之际,梅州大埔县洲瑞镇武装部组织基干民兵前往洲瑞镇下营村开展义务植树活动,用实际行动以绿为底绘就洲瑞壮美秀丽生态画卷。在植树现场,基干民
海外华侨华人热热闹闹过春节中国兔火遍世界各地 图为人们在马来西亚吉隆坡一家商场观赏生肖兔新春装饰。张纹综摄(新华社发)图为兔年春节主题电车行驶在意大利罗马街头。新华社记者金马梦妮摄1月14日,在法国巴黎举行的兔年生肖邮票发行仪
上网课需要,有没有一个靠谱的同声翻译软件? 英语渣渣上英语网课的时候真的好痛苦啊!口罩时期有幸感受过,有时候老师语速太快真的完全跟不上,实在是太难受了。真的很需要一个可以同声翻译的软件,如果你也需要的话那就一起看下去吧!!!
千亿住房租赁贷款支持计划蓄势待发租购并举进程正加速 2月9日,中指研究院发布住房租赁市场月报,数据显示,2023年1月,受春节影响,市场仍然处于淡季,租金整体水平继续调整。不过2022年,以培育发展保障性租赁住房为重心,我国住房租赁
(社会)北京出境团队旅游业务重启 文化和旅游部发布关于试点恢复旅行社经营中国公民赴有关国家出境团队旅游业务的通知,2月6日起,试点恢复全国旅行社及在线旅游企业经营中国公民赴20个国家的出境团队旅游和机票酒店业务。暂
加速亚太化进程,北约捆绑韩日兴风作浪! 近日,北约秘书长斯托尔滕贝格与日本首相岸田文雄发表联合声明称,双方将在海上安全网络空间军备控制等领域展开合作,同时不忘拿中国军力以及台湾问题说事。在此之前,斯托尔滕贝格在访问韩国时
ampampquot武汉新城ampampquot到底在豪赌什么?(经济观察之113) 武汉新城经济观察房地产市场投资理财一面是商品房天量库存卖不掉,房地产被纳入困难行业一面又规划再建一个面积超700多平方公里,容纳人口超700万的武汉新城。这两则消息近期都登上了网络
冰与火之歌落下帷幕,雷诺三菱谋制衡 2月6日,雷诺集团日产汽车和三菱汽车工业在雷诺集团和日产董事会批准后,宣布了三家公司联盟的新举措。其中,雷诺集团和日产已同意相互持有15的股份。雷诺集团表示,其15的股份涉及锁定和
信用卡分期太坑了怎么说?分期需要注意什么? 信用卡的使用人数逐年上升,并且这个趋势也逐渐明显,由此可见,信用卡的受欢迎程度是很高的。下面小编来给大家介绍一下信用卡分期太坑了怎么说?分期需要注意什么?感兴趣的话就和小编一起来看
尿素价格继续下跌,2月9日最新尿素出厂报价 哈喽,大家好!这里是化肥价格行情!关注我们每天看最新尿素复合肥磷铵钾肥价格行情!今天(2023年2月9日),下面我们说说今天国内尿素价格行情!价格继续下行!今日国内尿素市场继续稳中
下一个乌克兰出现?北约步步紧逼扩至中国家门口,中方如何反应 俄乌冲突还在持续,北约国家在对俄罗斯发起围堵的同时,又将目光放在了亚洲,甚至已经形成了俄乌冲突结束之后,下一个开战国就是中国的舆论风向。这一舆论其实并不准确,事实上,不管俄乌冲突是
科技助力涪陵青菜头提质增产 工人日报中工网记者赵昂通讯员夏斐然对青菜头的产量品质抗逆性有明显提高,对比效果好,建议在生产上大面积推广应用。这是重庆市涪陵区农学会晟农博316植物全营养素在青菜头应用课题验收小组