调用第三方接口实现呼叫台功能(两个端实时通信)+【系统声音(背景音乐)和麦克风声音混合】 -WebRTC
调用第三方接口实现呼叫台功能(两端实时通信) --WebRTC
WebRTC学习笔记——建立连接 - 简书 (jianshu.com)
(4条消息) webRTC(八):查看offer/answer 的 SDP_晓果博客的博客-CSDN博客
(4条消息) WebRTC源码研究(32)获取offer answer创建的SDP_极客雨露的博客-CSDN博客
(4条消息) webrtc 的 CreateOffer 过程分析_zhuiyuanqingya的博客-CSDN博客
参考网站一:WebRTC学习笔记——建立连接 - 简书 (jianshu.com)
WebRTC是一个开源的项目,可以提供浏览器,手机应用之间实时通信能力。
-
主要JavaScript API
- MediaStream 音视频流对象
- RTCPeerConnection 端对端音视频连接对象
- RTCDataChannel 端对端数据通道对象
1、由于浏览器API有相应的前缀,需要有两个兼容函数来首先处理一下:(这步作用不清楚照抄下来了)

function hasUserMedia() { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; return !!navigator.getUserMedia; } function hasRTCPeerConnection() { window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection; return !!window.RTCPeerConnection; }
2、可以通过配置自己的STUN服务器地址,或者不写配置使用浏览器默认的STUN服务器地址,来创建两个RTCPeerConnection对象来模拟连接
局域网可以不配置STUN服务器地址
不给config,局域网内两个peer是可以找到对方的 配置个内网stun也没用 如果有网络穿透的需求,那需要自己搭建一个stun服务器
//var configuration = { // //iceServers: [{ url: "stun:127.0.0.1:9876" }] //};
//,otherConnection是被调用方,被调用方是接收数据的
youConnection = new RTCPeerConnection(); otherConnection = new RTCPeerConnection();
3.通信双方交换ICE候选路径,也就是通过ICE获取到自己的IP和端口号后,再互相交换此信息(暂时也不清楚什么作用)
RTCPeerConnection 的属性 onicecandidate (是一个事件触发器 event handler) 能够让函数在事件icecandidate发生在实例 RTCPeerConnection 上时被调用。 只要本地代理ICE 需要通过信令服务器传递信息给其他对等端时就会触发。 这让本地代理与其他对等体相协商而浏览器本身在使用时无需知道任何详细的有关信令技术的细节,只需要简单地应用这种方法就可使用您选择的任何消息传递技术将ICE候选发送到远程对等方。
youConnection.onicecandidate = function (event) { if (event.candidate) { console.log(event.candidate); otherConnection.addIceCandidate(new RTCIceCandidate(event.candidate)); } }; otherConnection.onicecandidate = function (event) { if (event.candidate) { console.log(event.candidate); youConnection.addIceCandidate(new RTCIceCandidate(event.candidate)); } };
4.通信双方通过交换offer和answer来互换SDP信息 ---创建媒体协商
var offerOptions={ offerToRecceiveAudio: 1, offerToReceiveVideo: 1 }; youConnection.createOffer(offerOptions) .then(function(offer){ console.log(offer); youConnection.setLocalDescription(offer); otherConnection.setRemoteDescription(offer); otherConnection.createAnswer(offerOptions) .then(function(answer){ console.log(answer); otherConnection.setLocalDescription(answer); youConnection.setRemoteDescription(answer); }); });
5.这样通信双方的连接就建立起来了,可以向连接对象中添加媒体流,另一个连接对象就能读出媒体流,并实时显示在video标签中
youConnection.onaddstream=function(event){ you.srcObject=event.stream; }; otherConnection.addStream(stream);
参考网站二:(照着用发现错误,没实现功能)
(4条消息) WebRTC源码研究(32)获取offer answer创建的SDP_极客雨露的博客-CSDN博客
1、
// 1、使用JS严格语法 'use strict'
2、获取HTML中的元素标签--定义全局变量
// 16、定义全局变量localStream var localStream; // 22、定义全局变量pc1 var pc1; // 23、定义全局变量pc2 var pc2;
3、4、5、按钮--开始、关闭
6、实现start
判断浏览器是否支持navigator.mediaDevices 和 navigator.mediaDevices.getUserMedia
如果支持就要传人constraints使用getUserMedia获取音视频数据了
在调getUserMedia之前我们还有一个限制,是constraints

function start(){ // 11、在调getUserMedia之前我们还有一个限制,是constraints // 这这里面我们可以写video和audio,我们这里暂不采集音频了,如果你要采集设置audio为true好了 var constraints = { video: true, audio: false } // 7、判断浏览器是否支持navigator.mediaDevices 和 navigator.mediaDevices.getUserMedia // 如果有任何一个不支持我们就要打印一个错误信息并退出 if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ console.error('the getUserMedia is not supported!') return; }else { // 8、如果支持就要传人constraints使用getUserMedia获取音视频数据了 navigator.mediaDevices.getUserMedia(constraints) .then(gotMediaStream) // 9、如果成功我们就让他调gotMediaStream,这时候说明我们可以拿到这个stream了 .catch(handleError); // 10、否则就要处理这个错误 } }
支持并成功我们就调gotMediaStream,这时候说明我们可以拿到这个stream了

// 12、在我们采集成功之后我们会调用gotMediaStream,它有个参数就是stream function gotMediaStream(stream){ // 13、在这个stream里面要做两件事 // 14、第一件事就是将它设置为localVideo,这样采集到数据之后我们本地的localVideo就能将它展示出来 localVideo.srcObject = stream; /** 15、第二件事就是我们要将它赋值给一个全局的stream,这个localStream就是为了后面我们去添加流用的,就是我们在其他地方还要用到这个流, 所以我们不能让它是一个局部的,得让他是一个全局的 */ localStream = stream; }
否则处理错误-输出错误

// 17、处理错误 function handleError(err){ // 18、它有个参数err,当我们收到这个错误我们将它打印出来 console.log("Failed to call getUserMedia", err); }
7、实现call

function call() { var offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 0 } pc1 = new RTCPeerConnection(); pc2 = new RTCPeerConnection(); pc1.onicecandidate = (e) => { pc2.addIceCandidate(e.candidate) .catch(handleError); console.log('pc1 ICE candidate:', e.candidate); } pc1.iceconnectionstatechange = (e) => { console.log(`pc1 ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', e); } pc2.onicecandidate = (e) => { pc1.addIceCandidate(e.candidate) .catch(handleError); console.log('pc2 ICE candidate:', e.candidate); } pc2.iceconnectionstatechange = (e) => { console.log(`pc2 ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', e); } pc2.ontrack = gotRemoteStream; localStream.getTracks().forEach((track) => { pc1.addTrack(track, localStream); }); pc1.createOffer(offerOptions) .then(getOffer) .catch(handleError); }
7-1、这个逻辑略显复杂,首先是创建RTCPeerConnection
所以在这里先设置一个pc1等于new RTCPeerConnection()
在这个Connection里面实际上是有一个可选参数的,这个可选参数就涉及到网络传输的一些配置
我们整个ICE的一个配置,但是由于是我们在本机内进行传输,所以在这里我们就不设置参数了,因为它也是可选的,所以它这里就会使用本机host类型的candidate
这个pc1我们后面也要用到,所以我给他全局化一下
在创建一个pc2这样我们就创建了两个连接,拿到两个连接之后我们要添加一些事件给它
pc1 = new RTCPeerConnection(); pc2 = new RTCPeerConnection();
7-2、这些事件有几个重要的,那第一个因为这个是连接,连接建立好之后当我发送收集Candidate的时候,那我要知道现在已经收到一个了
// 收到之后我们要做一些事情,所以我们要处理addIceCandidate事件,其实做主要的就是这个事件
// send candidate to peer // receive candidate from peer
所以我们收到这个candidate之后就要交给对方去处理,所以pc1要调用pc2的addIceCandidate,因为是本机这里就没有信令了,假设信令被传回来了
当我们收集到一个candidate之后交给信令,那么信令传回来,这时候就给了pc2,
pc2收到这个candidate之后就调用addIceCandidate方法,传入的参数就是e.candidate

pc1.onicecandidate = (e) => { // 25、这些事件有几个重要的,那第一个因为这个是连接,连接建立好之后当我发送收集Candidate的时候,那我要知道现在已经收到一个了 // 收到之后我们要做一些事情,所以我们要处理addIceCandidate事件,其实做主要的就是这个事件 // send candidate to peer // receive candidate from peer /** * 27、它有个参数就是e,当有个事件的时候这个参数就传进来了,对于我们上面的逻辑,我们回顾一些此前的流程 * 当我们A调用者收到candidate之后,它会将这个candidate发送给这个信令服务器 * 那么信令服务器会中转到这个B端,那么这个B端会调用这个AddIceCandidate这个方法,将它存到对端的candidate List里去 * 所以整个过程就是A拿到它所有的可行的通路然后都交给B,B形成一个列表; * 那么B所以可行的通路又交给A,A拿到它的可行列表,然后双方进行这个连通性检测 * 那么如果通过之后那就可以传数据了,就是这样一个过程 * */ /** 28、所以我们收到这个candidate之后就要交给对方去处理,所以pc1要调用pc2的这个,因为是本机这里就没有信令了,假设信令被传回来了 * 当我们收集到一个candidate之后交给信令,那么信令传回来,这时候就给了pc2 */ /** * 29、pc2收到这个candidate之后就调用addIceCandidate方法,传入的参数就是e.candidate */ pc2.addIceCandidate(e.candidate) .catch(handleError); console.log('pc1 ICE candidate:', e.candidate); }

pc1.iceconnectionstatechange = (e) => { console.log(`pc1 ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', e); }
7-3、对于pc2也是同样道理,那它就交给p1
所以它就调用pc1.addIceCandidate,这是当他们收集到candidate之后要做的事情

pc2.onicecandidate = (e)=> { // send candidate to peer // receive candidate from peer /** * 31、所以它就调用pc1.addIceCandidate,这是当他们收集到candidate之后要做的事情 */ pc1.addIceCandidate(e.candidate) .catch(handleError); console.log('pc2 ICE candidate:', e.candidate); }

pc2.iceconnectionstatechange = (e) => { console.log(`pc2 ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', e); }
7-4、对于pc2还要处理一个onTrack,当双方通讯连接之后,当有流从对端过来的时候,会触发这个onTrack事件,
pc2是被调用方,被调用方是接收数据的,所以对于pc2它还有个ontrack事件 * 当它收到这个ontrack事件之后它需要调用gotRemoteStream
pc2.ontrack = gotRemoteStream;
7-5、在这个函数里它实际就是有多个流了

/** * 33、在这个函数里它实际就是有多个流了, */ function gotRemoteStream(e){ // 34、所以我们只要取其中的第一个就可以了 if(remoteVideo.srcObject !== e.streams[0]){ // 35、这样我们就将远端的音视频流传给了remoteVideo,当发送ontrack的时候也就是数据通过的时候, remoteVideo.srcObject = e.streams[0]; } }
7-6、add Stream to caller
接下来我们就要将本地采集的数据添加到第一添加到第一个pc1 = new RTCPeerConnection()中去
* 这样在创建媒体协商的时候才知道我们有哪些媒体数据,这个顺序不能乱,必须要先添加媒体数据再做后面的逻辑
* 二不能先做媒体协商然后在添加数据,因为你先做媒体协商的时候它知道你这里没有数据那么在媒体协商的时候它就没有媒体流
* 那么就是说在协商的时候它知道你是没有的,那么它在底层就不设置这些接收信息发收器,那么这个时候即使你后面设置了媒体流传给这个PeerConnection
* 那么它也不会进行传输的,所以我们要先添加流
* 添加流也比较简单,通过localStream调用getTracks就能调用到所有的轨道(音频轨/视频轨)
* 那对于每个轨道我们添加进去就完了,也就是forEach遍历进去,每次循环都能拿到一个track
* 当我们拿到这个track之后直接调用pc1.addTrack添加就好了,第一个参数就是track,第二个参数就是这个track所在的流localStream
* 这样就将本地所采集的音视频流添加到了pc1 这个PeerConnection
localStream.getTracks().forEach((track)=>{
pc1.addTrack(track, localStream);
});
7-7、那么这个时候我们就可以去创建这个pc1去媒体协商了,媒体协商第一步就是创建createOffer,
创建这个createOffer实际它里面有个 * offerOptions的,那么这个offerOptions我们在上面定义一下
创建offerOptions,那在这里你可以指定我创建我本地的媒体的时候,那都包括哪些信息, * 可以有视频流和音频流,
因为我们这里没有采集视频所以offerToReceiveVidio是0 * 有了这个之后我们就可以创建本地的媒体信息了
var offerOptions = { offerToReceiveAudio: 1, //音频 offerToReceiveVideo: 0 //视频 }
调用去创建offer
pc1.createOffer(offerOptions) .then(getOffer) // 39、它也是一个Promise的形式,当他成功的时候我们去调用getOffer .catch(handleError); // 40、如果失败了去调用一下handleError
7-8、创建offer
首先传入一个desc一个描述信息

function getOffer(desc){ /** 42、当我们拿到这个描述信息之后呢,还是回到我们当时协商的逻辑,对于A来说它首先创建Offer,创建Offer之后它会调用setLocalDescription * 将它设置到这个PeerConnection当中去,那么这个时候它会触发底层的ICE的收集candidate的这个动作 * 所以这里要调用pc1.setLocalDescription这个时候处理完了它就会收集candidate * 这个处理完了之后按照正常逻辑它应该send desc to signal到信令服务器 */ /** * 51、在这里首先是设置setLocalDescription */ pc1.setLocalDescription(desc); /** * 52、设置完了setLocalDescription之后呢,我们就可以将它的内容展示在textarea里 * desc本身并不是SDP,它下面有一个SDP的属性,它里面包含了我们第一个连接创建的offer的SDP * */ offerSdpTextarea.value = desc.sdp //send sdp to callee /** * 43、到了信令服务器之后,信令服务器会发给第二个人(b) * 所以第二个人就会receive * 所以第二个人收到desc之后呢首先pc2要调用setRemoteDescription,这时候将desc设置成它的远端 */ //receive sdp from caller pc2.setRemoteDescription(desc); /** * 44、设成远端之后呢它也要做它自己的事 * pc2就要调用createAnswer,如果调用createAnswer成功了它要调用gotAnswerDescription */ /** * 53、同样的到了我们在找找这个answer,调用createAnswer成功我们再调用getAnswer */ pc2.createAnswer().then(getAnswer) .catch(handleError); }
7-9、设成远端之后呢它也要做它自己的事 * pc2就要调用createAnswer,如果调用createAnswer成功了它要调用gotAnswerDescription

// 45、这样也会传入一个描述信息 function getAnswer(desc){ // 46、当远端它得到这个Answer之后,它也要设置它的setLocalDescription,当它调用了setLocalDescription之后它也开始收集candidate了 pc2.setLocalDescription(desc); // 54、设置textarea,这个也就是我们获取的answer成功之后answer的内容 answerSdpTextarea.value = desc.sdp //send sdp to caller //recieve sdp from callee /** 47、完了之后它去进行send desc to signal与pc1进行交换,pc1会接收recieve desc from signal 那么收到之后他就会设置这个pc1的setRemoteDescription 那么经过这样一个步骤整个协商就完成了 当所有协商完成之后,这些底层对candidate就收集完成了 收集完了进行交换形成对方的列表然后进行连接检测 连接检测完了之后就开始真正的数据发送过来了 */ pc1.setRemoteDescription(desc); }
设置这个pc1的setRemoteDescription,那么经过这样一个步骤整个协商就完成了 当所有协商完成之后,
这些底层对candidate就收集完成了 收集完了进行交换形成对方的列表然后进行连接检测 连接检测完了之后就开始真正的数据发送过来了
7-10、挂断,将pc1和pc2分别关闭,在将pc1和pc2对象设为null (挂断按钮触发事件)

// 48、挂断,将pc1和pc2分别关闭,在将pc1和pc2对象设为null function hangup(){ pc1.close(); pc2.close(); pc1 = null; pc2 = null; }
有相应错误 --暂时未解决
https://www.jb51.cc/faq/2890375.html
三、个人实现功能步骤--
通过调用第三方接口的推流技术,我们传递offer给第三方接收第三方返回的answer成功建立起通信
参考上面的两网址实现了呼叫台(收取系统声音媒体流MediaStream和麦克风声音媒体流MediaStream传给音响)的功能
1、收取系统声音MediaStream参考博客:获取媒体声音媒体流
2、收取麦克风声音MediaStream参考博客:获取麦克风媒体流
3、需要进行混流:系统声音媒体流MediaStream和麦克风媒体流MediaStream混合生成一个新的MediaStream给WebRTC (下面第四点)
注意:前三步在页面一加载就实现(获取两个流并且混流)--没必要等开启switch后再执行
4、使用WebRTC技术将混流的stream用来生成offer给对端,对端返回answer(当前没有掌握推流技术,借助第三方-给第三方offer,第三方返回我answer)
4-1、界面添加开关switch标签用于控制WebRTC开启关闭、js编写触发事件(开启、关闭)

<input class="switchbtn" type="checkbox" lay-filter="setcall_isrun" name="set_isrun" lay-skin="switch" lay-text="开启|关闭">
4-2、使用layui的switch,监听开关的状态(开、关)

var form; //form控制变量 var itemtable;//table控制变量 function initDataTable() { layui.use(['table', 'form'], function () { var table = layui.table; itemtable = table; var form1 = layui.form; form = form1; }); //监听开关事件 form.on('switch(setcall_isrun)', function (data) { var switchstate = data.elem.checked;//判断开关状态 if (switchstate == true) { start();//开启呼叫时间 $(".callstatediv").css("color", "#67C23A"); //css按钮变色 $("#callstatetext").css("color", "#67C23A");//css字体变色 document.getElementById("callstatetext").innerText = "已连接";//css修改相应div文本 //获取音量标签值调用方法设置麦克风音量 ctrsys(bboxvals); } else { hangup();//挂断事件 } }); }
4-3、开启呼叫方法

//呼叫台开启 function start() { //initMain() //实现呼叫 call(); }

//实现call function call() { //本人当前没有视频,所以video为0 var offerOptions = { offerToReceiveAudio: 1, //音频 offerToReceiveVideo: 0 //视频 } pc1 = new RTCPeerConnection(); // RTCPeerConnection 的属性 onicecandidate (是一个事件触发器 event handler) 能够让函数在事件icecandidate发生在实例 RTCPeerConnection 上时被调用。 只要本地代理ICE 需要通过信令服务器传递信息给其他对等端时就会触发。 这让本地代理与其他对等体相协商而浏览器本身在使用时无需知道任何详细的有关信令技术的细节,只需要简单地应用这种方法就可使用您选择的任何消息传递技术将ICE候选发送到远程对等方。 //pc1.onicecandidate = (e) => { // pc2.addIceCandidate(e.candidate) // .catch(handleError); // console.log('pc1 ICE candidate:', e.candidate); //} //console.error(localStream); //将混流后的stream给pc1 pc1.addStream(localStream); //pc1.addStream(localStream1); //pc1根据媒体SDP生成offer pc1.createOffer(offerOptions) .then(getOffer) .catch(handleError); }
4-4、我这里将生成的offer交给第三方,第三方返回answer(第三方提供推流的技术,他返回我answer)
/** * 获取offer和answer 实现RTC * @param {any} offer */ function getOffer(offer) { //获取到的offer(自动生成的)这样给pc1--- offer的值数据格式详见下图4-4-1 pc1.setLocalDescription(offer); /* console.error(offer);*/ //本人这里编码base64编码是因为第三方接口需要这样的数据格式 var sdp64 = window.btoa(offer.sdp) // 第三方接口需要base64编码的sdp数据 //调用第三方api接口同时获取返回的answer var Answer = getAnswer(sdp64); //解码--对返回的数据解码 var bitmap = window.atob(Answer); // 解码 //示例个对象--我必须这种方式 var RTCSessionDescription1 = { type: "answer", sdp: bitmap } //将这个对象(answer)再次给pc1 --此时两端成功建立了通信 answer值数据格式详见下图4-4-2 pc1.setRemoteDescription(new RTCSessionDescription(RTCSessionDescription1), function () { //pc1.createAnswer(function (bitmap) { // pc1.setLocalDescription(offer, function () { // // send the answer to the remote connection // }) //}) }); //console.error(Answer); //console.error(bitmap); }
offere和answer数据值示例:

sdp: "v=0 ↵o=- 2191361014014315170 2 IN IP4 127.0.0.1 ↵s=- ↵t=0 0 ↵a=group:BUNDLE 0 ↵a=msid-semantic: WMS 4d20f0dc-049f-4e02-b7cb-3221c73c7c6b ↵m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 ↵c=IN IP4 0.0.0.0 ↵a=rtcp:9 IN IP4 0.0.0.0 ↵a=ice-ufrag:LaMl ↵a=ice-pwd:ZLUulpQ2Sk7/jCF5B55Scit1 ↵a=ice-options:trickle ↵a=fingerprint:sha-256 3A:5D:AC:63:95:25:84:C0:4F:21:2E:AB:A3:68:BE:3F:36:CC:28:25:02:03:31:87:E4:02:0B:EE:DC:C7:F0:95 ↵a=setup:actpass ↵a=mid:0 ↵a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level ↵a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time ↵a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 ↵a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid ↵a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id ↵a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id ↵a=sendrecv ↵a=msid:4d20f0dc-049f-4e02-b7cb-3221c73c7c6b 71355d26-0949-4f86-a9a3-04b319ce4644 ↵a=rtcp-mux ↵a=rtpmap:111 opus/48000/2 ↵a=rtcp-fb:111 transport-cc ↵a=fmtp:111 minptime=10;useinbandfec=1 ↵a=rtpmap:103 ISAC/16000 ↵a=rtpmap:104 ISAC/32000 ↵a=rtpmap:9 G722/8000 ↵a=rtpmap:0 PCMU/8000 ↵a=rtpmap:8 PCMA/8000 ↵a=rtpmap:106 CN/32000 ↵a=rtpmap:105 CN/16000 ↵a=rtpmap:13 CN/8000 ↵a=rtpmap:110 telephone-event/48000 ↵a=rtpmap:112 telephone-event/32000 ↵a=rtpmap:113 telephone-event/16000 ↵a=rtpmap:126 telephone-event/8000 ↵a=ssrc:3970615299 cname:fBcIWkFxHwLIgaAC ↵a=ssrc:3970615299 msid:4d20f0dc-049f-4e02-b7cb-3221c73c7c6b 71355d26-0949-4f86-a9a3-04b319ce4644 ↵a=ssrc:3970615299 mslabel:4d20f0dc-049f-4e02-b7cb-3221c73c7c6b ↵a=ssrc:3970615299 label:71355d26-0949-4f86-a9a3-04b319ce4644 ↵"

dj0wDQpvPS0gMjE5MTM2MTAxNDAxNDMxNTE3MCAyIElOIElQNCAxMjcuMC4wLjENCnM9LQ0KdD0wIDANCmE9Z3JvdXA6QlVORExFIDANCmE9bXNpZC1zZW1hbnRpYzogV01TIDRkMjBmMGRjLTA0OWYtNGUwMi1iN2NiLTMyMjFjNzNjN2M2Yg0KbT1hdWRpbyA5IFVEUC9UTFMvUlRQL1NBVlBGIDExMSAxMDMgMTA0IDkgMCA4IDEwNiAxMDUgMTMgMTEwIDExMiAxMTMgMTI2DQpjPUlOIElQNCAwLjAuMC4wDQphPXJ0Y3A6OSBJTiBJUDQgMC4wLjAuMA0KYT1pY2UtdWZyYWc6TGFNbA0KYT1pY2UtcHdkOlpMVXVscFEyU2s3L2pDRjVCNTVTY2l0MQ0KYT1pY2Utb3B0aW9uczp0cmlja2xlDQphPWZpbmdlcnByaW50OnNoYS0yNTYgM0E6NUQ6QUM6NjM6OTU6MjU6ODQ6QzA6NEY6MjE6MkU6QUI6QTM6Njg6QkU6M0Y6MzY6Q0M6Mjg6MjU6MDI6MDM6MzE6ODc6RTQ6MDI6MEI6RUU6REM6Qzc6RjA6OTUNCmE9c2V0dXA6YWN0cGFzcw0KYT1taWQ6MA0KYT1leHRtYXA6MSB1cm46aWV0ZjpwYXJhbXM6cnRwLWhkcmV4dDpzc3JjLWF1ZGlvLWxldmVsDQphPWV4dG1hcDoyIGh0dHA6Ly93d3cud2VicnRjLm9yZy9leHBlcmltZW50cy9ydHAtaGRyZXh0L2Ficy1zZW5kLXRpbWUNCmE9ZXh0bWFwOjMgaHR0cDovL3d3dy5pZXRmLm9yZy9pZC9kcmFmdC1ob2xtZXItcm1jYXQtdHJhbnNwb3J0LXdpZGUtY2MtZXh0ZW5zaW9ucy0wMQ0KYT1leHRtYXA6NCB1cm46aWV0ZjpwYXJhbXM6cnRwLWhkcmV4dDpzZGVzOm1pZA0KYT1leHRtYXA6NSB1cm46aWV0ZjpwYXJhbXM6cnRwLWhkcmV4dDpzZGVzOnJ0cC1zdHJlYW0taWQNCmE9ZXh0bWFwOjYgdXJuOmlldGY6cGFyYW1zOnJ0cC1oZHJleHQ6c2RlczpyZXBhaXJlZC1ydHAtc3RyZWFtLWlkDQphPXNlbmRyZWN2DQphPW1zaWQ6NGQyMGYwZGMtMDQ5Zi00ZTAyLWI3Y2ItMzIyMWM3M2M3YzZiIDcxMzU1ZDI2LTA5NDktNGY4Ni1hOWEzLTA0YjMxOWNlNDY0NA0KYT1ydGNwLW11eA0KYT1ydHBtYXA6MTExIG9wdXMvNDgwMDAvMg0KYT1ydGNwLWZiOjExMSB0cmFuc3BvcnQtY2MNCmE9Zm10cDoxMTEgbWlucHRpbWU9MTA7dXNlaW5iYW5kZmVjPTENCmE9cnRwbWFwOjEwMyBJU0FDLzE2MDAwDQphPXJ0cG1hcDoxMDQgSVNBQy8zMjAwMA0KYT1ydHBtYXA6OSBHNzIyLzgwMDANCmE9cnRwbWFwOjAgUENNVS84MDAwDQphPXJ0cG1hcDo4IFBDTUEvODAwMA0KYT1ydHBtYXA6MTA2IENOLzMyMDAwDQphPXJ0cG1hcDoxMDUgQ04vMTYwMDANCmE9cnRwbWFwOjEzIENOLzgwMDANCmE9cnRwbWFwOjExMCB0ZWxlcGhvbmUtZXZlbnQvNDgwMDANCmE9cnRwbWFwOjExMiB0ZWxlcGhvbmUtZXZlbnQvMzIwMDANCmE9cnRwbWFwOjExMyB0ZWxlcGhvbmUtZXZlbnQvMTYwMDANCmE9cnRwbWFwOjEyNiB0ZWxlcGhvbmUtZXZlbnQvODAwMA0KYT1zc3JjOjM5NzA2MTUyOTkgY25hbWU6ZkJjSVdrRnhId0xJZ2FBQw0KYT1zc3JjOjM5NzA2MTUyOTkgbXNpZDo0ZDIwZjBkYy0wNDlmLTRlMDItYjdjYi0zMjIxYzczYzdjNmIgNzEzNTVkMjYtMDk0OS00Zjg2LWE5YTMtMDRiMzE5Y2U0NjQ0DQphPXNzcmM6Mzk3MDYxNTI5OSBtc2xhYmVsOjRkMjBmMGRjLTA0OWYtNGUwMi1iN2NiLTMyMjFjNzNjN2M2Yg0KYT1zc3JjOjM5NzA2MTUyOTkgbGFiZWw6NzEzNTVkMjYtMDk0OS00Zjg2LWE5YTMtMDRiMzE5Y2U0NjQ0DQo=
图4-4-1

sdp: "v=0 ↵o=- 2912908088209489875 1670835296 IN IP4 0.0.0.0 ↵s=- ↵t=0 0 ↵a=fingerprint:sha-256 66:B5:08:FF:C6:4B:6E:70:14:C0:60:BD:4D:07:C1:ED:FE:73:FE:0E:5E:64:2F:B5:8C:98:88:1F:E2:0F:C5:19 ↵a=group:BUNDLE 0 ↵m=audio 9 UDP/TLS/RTP/SAVPF 111 9 0 8 ↵c=IN IP4 0.0.0.0 ↵a=setup:active ↵a=mid:0 ↵a=ice-ufrag:wCtkysHeNdAVBMyT ↵a=ice-pwd:NXUReimhxfbhZyFMJZyKrNTOWFPstvDJ ↵a=rtcp-mux ↵a=rtcp-rsize ↵a=rtpmap:111 opus/48000/2 ↵a=fmtp:111 minptime=10;useinbandfec=1 ↵a=rtpmap:9 G722/8000 ↵a=rtpmap:0 PCMU/8000 ↵a=rtpmap:8 PCMA/8000 ↵a=recvonly ↵a=candidate:foundation 1 udp 2130706431 192.168.2.89 34121 typ host generation 0 ↵a=candidate:foundation 2 udp 2130706431 192.168.2.89 34121 typ host generation 0 ↵a=end-of-candidates ↵"

dj0wDQpvPS0gMjkxMjkwODA4ODIwOTQ4OTg3NSAxNjcwODM1Mjk2IElOIElQNCAwLjAuMC4wDQpzPS0NCnQ9MCAwDQphPWZpbmdlcnByaW50OnNoYS0yNTYgNjY6QjU6MDg6RkY6QzY6NEI6NkU6NzA6MTQ6QzA6NjA6QkQ6NEQ6MDc6QzE6RUQ6RkU6NzM6RkU6MEU6NUU6NjQ6MkY6QjU6OEM6OTg6ODg6MUY6RTI6MEY6QzU6MTkNCmE9Z3JvdXA6QlVORExFIDANCm09YXVkaW8gOSBVRFAvVExTL1JUUC9TQVZQRiAxMTEgOSAwIDgNCmM9SU4gSVA0IDAuMC4wLjANCmE9c2V0dXA6YWN0aXZlDQphPW1pZDowDQphPWljZS11ZnJhZzp3Q3RreXNIZU5kQVZCTXlUDQphPWljZS1wd2Q6TlhVUmVpbWh4ZmJoWnlGTUpaeUtyTlRPV0ZQc3R2REoNCmE9cnRjcC1tdXgNCmE9cnRjcC1yc2l6ZQ0KYT1ydHBtYXA6MTExIG9wdXMvNDgwMDAvMg0KYT1mbXRwOjExMSBtaW5wdGltZT0xMDt1c2VpbmJhbmRmZWM9MQ0KYT1ydHBtYXA6OSBHNzIyLzgwMDANCmE9cnRwbWFwOjAgUENNVS84MDAwDQphPXJ0cG1hcDo4IFBDTUEvODAwMA0KYT1yZWN2b25seQ0KYT1jYW5kaWRhdGU6Zm91bmRhdGlvbiAxIHVkcCAyMTMwNzA2NDMxIDE5Mi4xNjguMi44OSAzNDEyMSB0eXAgaG9zdCBnZW5lcmF0aW9uIDANCmE9Y2FuZGlkYXRlOmZvdW5kYXRpb24gMiB1ZHAgMjEzMDcwNjQzMSAxOTIuMTY4LjIuODkgMzQxMjEgdHlwIGhvc3QgZ2VuZXJhdGlvbiAwDQphPWVuZC1vZi1jYW5kaWRhdGVzDQo=
图4-4-2
此上实现两个端的通信
本地播放媒体静音
https://www.cnblogs.com/whlBooK/p/16785555.html
audio不播放音频
四、系统声音(背景音乐)和麦克风声音混合
参考博客获取麦克风声音和另一博客获取系统声音的MediaStream(媒体流)
将两个媒体流合流 调用这个方法传入系统媒体流和麦克风采集流

function getNewStream(SysStream, micStream) { console.error(SysStream) console.error(micStream) if (!SysStream.getAudioTracks().length) { SysStream.addTrack(micStream.getAudioTracks()[0]) return SysStream; } var context = new AudioContext(); var baseSource = context.createMediaStreamSource(SysStream); var extraSource = context.createMediaStreamSource(micStream); var dest = context.createMediaStreamDestination(); var baseGain = context.createGain(); var extraGain = context.createGain(); baseGain.gain.value = 0.8; extraGain.gain.value = 0.8; baseSource.connect(baseGain).connect(dest); extraSource.connect(extraGain).connect(dest); localStream = dest.stream; }
采集麦克风声音和系统声音
利用HTML5 Web Audio API给网页JS交互增加声音 « 张鑫旭-鑫空间-鑫生活 (zhangxinxu.com)
https://blog.csdn.net/qq_28306529/article/details/82620753
Web Audio API 的运用 - Web API 接口参考 | MDN (mozilla.org)
控制麦克风输入分量
本文来自博客园,作者:じ逐梦,转载请注明原文链接:https://www.cnblogs.com/ZhuMeng-Chao/p/16937583.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现