codec:編碼譯碼器,編解碼器。它是一個程式,也可以是演算法,或者裝置,用於編碼(encode)和解碼(decode)資料流。
WebRTC能讓兩個web或者app之間建立音視訊通訊。通訊過程中,資料流的格式必須被兩邊的裝置支援。
WebRTC提供了介面查詢支援的codec,並且可以設定要使用的codec。本文演示選擇視訊codec的過程。
示例
使用者可以在傳送視訊流之前選擇codec。把支援的codec型別列出來,使用者自行選擇。
開啟視訊後,建立連線前,我們可以選擇設定codec。如上圖藍色區域所示。
html
先來準備頁面。2個video控制元件分別顯示收發視訊。
按鈕分別控制開始,呼叫(發起連線)和結束通話。
select
用來選擇codec。獲取支援的codec資訊,放到下拉欄裡讓使用者選擇。
以下是index.html主要內容
<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>
adapter-2021.js是存放在本地的檔案。要使用最新的adapter,按以下地址引入
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
js
main.js控制主要邏輯。從開啟攝像頭開始。建立連線前可以選擇codec。
建立連線的流程與「WebRTC模擬傳輸視訊流,video通過本地節點peer傳輸視訊流」類似。
獲取可用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
呼叫之前,找到使用者選擇的codec。
呼叫transceiver.setCodecPreferences(codecs)
,把選中的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);
}
}
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資訊說明
觀察控制檯,列印出了可用codec資訊(Mac,97.0.4692.71(正式版本)x86_64)。主要關注下面3種
{clockRate: 90000, mimeType: 'video/VP8'}
{clockRate: 90000, mimeType: 'video/VP9', sdpFmtpLine: 'profile-id=0'}
{clockRate: 90000, mimeType: 'video/H264', sdpFmtpLine: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f'}
clockRate
是codec的時脈頻率,單位hz
sdpFmtpLine
是codec的SDP裡a=fmtp
的引數資訊
mimeType
裡說的是視訊編碼型別,常見的有VP8和H264等等
支援WebRTC的瀏覽器,必須要支援視訊codec 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