视频编码后,再进行发送。WebRTC建立视频连接前,可以选择codec。一般来说支持多种codec,以VP8和H264为代表。

Codec: 编码译码器,编解码器

示例代码

写一个示例,用户可以在发送视频流之前选择codec。把支持的codec类型列出来,用户自行选择。

html

放上2个video控件显示收发视频。按钮控制开始,呼叫(发起连接)和挂断。

<div id="container">
<h1><a href="https://an.rustfisher.com/webrtc/peerconnection/change-codec/" title="WebRTC示例,修改codec">WebRTC示例,修改codec</a>
</h1> <video id="localVideo" playsinline autoplay muted></video>
<video id="remoteVideo" playsinline autoplay></video> <div class="box">
<button id="startBtn">开始</button>
<button id="callBtn">呼叫</button>
<button id="hangupBtn">挂断</button>
</div> <div class="box">
<span>选择Codec:</span>
<select id="codecPreferences" disabled>
<option selected value="">Default</option>
</select>
<div id="actualCodec"></div>
</div>
<p>可以在控制台观察 <code>MediaStream</code>, <code>localStream</code>, 和 <code>RTCPeerConnection</code></p>
</div> <script src="../../src/js/adapter-2021.js"></script>
<script src="js/main.js" async></script>

select用来选择codec。获取支持的codec信息,放到下拉栏里让用户选择。

adapter-2021.js是存放在本地的文件。要使用最新的adapter,按以下地址引入

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

js

main.js控制主要逻辑。从开启摄像头开始。建立连接前可以选择codec。

建立连接的流程与「WebRTC模拟传输视频流,video通过本地节点peer传输视频流」类似

main.js完整代码如下


'use strict'; console.log('WebRTC示例,选择codec'); // --------- ui准备 ---------
const startBtn = document.getElementById('startBtn');
const callBtn = document.getElementById('callBtn');
const hangupBtn = document.getElementById('hangupBtn');
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo'); callBtn.disabled = true;
hangupBtn.disabled = true;
startBtn.addEventListener('click', start);
callBtn.addEventListener('click', call);
hangupBtn.addEventListener('click', hangup);
// --------------------------- // -------- codec 的配置 --------
const codecPreferences = document.querySelector('#codecPreferences');
const supportsSetCodecPreferences = window.RTCRtpTransceiver &&
'setCodecPreferences' in window.RTCRtpTransceiver.prototype;
// ----------------------------- let startTime; remoteVideo.addEventListener('resize', () => {
console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`);
if (startTime) {
const elapsedTime = window.performance.now() - startTime;
console.log('视频流连接耗时: ' + elapsedTime.toFixed(3) + 'ms');
startTime = null;
}
}); let localStream;
let pc1;
let pc2;
const offerOptions = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
}; function getName(pc) {
return (pc === pc1) ? 'pc1' : 'pc2';
} function getOtherPc(pc) {
return (pc === pc1) ? pc2 : pc1;
} // 启动本地视频
async function start() {
console.log('启动本地视频');
startBtn.disabled = true;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
console.log('获取到本地视频');
localVideo.srcObject = stream;
localStream = stream;
callBtn.disabled = false;
} catch (e) {
alert(`getUserMedia() error: ${e.name}`);
}
if (supportsSetCodecPreferences) {
const { codecs } = RTCRtpSender.getCapabilities('video');
console.log('RTCRtpSender.getCapabilities(video):\n', codecs);
codecs.forEach(codec => {
if (['video/red', 'video/ulpfec', 'video/rtx'].includes(codec.mimeType)) {
return;
}
const option = document.createElement('option');
option.value = (codec.mimeType + ' ' + (codec.sdpFmtpLine || '')).trim();
option.innerText = option.value;
codecPreferences.appendChild(option);
});
codecPreferences.disabled = false;
} else {
console.warn('当前不支持更换codec');
}
} // 呼叫并建立连接
async function call() {
callBtn.disabled = true;
hangupBtn.disabled = false;
console.log('开始呼叫');
startTime = window.performance.now();
const videoTracks = localStream.getVideoTracks();
const audioTracks = localStream.getAudioTracks();
if (videoTracks.length > 0) {
console.log(`使用的摄像头: ${videoTracks[0].label}`);
}
if (audioTracks.length > 0) {
console.log(`使用的麦克风: ${audioTracks[0].label}`);
}
const configuration = {};
pc1 = new RTCPeerConnection(configuration);
pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e));
pc2 = new RTCPeerConnection(configuration);
pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
pc2.addEventListener('track', gotRemoteStream); localStream.getTracks().forEach(track => pc1.addTrack(track, localStream));
if (supportsSetCodecPreferences) {
// 获取选择的codec
const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex];
if (preferredCodec.value !== '') {
const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' ');
const { codecs } = RTCRtpSender.getCapabilities('video');
const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
const selectedCodec = codecs[selectedCodecIndex];
codecs.splice(selectedCodecIndex, 1);
codecs.unshift(selectedCodec);
console.log(codecs);
const transceiver = pc1.getTransceivers().find(t => t.sender && t.sender.track === localStream.getVideoTracks()[0]);
transceiver.setCodecPreferences(codecs);
console.log('选择的codec', selectedCodec);
}
}
codecPreferences.disabled = true; try {
const offer = await pc1.createOffer(offerOptions);
await onCreateOfferSuccess(offer);
} catch (e) {
console.log(`Failed, pc1 createOffer: ${e.toString()}`);
}
} async function onCreateOfferSuccess(desc) {
try {
await pc1.setLocalDescription(desc);
console.log('pc1 setLocalDescription 成功');
} catch (e) {
console.error('pc1 setLocalDescription 出错', e);
}
try {
await pc2.setRemoteDescription(desc);
console.log('pc2 setRemoteDescription ok');
} catch (e) {
console.error('pc2 setRemoteDescription fail', e);
}
try {
const answer = await pc2.createAnswer();
await onCreateAnswerSuccess(answer);
} catch (e) {
console.log(`pc2 create answer fail: ${e.toString()}`);
}
} function gotRemoteStream(e) {
if (remoteVideo.srcObject !== e.streams[0]) {
remoteVideo.srcObject = e.streams[0];
console.log('pc2 received remote stream');
}
} // 应答(接收)成功
async function onCreateAnswerSuccess(desc) {
console.log(`Answer from pc2:\n${desc.sdp}`);
console.log('pc2 setLocalDescription start');
try {
await pc2.setLocalDescription(desc);
} catch (e) {
console.error('pc2 set local d fail', e);
}
console.log('pc1 setRemoteDescription start');
try {
await pc1.setRemoteDescription(desc); // Display the video codec that is actually used.
setTimeout(async () => {
const stats = await pc1.getStats();
stats.forEach(stat => {
if (!(stat.type === 'outbound-rtp' && stat.kind === 'video')) {
return;
}
const codec = stats.get(stat.codecId);
document.getElementById('actualCodec').innerText = 'Using ' + codec.mimeType +
' ' + (codec.sdpFmtpLine ? codec.sdpFmtpLine + ' ' : '') +
', payloadType=' + codec.payloadType + '. Encoder: ' + stat.encoderImplementation;
});
}, 1000);
} catch (e) {
console.error(e);
}
} async function onIceCandidate(pc, event) {
try {
await (getOtherPc(pc).addIceCandidate(event.candidate));
onAddIceCandidateSuccess(pc);
} catch (e) {
onAddIceCandidateError(pc, e);
}
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
} function onAddIceCandidateSuccess(pc) {
console.log(`${getName(pc)} addIceCandidate success`);
} function onAddIceCandidateError(pc, error) {
console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);
} localVideo.addEventListener('loadedmetadata', function () {
console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`);
}); remoteVideo.addEventListener('loadedmetadata', function () {
console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`);
}); // 挂断
function hangup() {
console.log('挂断');
pc1.close();
pc2.close();
pc1 = null;
pc2 = null;
hangupBtn.disabled = true;
callBtn.disabled = false;
codecPreferences.disabled = false;
}

获取可用codec

先判断浏览器是否有RTCRtpTransceiver,并且要能支持setCodecPreferences方法

const supportsSetCodecPreferences = window.RTCRtpTransceiver &&
'setCodecPreferences' in window.RTCRtpTransceiver.prototype;

通过RTCRtpSender.getCapabilities('video')获取可支持的codec。

然后把它们放进列表codecPreferences

  if (supportsSetCodecPreferences) {
const { codecs } = RTCRtpSender.getCapabilities('video'); codecs.forEach(codec => {
if (['video/red', 'video/ulpfec', 'video/rtx'].includes(codec.mimeType)) {
return;
}
const option = document.createElement('option');
option.value = (codec.mimeType + ' ' + (codec.sdpFmtpLine || '')).trim();
option.innerText = option.value;
codecPreferences.appendChild(option);
});
codecPreferences.disabled = false;
}

呼叫的时候,把选中的codec交给transceiver

  if (supportsSetCodecPreferences) {
// 获取选择的codec
const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex];
if (preferredCodec.value !== '') {
const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' ');
const { codecs } = RTCRtpSender.getCapabilities('video');
const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
const selectedCodec = codecs[selectedCodecIndex];
codecs.splice(selectedCodecIndex, 1);
codecs.unshift(selectedCodec);
console.log(codecs);
const transceiver = pc1.getTransceivers().find(t => t.sender && t.sender.track === localStream.getVideoTracks()[0]);
transceiver.setCodecPreferences(codecs);
console.log('选择的codec', selectedCodec);
}
}

codec

观察控制台的log,打印出了可用codec信息。

0: {clockRate: 90000, mimeType: 'video/VP8'}
1: {clockRate: 90000, mimeType: 'video/rtx'}
2: {clockRate: 90000, mimeType: 'video/VP9', sdpFmtpLine: 'profile-id=0'}
3: {clockRate: 90000, mimeType: 'video/VP9', sdpFmtpLine: 'profile-id=2'}
4: {clockRate: 90000, mimeType: 'video/H264', sdpFmtpLine: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f'}
5: {clockRate: 90000, mimeType: 'video/H264', sdpFmtpLine: 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f'}
6: {clockRate: 90000, mimeType: 'video/H264', sdpFmtpLine: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f'}
7: {clockRate: 90000, mimeType: 'video/H264', sdpFmtpLine: 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f'}
8: {clockRate: 90000, mimeType: 'video/AV1'}
9: {clockRate: 90000, mimeType: 'video/H264', sdpFmtpLine: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032'}
10: {clockRate: 90000, mimeType: 'video/H264', sdpFmtpLine: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032'}
11: {clockRate: 90000, mimeType: 'video/red'}
12: {clockRate: 90000, mimeType: 'video/ulpfec'}

clockRate 是codec的时钟频率,单位hz

sdpFmtpLine 是codec的SDP里a=fmtp的参数信息

mimeType 里说的是视频编码类型,常见的有VP8和H264等等。下面是视频格式的一些介绍。

VP8 VP9

2010年5月Google收购了On2 Technologies,获得了VP8。

Opera,FireFox,Chrome和Chromium支持HTML5中的video播放VP8视频。

WebM作为一个容器格式,图像部分使用VP8,音频使用Vorbis和Opus。

VP9由Google开发,一个开放的无版权费的视频编码标准。开发初期曾用名“Next Gen Open Video”。VP9也被视为是VP8的下一代视频编码标准。

H264

H.264,又称为MPEG-4第10部分,高级视频编码是一种面向块,基于运动补偿的视频编码标准。

到2014年,它已经成为高精度视频录制、压缩和发布的最常用格式之一。

优势:

  • 1)网络亲和性,即可适用于各种传输网络
  • 2)高的视频压缩比

目前我们用的比较多的还是H264。

效果预览

网页做好,效果请参考 选择codec

扩展阅读

WebRTC本地选择codec(web本地模拟)的更多相关文章

  1. ArcGIS Portal 10.4 本地坐标系的web 3d地形展示制作说明

    原文:ArcGIS Portal 10.4 本地坐标系的web 3d地形展示制作说明 ArcGIS Portal 10.4 本地坐标系的web 3d地形展示制作说明 By 李远祥 ArcGIS Por ...

  2. 本地git部署web连接azure的git存储库

    ​​​本地git部署web 创建本地存储仓库 输入以下命令创建git本地仓库(会在当前目录下生产一个.git的目录) git init 然后提交内容 在git仓库所在的目录下存放好需要的网页文件 将文 ...

  3. web 本地存储 (localStorage、sessionStorage)

    web 本地存储 (localStorage.sessionStorage,cookie) localStorage(长期储存):即使关闭浏览器数据也不会删除,除非使用localStorage.cle ...

  4. [web 前端] web本地存储(localStorage、sessionStorage)

    cp from : https://blog.csdn.net/mjzhang1993/article/details/70820868 web 本地存储 (localStorage.sessionS ...

  5. web本地存储(localStorage、sessionStorage)

    web 本地存储 (localStorage.sessionStorage) 说明 对浏览器来说,使用 Web Storage 存储键值对比存储 Cookie 方式更直观,而且容量更大,它包含两种:l ...

  6. intelliJ IDEA 怎么添加本地的idea web项目

    概述:这篇文章主要讲述idea开发工具怎么添加本地的idea web项目. 一:首先介绍一下idea web项目的目录结构: 上图详细简单的说了一下idea web项目的文件情况. 二:说明一下部署本 ...

  7. HTML5权威指南--Web Storage,本地数据库,本地缓存API,Web Sockets API,Geolocation API(简要学习笔记二)

    1.Web Storage HTML5除了Canvas元素之外,还有一个非常重要的功能那就是客户端本地保存数据的Web Storage功能. 以前都是用cookies保存用户名等简单信息.   但是c ...

  8. 【HTML5】HTML5本地数据库(Web Sql Database)

    Web Sql数据库简介 Web SQL数据库API实际上不是HTML5规范的组成部分,而是单独的规范.它通过一套API来操纵客户端的数据库. Web SQL数据库的浏览器支持情况 Web SQL 数 ...

  9. (转)HTML5开发学习(3):本地存储之Web Sql Database

    原文:http://www.cnblogs.com/xumingxiang/archive/2012/03/25/2416386.html HTML5开发学习(3):本地存储之Web Sql Data ...

随机推荐

  1. mysql深度优化与理解(迄今为止读到最优秀的mysql博客)

    转载:https://www.cnblogs.com/shenzikun1314/p/6396105.html 本篇深入了解查询优化和服务器的内部机制,了解MySql如何执行特定查询,从中也可以知道如 ...

  2. [OpenGL ES 02]OpenGL ES渲染管线与着色器

    [OpenGL ES 02]OpenGL ES渲染管线与着色器 罗朝辉 (http://www.cnblogs.com/kesalin/) 本文遵循"署名-非商业用途-保持一致"创 ...

  3. Java synchonized 同步

    Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this ...

  4. 一个使用 asyncio 开发的网络爬虫(译文)

    原文地址:https://www.aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html 作者简介 A. Jesse Jiryu ...

  5. CF1177A Digits Sequence (Easy Edition) 题解

    Content 一个序列由从 \(1\) 开始的数字不断在末端拼接,就像这样:\(12345678910111213141516...\).现在,给定一个数字 \(k\),请输出这个序列的第 \(k\ ...

  6. Java面向对象~类和对象&方法,类方法

    面向对象 概念:     1.同一类事物的抽象描述,不是具体的    2.类和对象的关系:        类 是抽象的.        对象 是具体的.    3.对象的体征,称为"属性&q ...

  7. [WPF] 实现 WPF 的 Inner Shadow

    在 WPF 中,我们通常用 DropShadow 做阴影效果,但都是做外阴影.内阴影(Inner Shadow)的话其实也不是不可以,就是有些曲折.这篇文章介绍几种做内引用的做法. 文章涉及到以下概念 ...

  8. 【LeetCode】544. Output Contest Matches 解题报告 (C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 遍历 日期 题目地址:https://leetcode ...

  9. 过河(状态压缩,dp)

    描述 在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧.在桥上有一些石子,青蛙很讨厌踩在这些石子上.由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数轴上 ...

  10. 【python】PyQt5 QAction 添加点击事件

    def test(): #your function ui.yourQActionName.triggered.connect(lambda:test()) #添加lambda: 就不报错了