我们有时候在音频通话过程中,想要改成视频通话。如果挂断当前通话再重新发起视频通话就会显得比较麻烦。

因此很多app提供了将音频通话升级成视频通话的功能,同时也有将视频通话降为音频通话的功能。

本文演示的是在本地模拟音频通话,并且将音频通话升级为视频通话。

准备

界面很简单,2个video加上几个按钮。

<video id="localVideo" playsinline autoplay muted></video>
<video id="remoteVideo" playsinline autoplay></video> <div>
<button id="startBtn">开始</button>
<button id="callBtn">Call</button>
<button id="upgradeBtn">升级为视频通话</button>
<button id="hangupBtn">挂断</button>
</div>

用的是本地的adapter

<script src="../../src/js/adapter-2021.js"></script>

js

先来把元素拿到

const startBtn = document.getElementById('startBtn');
const callBtn = document.getElementById('callBtn');
const upgradeToVideoBtn = document.getElementById('upgradeBtn');
const hangupBtn = document.getElementById('hangupBtn');
const localVideo = document.getElementById('localVideo'); // 本地预览
const remoteVideo = document.getElementById('remoteVideo'); // 接收方

监听器

设置一些监听

localVideo.addEventListener('loadedmetadata', function () {
console.log(`localVideo 宽高: ${this.videoWidth}px, ${this.videoHeight}px`);
}); remoteVideo.addEventListener('loadedmetadata', function () {
console.log(`remoteVideo 宽高: ${this.videoWidth}px, ${this.videoHeight}px`);
}); let startTime;
remoteVideo.onresize = () => {
console.log(`remoteVideo onresize 宽高: ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`);
if (startTime) {
const elapsedTime = window.performance.now() - startTime;
console.log(`建立连接耗时: ${elapsedTime.toFixed(3)}ms`);
startTime = null;
}
}; startBtn.onclick = start;
callBtn.onclick = call;
upgradeToVideoBtn.onclick = upgrade;
hangupBtn.onclick = hangup;

打一些状态变化的log

function onCreateSessionDescriptionError(error) {
console.log(`rustfisher.com:创建会话描述失败, session description err: ${error.toString()}`);
} function onIceStateChange(pc, event) {
if (pc) {
console.log(`rustfisher.com:${getName(pc)} ICE状态: ${pc.iceConnectionState}`);
console.log('rustfisher.com:ICE状态变化: ', event);
}
} function onAddIceCandidateSuccess(pc) {
console.log(`rustfisher.com:${getName(pc)} addIceCandidate success 添加ICE候选成功`);
} function onAddIceCandidateError(pc, error) {
console.log(`rustfisher.com:${getName(pc)} 添加ICE候选失败 failed to add ICE Candidate: ${error.toString()}`);
} function onSetLocalSuccess(pc) {
console.log(`rustfisher.com:${getName(pc)} setLocalDescription 成功`);
} function onSetSessionDescriptionError(error) {
console.log(`rustfisher.com:设置会话描述失败: ${error.toString()}`);
} function onSetRemoteSuccess(pc) {
console.log(`rustfisher.com:${getName(pc)} 设置远程描述成功 setRemoteDescription complete`);
} // 辅助方法
function getName(pc) {
return (pc === pc1) ? 'pc1' : 'pc2';
} function getOtherPc(pc) {
return (pc === pc1) ? pc2 : pc1;
}

开始

获取本地的音频数据流,交给localVideo

function gotStream(stream) {
console.log('获取到了本地数据流');
localVideo.srcObject = stream;
localStream = stream;
callBtn.disabled = false;
} function start() {
console.log('请求本地数据流 纯音频');
startBtn.disabled = true;
navigator.mediaDevices
.getUserMedia({ audio: true, video: false })
.then(gotStream)
.catch(e => alert(`getUserMedia() error: ${e.name}`));
}

call

发起音频呼叫

function call() {
callBtn.disabled = true;
upgradeToVideoBtn.disabled = false;
hangupBtn.disabled = false;
console.log('开始呼叫...');
startTime = window.performance.now();
const audioTracks = localStream.getAudioTracks();
if (audioTracks.length > 0) {
console.log(`使用的音频设备: ${audioTracks[0].label}`);
}
const servers = null; // 就在本地测试
pc1 = new RTCPeerConnection(servers);
console.log('创建本地节点 pc1');
pc1.onicecandidate = e => onIceCandidate(pc1, e);
pc2 = new RTCPeerConnection(servers);
console.log('rustfisher.com:创建模拟远端节点 pc2');
pc2.onicecandidate = e => onIceCandidate(pc2, e);
pc1.oniceconnectionstatechange = e => onIceStateChange(pc1, e);
pc2.oniceconnectionstatechange = e => onIceStateChange(pc2, e);
pc2.ontrack = gotRemoteStream; localStream.getTracks().forEach(track => pc1.addTrack(track, localStream));
console.log('rustfisher.com:将本地数据流交给pc1'); console.log('rustfisher.com:pc1开始创建offer');
pc1.createOffer(offerOptions).then(onCreateOfferSuccess, onCreateSessionDescriptionError);
} function gotRemoteStream(e) {
console.log('获取到远程数据流', e.track, e.streams[0]);
remoteVideo.srcObject = null;
remoteVideo.srcObject = e.streams[0];
} function onIceCandidate(pc, event) {
getOtherPc(pc)
.addIceCandidate(event.candidate)
.then(() => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err));
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
} function onCreateOfferSuccess(desc) {
console.log(`pc1提供了offer\n${desc.sdp}`);
console.log('pc1 setLocalDescription start');
pc1.setLocalDescription(desc).then(() => onSetLocalSuccess(pc1), onSetSessionDescriptionError);
console.log('pc2 setRemoteDescription start');
pc2.setRemoteDescription(desc).then(() => onSetRemoteSuccess(pc2), onSetSessionDescriptionError);
console.log('pc2 createAnswer start');
pc2.createAnswer().then(onCreateAnswerSuccess, onCreateSessionDescriptionError);
} function onCreateAnswerSuccess(desc) {
console.log(`rustfisher.com:pc2应答成功: ${desc.sdp}`);
console.log('pc2 setLocalDescription start');
pc2.setLocalDescription(desc).then(() => onSetLocalSuccess(pc2), onSetSessionDescriptionError);
console.log('pc1 setRemoteDescription start');
pc1.setRemoteDescription(desc).then(() => onSetRemoteSuccess(pc1), onSetSessionDescriptionError);
}
  • 创建RTCPeerConnection
  • 设置onicecandidate监听ICE候选
  • 设置oniceconnectionstatechange监听ICE连接状态变化
  • 接收方监听ontrack
  • 发送方pc1 addTrack将当前数据流添加进去
  • 发送方pc1创建offer createOffer
  • pc1创建好offer后,接收方pc2应答 createAnswer

升级到视频通话

upgrade()方法处理升级操作

function upgrade() {
upgradeToVideoBtn.disabled = true;
navigator.mediaDevices
.getUserMedia({ video: true })
.then(stream => {
console.log('rustfisher.com:获取到了视频流');
const videoTracks = stream.getVideoTracks();
if (videoTracks.length > 0) {
console.log(`video device: ${videoTracks[0].label}`);
}
localStream.addTrack(videoTracks[0]);
localVideo.srcObject = null; // 重置视频流
localVideo.srcObject = localStream;
pc1.addTrack(videoTracks[0], localStream);
return pc1.createOffer();
})
.then(offer => pc1.setLocalDescription(offer))
.then(() => pc2.setRemoteDescription(pc1.localDescription))
.then(() => pc2.createAnswer())
.then(answer => pc2.setLocalDescription(answer))
.then(() => pc1.setRemoteDescription(pc2.localDescription));
}

发送方去获取音频数据流getUserMedia

将音频轨道添加进localStream,并且发送方也要添加轨道 pc1.addTrack

创建offer createOffer

后面就是接收方pc2应答

挂断

简单的挂断功能如下

function hangup() {
console.log('rustfisher.com:挂断');
pc1.close();
pc2.close();
pc1 = null;
pc2 = null; const videoTracks = localStream.getVideoTracks();
videoTracks.forEach(videoTrack => {
videoTrack.stop();
localStream.removeTrack(videoTrack);
}); localVideo.srcObject = null;
localVideo.srcObject = localStream; hangupBtn.disabled = true;
callBtn.disabled = false;
}

主要是把呼出方的流关闭掉

代码流程描述图

将用户的操作(按钮)和主要代码对应起来

效果预览

效果预览请参考WebRTC音频通话升级到视频通话

原文链接

WebRTC音频通话升级为视频通话的更多相关文章

  1. WebRTC VoiceEngine综合应用示例(二)——音频通话的基本流程(转)

    下面将以实现一个音频通话功能为示例详细介绍VoiceEngine的使用,在文末将附上相应源码的下载地址.这里参考的是voiceengine\voe_cmd_test. 第一步是创建VoiceEngin ...

  2. AliIAC 智能音频编解码器:在有限带宽条件下带来更高质量的音频通话体验

    随着信息技术的发展,人们对实时通信的需求不断增加,并逐渐成为工作生活中不可或缺的一部分.每年海量的音视频通话分钟数对互联网基础设施提出了巨大的挑战.尽管目前全球的互联网用户绝大多数均处于良好的网络状况 ...

  3. 单独编译和使用webrtc音频回声消除模块(附完整源码+测试音频文件)

    单独编译和使用webrtc音频降噪模块(附完整源码+测试音频文件) 单独编译和使用webrtc音频增益模块(附完整源码+测试音频文件) 说实话很不想写这篇文章,因为这和我一贯推崇的最好全部编译并使用w ...

  4. 单独编译和使用webrtc音频降噪模块(附完整源码+测试音频文件)

    单独编译和使用webrtc音频增益模块(附完整源码+测试音频文件) 单独编译和使用webrtc音频回声消除模块(附完整源码+测试音频文件) webrtc的音频处理模块分为降噪ns,回音消除aec,回声 ...

  5. 单独编译和使用webrtc音频增益模块(附完整源码+测试音频文件)

    webrtc的音频处理模块分为降噪ns和nsx,回音消除aec,回声控制acem,音频增益agc,静音检测部分.另外webrtc已经封装好了一套音频处理模块APM,如果不是有特殊必要,使用者如果要用到 ...

  6. WebRTC音频预处理单元APM的整体编译及使用

    正文 行的gnu静态库链接路径是针对NDK版本 r8d 的,如读者版本不匹配,请自行找到 libgnustl_static.a 静态库的路径进行替换. 3)本示例并不打算编译 WebRTC 的测试工程 ...

  7. WebRTC 音频采样算法 附完整C++示例代码

    之前有大概介绍了音频采样相关的思路,详情见<简洁明了的插值音频重采样算法例子 (附完整C代码)>. 音频方面的开源项目很多很多. 最知名的莫过于谷歌开源的WebRTC, 其中的音频模块就包 ...

  8. WebRTC 音频算法 附完整C代码

    WebRTC提供一套音频处理引擎, 包含以下算法: AGC自动增益控制(Automatic Gain Control) ANS噪音抑制(Automatic Noise Suppression) AEC ...

  9. webrtc 音频一点相关知识

    采样频率:  44.1kHz ,它的意思是每秒取样44100次   .8kHz    8000次,  16kHz   160000次 比特率:  比特率是大家常听说的一个名词,数码录音一般使用16比特 ...

随机推荐

  1. 【Tool】IntelliJ IDEA Ultimate2019.1 中文版 安装

    IntelliJ IDEA Ultimate2019.1 2019-07-26  09:26:24  by冲冲 1. 下载 https://mp.weixin.qq.com/s/SdFQqGzMy-g ...

  2. [NOI Online #3 提高组] 魔法值

    现在只会\(O(qn^3log)\)的\(40pts\)做法,鸽了. 反正就是预处理之后,去掉一个\(n\). 我预处理了,没去\(n\),hhhh,成功减少了一半的常数.

  3. Matlab指针

    Matlab指针 第一印象貌似是Matlab中不存在指针,所有变量与函数的赋值都是按值传递的,不会对参数进行修改.其实Matlab提供了handle类作为指针代替品.只要我们利用handle子类,就可 ...

  4. [Linux] 非root安装Lefse软件及其数据分析

    说明 Lefse软件是宏组学物种研究常用软件,一般大家用在线版本即可.但要搭建在Linux集群环境中有点烦,记录一下折腾过程. 安装 这个软件是python2写的,因此假设我已经安装好了较高版本的py ...

  5. R语言与医学统计图形-【20】ggplot2图例

    ggplot2绘图系统--图例:guide函数.标度函数.overrides.aes参数 图例调整函数guide_legend也属于标度函数,但不能单独作为对象使用,即不能如p+guide_legen ...

  6. 也谈string.Join和StringBuilder的性能比较

    前几天在园子里面看到一篇讲StringBuilder性能的文章.文章里面给出了一个测试用例,比较StringBuilder.AppendJoin和String.Join的性能.根据该测试结果,&quo ...

  7. 【Redis集群原理专题】分析一下相关的Redis集群模式下的脑裂问题!

    技术格言 世界上并没有完美的程序,但是我们并不因此而沮丧,因为写程序就是一个不断追求完美的过程. 什么是脑裂 字面含义 首先,脑裂从字面上理解就是脑袋裂开了,就是思想分家了,就是有了两个山头,就是有了 ...

  8. Python中的随机采样和概率分布(一)

    Python(包括其包Numpy)中包含了了许多概率算法,包括基础的随机采样以及许多经典的概率分布生成.我们这个系列介绍几个在机器学习中常用的概率函数.先来看最基础的功能--随机采样. 1. rand ...

  9. 在C++的map类型中按value排序

    1.将map转化为vector类型 2.使用sort函数对vector进行排序,写出compare比较器函数 3.比较器中指明按照第几个元素来排序 1 #include <iostream> ...

  10. 【Go语言学习笔记】包

    包其实是每个大型工程都会使用的模块化工具. 将相关的代码封装成一个包,给其他项目调用,提供不同的功能. GO的设计是将一个文件夹看成一个包,虽然不一定非要用文件夹的名字,但是比较建议. 同一个文件夹下 ...