文章目录
在开始一对一通话实战前,先看下RTCPeerConnection的定义及可选参数;
RTCPeerConnection接口代表一个由本地计算机到远端的WebRTC连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。
其接口的定义如下:
declare var RTCPeerConnection: {
prototype: RTCPeerConnection;
new(configuration?: RTCConfiguration): RTCPeerConnection;
generateCertificate(keygenAlgorithm: AlgorithmIdentifier): Promise<RTCCertificate>;
};
注意其中有一个可选参数RTCConfiguration, 在文档中定义如下;
interface RTCConfiguration {
bundlePolicy?: RTCBundlePolicy;
certificates?: RTCCertificate[];
iceCandidatePoolSize?: number;
iceServers?: RTCIceServer[];
iceTransportPolicy?: RTCIceTransportPolicy;
rtcpMuxPolicy?: RTCRtcpMuxPolicy;
}
iceServers,由多个RTCIceServer组成需要填入stun货turn服务的地址;

iceTransportPolicy :ice的传输策略,默认值是all允许考虑所有候选者,值有"all",“public” 已弃用 ,“relay” 只收集中继候选者;
rtcpMuxPolicy:收集 ICE 候选时是否使用的 RTCP 多路复用策略。值有 'negotiate’和 ‘require’;
bundlePolicy: ‘balanced’、‘max-compat’和’max-bundle’;各个含义如下:

一般的使用如下:
const config = {
bundlePolicy: 'balanced',
// certificates?: RTCCertificate[];
// iceCandidatePoolSize?: number;
iceTransportPolicy: "all",// public relay
rtcpMuxPolicy : 'negotiate',
iceServers: [
{
urls: "turn:www.lymggylove.top:3478",
username: "lym",
credential: "123456"
}
]
};
主要以js为例,做简单的demo展示,本地设备获取的逻辑之前的文章有介绍,这里修改如下:
// 客户端的socketio ,用于后续发送信令
var socket;
// 房间ID 后续的消息都要携带这个ID
var room;
// 本地流 mediastream
var localStream;
// 防止重复去获取设备列表
var isGet = false;
var isStartRecored = false;
// 记录是不是已经调用set remote接口,因为addicecandidate的调用,要在set remote之后
var isSetRemote = false;
// 全局的RTCPeerconnection对象
let peerconnetion = null;
// 是不是主叫
var isOffer = true;
// sdp对象。主要是主角方发送的offer sdp的缓存;
var recvSdp = {
sdp: null,
type: null
};
//消息队列用于存放 candidate消息
var cacheCandidateMsg = [];
//记录socket连接服务后返回的自己当前客户端的ID信息
var selfid = '';
其中localstream做成全局的是因为其他地方需要使用,比如录制视频的时候,赋值代码如下:
function startWebCam() {
return new Promise((resolve, reject) => {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
document.write('当前浏览器不支持 getUserMedia()!!!!/n');
return reject('当前浏览器不支持 getUserMedia()!!!!/n');
} else {
// 想要获取一个最接近 1280x720 的相机分辨率
const videoDeviceIds = videoSource.value;
const audioDeviceIds = audioSource.value;
var constraints = {
audio: {
noiseSuppression: true, // 降噪
echoCancellation: true,// 回音消除
deviceId: videoDeviceIds ? videoDeviceIds : undefined
},
video: {
width: 320,
height: 240,
frameRate: { ideal: 10, max: 15 },
deviceId: audioDeviceIds ? audioDeviceIds : undefined
},
};
navigator.mediaDevices.getUserMedia(constraints).then(function (mediaStream) {
localStream = mediaStream;
// 获取视频的track
const videoTrack = mediaStream.getVideoTracks()[0];
//拿到video的所有约束
const videoConstraints = videoTrack.getSettings();
// 转成jsonstring显示到div标签上
showDiv.textContent = JSON.stringify(videoConstraints, null, 2);
videoPlayer.srcObject = mediaStream;
videoPlayer.onloadedmetadata = function (e) {
videoPlayer.play();
};
// 获取权限后开始获取设备
return resolve(mediaStream);
}).catch((err) => {
return reject(err);
console.log(err.name + ": " + err.message);
}); // 总是在最后检查错误
}
});
}
这里转成Promise写法是为了后面使用asyn/await方便,同样的获取设备列表修改如下:
function getUserMedia() {
return new Promise((resolve, reject) => {
navigator.mediaDevices.enumerateDevices().then((devices) => {
if (!isGet) {
isGet = true;
devices.forEach((devInfo) => {
var option = document.createElement('option');
option.text = devInfo.label;
option.value = devInfo.deviceId;
if (devInfo.kind === 'audioinput') {
audioSource.appendChild(option);
} else if (devInfo.kind === 'audiooutput') {
audioOutput.appendChild(option);
} else if (devInfo.kind === 'videoinput') {
videoSource.appendChild(option);
}
});
}
resolve(devices);
});
});
}
async function InitPeerconnect() {
console.log('开始初始化摄像头。。。。');
await startWebCam();
await getUserMedia();
console.log('结束初始化摄像头。。。。');
const config = {
bundlePolicy: 'balanced',
// certificates?: RTCCertificate[];
// iceCandidatePoolSize?: number;
iceTransportPolicy: "all",// public relay
rtcpMuxPolicy: 'negotiate',
iceServers: [
{
urls: "turn:www.lymggylove.top:3478",
username: "lym",
credential: "123456"
}
]
};
peerconnetion = new RTCPeerConnection(config);
peerconnetion.ontrack = (ev) => {
if (ev.streams && ev.streams[0]) {
remoteVideoPlayer.srcObject = ev.streams[0];
} else {
const inboundStream = new MediaStream();
inboundStream.addTrack(ev.track);
remoteVideoPlayer.srcObject = inboundStream;
}
// if (trackEvent.track.kind === 'video') {
// remoteVideoPlayer.srcObject = trackEvent[0];
// }
};
peerconnetion.onicecandidate = async (ev) => {
console.log('=======>' + JSON.stringify(ev.candidate));
if (socket) {
await socket.emit('message', room, {
type: 2,
candidate: ev.candidate
});
}
};
peerconnetion.oniceconnectionstatechange = (ev)=>{
outputArea.scrollTop = outputArea.scrollHeight;//窗口总是显示最后的内容
outputArea.value = outputArea.value + JSON.stringify(peerconnetion.iceConnectionState) + '\r';
};
//添加本地媒体流
for (const track of localStream.getTracks()) {
peerconnetion.addTrack(track);
}
if (isOffer) {
const offerOption = {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
const offerSdp = await peerconnetion.createOffer(offerOption);
if (socket) {
await socket.emit('message', room, {
type: 0,
sdp: offerSdp
});
}
const errLocalDescription = await peerconnetion.setLocalDescription(offerSdp);
if (errLocalDescription) {
console.error('setLocalDescription err :' + JSON.stringify(offerSdp));
return;
}
} else {
const answerOption = {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
// RTCSessionDescriptionInit init =
const errsetRemoteDescription = await peerconnetion.setRemoteDescription(recvSdp);
if (errsetRemoteDescription) {
console.error('answer setRemoteDescription err :' + JSON.stringify(recvSdp));
return;
}
isSetRemote = true;
const answerSDP = await peerconnetion.createAnswer(answerOption);
if (socket) {
await socket.emit('message', room, {
type: 1,
sdp: answerSDP
});
}
//发送出去
const setLocalDescriptionErr = await peerconnetion.setLocalDescription(answerSDP);
addcandidateFUN();
}
}
函数开始使用asyc/await的语法糖去获取本地的媒体流,这样可以使的流程看起来更简洁,
onicecandidate回调ice打洞地址信息用于通过信令服务发送个对端。oniceconnectionstatechange是ice打洞的状态信息,可以输出到控制台,这里为了方便直接输出到textview上,显示的信息如下:
socket.on('message', (room, id, data) => {
if (id === selfid) {
return;
}
const type = data.type;
switch (type) {
case 0: {// offer
isOffer = false;
recvSdp = data.sdp;
InitPeerconnect();
}
break;
case 1: {// answer
peerconnetion.setRemoteDescription(data.sdp);
isSetRemote = true;
addcandidateFUN();
}
break;
case 2: {// candidate
if (isSetRemote == true) {
outputArea.scrollTop = outputArea.scrollHeight;//窗口总是显示最后的内容
} else {
cacheCandidateMsg.push(data.candidate);
addcandidateFUN();
}
outputArea.value = outputArea.value + JSON.stringify(data.candidate) + '\r';
peerconnetion.addIceCandidate(data.candidate);
}
break;
default:
break;
}
// outputArea.scrollTop = outputArea.scrollHeight;//窗口总是显示最后的内容
// outputArea.value = outputArea.value + data + '\r';
});
这里发送的消息每一个都有一个type用于表示消息的类型,客户端在收到后按照不同类型处理。如果是offer的消息,这时候被叫就可以开始初始化peer及接口调用;
function addcandidateFUN(){
cacheCandidateMsg.forEach((item, index, arr)=> {
peerconnetion.addIceCandidate(item) }); // undefined
cacheCandidateMsg = [];
}
循环去调用addIceCandidate方法,把缓存的所有candidate设置给peer;然后清空缓存;
5 结束方法如下:
function peerCloseFun () {
isStartRecored = false;
for (const track of localStream.getTracks()) {
// peerconnetion.removeTrack(track);
track.stop();
}
peerconnetion.close();
localStream = null;
cacheCandidateMsg = [];
videoPlayer.srcObject = null;
remoteVideoPlayer.srcObject = null;
isSetRemote = false;
isOffer = true;
recvSdp = null;
inputArea.value = '';
peerconnetion = null;
}
释放的主要是把所有的本地流停掉,然后调用peerconnection的stop方法;接着释放全局变量为下一次通话做准备;
完整代码地址:WebRTCDemo js
测试网页:demo效果展示
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
基础版云数据库RDS的产品系列包括基础版、高可用版、集群版、三节点企业版,本文介绍基础版实例的相关信息。RDS基础版实例也称为单机版实例,只有单个数据库节点,计算与存储分离,性价比超高。说明RDS基础版实例只有一个数据库节点,没有备节点作为热备份,因此当该节点意外宕机或者执行重启实例、变更配置、版本升级等任务时,会出现较长时间的不可用。如果业务对数据库的可用性要求较高,不建议使用基础版实例,可选择其他系列(如高可用版),部分基础版实例也支持升级为高可用版。基础版与高可用版的对比拓扑图如下所示。优势 性能由于不提供备节点,主节点不会因为实时的数据库复制而产生额外的性能开销,因此基础版的性能相对于
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复