WebRTC-Experiment demos 目录实战指南:即插即用的 WebRTC 实验与 Demo 全解析
WebRTC-Experiment demos 目录实战指南:即插即用的 WebRTC 实验与 Demo 全解析
导读
本指南基于 WebRTC-Experiment 开源仓库中的 demos/README.md 及其同目录下的 9 个真实实验页面,系统讲解这些"即插即用"(Plug & play)WebRTC 演示的原理与用法:从 Chrome/Firefox 两大浏览器的 RTCDataChannel 数据通道实现,到视频聊天、屏幕共享、流切换(重协商)等经典场景。读完本文,你将掌握这些实验的完整可运行代码、底层 RTCPeerConnection 信令调用链,以及如何把其中的 JavaScript 片段直接复制进自己的站点。
一、demos 目录定位:零安装、零依赖的 WebRTC 实验集
demos/README.md 开篇就给出了这个目录的核心定位:Realtime working WebRTC Experiments and Demos,即"实时可工作的 WebRTC 实验与演示"。其设计理念非常明确——Plug & play(即插即用):无需安装任何东西,没有额外依赖,只需要把页面里的 JavaScript 代码复制到你的站点中即可运行。
这种"单页面实验"的思路贯穿整个目录。与仓库中其他需要信令服务器、需要 npm install 的完整产品(如 RTCMultiConnection、RecordRTC)不同,demos 目录刻意保持了最小化:大多数实验在浏览器里打开就能跑,甚至可以直接把代码粘贴到浏览器控制台执行。
从源码结构看,该目录共包含 9 个演示页面(见 demos 目录):
| 文件 | 实验主题 |
|---|---|
| client-side-datachannel.html | 基于 RTCDataChannel 的文本聊天 |
| client-side.html | 简单视频聊天(PeerConnection 直连) |
| client-side-socket-io.html | 基于 Socket.io 信令的视频分享 |
| client-side-websocket.html | 基于 WebSocket 信令的视频分享 |
| audio-only-streaming.html | 纯音频流分享 |
| MediaStreamTrack.getSources.html | 枚举媒体输入设备 |
| screen-and-video-from-single-peer.html | 单条 PeerConnection 同时共享屏幕与音视频 |
| switch-streams.html | 运行时把屏幕共享流切换为音视频流(重协商) |
| remote-stream-recording.html | 远端流录制(配合 MediaStreamRecorder) |
README 中的实验索引表将这 9 个实验分为三大主题:流切换(重协商)、屏幕共享、基于不同信令通道的视频分享,外加单独的 RTCDataChannel 数据通道专题。下面我们逐一深入。
二、Chrome-to-Chrome RTCDataChannel 数据连接:单页即跑的完整示例
demos/README.md 中第一个核心代码示例就是 Chrome-to-Chrome 的 DataChannel 数据连接。原文档特别强调:"这是一个单页面 demo,把代码复制/粘贴到控制台标签页,就完成了!"(It is a single-page demo. Copy/paste code in the console tab and you're done!)
2.1 完整可运行代码
var iceServers = {
iceServers: [{
url: 'stun:stun.l.google.com:19302'
}]
};
var optionalRtpDataChannels = {
optional: [{
RtpDataChannels: true
}]
};
var offerer = new webkitRTCPeerConnection(iceServers, optionalRtpDataChannels),
answerer, answererDataChannel;
var offererDataChannel = offerer.createDataChannel('RTCDataChannel', {
reliable: false
});
setChannelEvents(offererDataChannel, 'offerer');
offerer.onicecandidate = function (event) {
if (!event || !event.candidate) return;
answerer && answerer.addIceCandidate(event.candidate);
};
var mediaConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: false, // Hmm!!
OfferToReceiveVideo: false // Hmm!!
}
};
offerer.createOffer(function (sessionDescription) {
offerer.setLocalDescription(sessionDescription);
createAnswer(sessionDescription);
}, null, mediaConstraints);
function createAnswer(offerSDP) {
answerer = new webkitRTCPeerConnection(iceServers, optionalRtpDataChannels);
answererDataChannel = answerer.createDataChannel('RTCDataChannel', {
reliable: false
});
setChannelEvents(answererDataChannel, 'answerer');
answerer.onicecandidate = function (event) {
if (!event || !event.candidate) return;
offerer && offerer.addIceCandidate(event.candidate);
};
answerer.setRemoteDescription(offerSDP);
answerer.createAnswer(function (sessionDescription) {
answerer.setLocalDescription(sessionDescription);
offerer.setRemoteDescription(sessionDescription);
}, null, mediaConstraints);
}
function setChannelEvents(channel, channelNameForConsoleOutput) {
channel.onmessage = function (event) {
console.debug(channelNameForConsoleOutput, 'received a message:', event.data);
};
channel.onopen = function () {
channel.send('first text message over RTP data ports');
};
}
当 RTP 数据端口打开之后,可以这样双向发送文本消息:
offererDataChannel.send('text message');
answererDataChannel.send('text message');
2.2 关键参数与调用链解析
对照同目录的源码 client-side-datachannel.html(其中的 RTCDataChannelForChrome 函数),可以还原这套代码的完整语义:
- ICE 服务器:使用 Google 公共 STUN 服务器
stun:stun.l.google.com:19302,帮助双方穿越 NAT 发现公网候选地址。仅用于本地回环测试时可省略,但真实网络环境下必须配置。 - RTP 数据通道选项:
optional: [{ RtpDataChannels: true }]是早期 Chrome 实现中将 DataChannel 承载在 RTP 之上的开关(现代浏览器已统一为 SCTP,该选项已过时)。 reliable: false:指定通道为不可靠(UDP 语义)模式,牺牲部分可靠性换取低延迟;若需要可靠传输应设为true(默认)。OfferToReceiveAudio: false/OfferToReceiveVideo: false:纯数据通道场景下显式关闭媒体接收,避免协商出无用的媒体 m-line——代码注释中的 "Hmm!!" 正是原作者提醒读者此处刻意置 false 的用意。- 握手顺序:offerer 先
createOffer→setLocalDescription,把 offer SDP 通过createAnswer传入;answerersetRemoteDescription后createAnswer并setLocalDescription,最后把 answer SDP 回传给 offerer 的setRemoteDescription。ICE candidate 则在两端间互相addIceCandidate交换。 - 事件绑定:
onopen触发后立即发送首条测试消息;onmessage把收到的数据打印到控制台。
值得注意:README 展示的是压缩到控制台可直接运行的版本,而 HTML 页面 client-side-datachannel.html 则额外做了 UI 层:两个 <input> 输入框绑定到 offererDataChannel.send() 与 answererDataChannel.send(),并用 HTMLElement.prototype.addLI 把收到的消息渲染成 <li> 列表,是理解"DataChannel 双向收发"最直观的页面。
2.3 版本提醒
原文档与 client-side-datachannel.html 页面都明确标注:该 Demo 发布于 2013 年、已经过时(out-dated)。当时使用的 webkitRTCPeerConnection、createOffer(successCallback, errorCallback, constraints) 回调式 API 已被现代浏览器的 Promise 式 RTCPeerConnection 取代。因此本文代码的价值在于教学理解信令握手与 DataChannel 建立流程;在当下浏览器中运行请改用 new RTCPeerConnection() + navigator.mediaDevices 等新 API。
三、Firefox-to-Firefox RTCDataChannel 数据连接:SCTP 数据端口
3.1 背景与完整代码
README 指出:2013 年 4 月起,Mozilla 在 Firefox 的 DataChannel 实现中做了破坏性变更,因此下面的代码适用于后续版本("following code will work with upcoming releases")。与 Chrome 的 RTP 承载不同,Firefox 走的是 SCTP 数据端口 路线。
function setChannelEvents(channel, channelNameForConsoleOutput) {
channel.onmessage = function (event) {
console.debug(channelNameForConsoleOutput, 'received a message:', event.data);
};
channel.onopen = function () {
channel.send('first text message over SCTP data ports');
};
}
function useless() {}
var iceServers = {
iceServers: [{
url: 'stun:23.21.150.121'
}
]
};
var offerer = new mozRTCPeerConnection(iceServers),
answerer, answererDataChannel, offererDataChannel;
offererDataChannel = offerer.createDataChannel('channel', {});
offererDataChannel.binaryType = 'blob';
setChannelEvents(offererDataChannel, 'offerer');
navigator.mozGetUserMedia({
audio: true,
fake: true
}, function (stream) {
offerer.addStream(stream);
offerer.createOffer(function (sessionDescription) {
offerer.setLocalDescription(sessionDescription);
createAnswer(sessionDescription);
}, null, mediaConstraints);
}, useless);
var mediaConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
function createAnswer(offerSDP) {
answerer = new mozRTCPeerConnection(iceServers);
answerer.ondatachannel = function (event) {
answererDataChannel = event.channel;
answererDataChannel.binaryType = 'blob';
setChannelEvents(answererDataChannel, 'answerer');
};
navigator.mozGetUserMedia({
audio: true,
fake: true
}, function (stream) {
answerer.addStream(stream);
answerer.setRemoteDescription(offerSDP);
answerer.createAnswer(function (sessionDescription) {
answerer.setLocalDescription(sessionDescription);
offerer.setRemoteDescription(sessionDescription);
}, null, mediaConstraints);
}, useless);
}
SCTP 数据端口打开后,不仅可以发送文本,还可以发送对象(object)、Blob 或文件:
offererDataChannel.send('text message');
offererDataChannel.send(data);
offererDataChannel.send(blob);
offererDataChannel.send(file);
answererDataChannel.send('text message');
answererDataChannel.send(data);
answererDataChannel.send(blob);
answererDataChannel.send(file);
3.2 与 Chrome 实现的关键差异
对照 client-side-datachannel.html 中的 RTCDataChannelForFirefox 函数,Firefox 路线有三个显著不同点:
- 接收端通道获取方式不同:Chrome 的 answerer 也需要显式
createDataChannel;而 Firefox 的 answerer 靠ondatachannel事件被动接收 offerer 创建的通道(event.channel)。这正是 SDP 协商语义差异的体现——通道只由 offerer 侧创建,对端通过事件回调获得引用。 binaryType = 'blob':设置二进制数据的接收类型为 Blob,从而支持发送/接收 Blob 与文件。- 必须附带媒体流:Firefox 早期实现要求连接中至少有一个媒体流才能建立,因此代码里用
mozGetUserMedia({ audio: true, fake: true })获取"假"音频流并addStream到连接中——fake: true让 Firefox 不请求真实麦克风权限即可生成静音流。mediaConstraints也因此必须OfferToReceiveAudio/Video: true。
3.3 旧版(已废弃)Firefox 数据连接实现
README 还保留了更早期 Firefox 的实现供历史参考。旧版需要手动指定数据端口:var dataPorts = [123, 321]; // [inbound, outbound],并在 SDP 交换完成后调用 connectDataConnection 显式建立数据连接:
offerer.setRemoteDescription(sessionDescription, function () {
answerer.connectDataConnection(dataPorts[0], dataPorts[1]);
offerer.connectDataConnection(dataPorts[1], dataPorts[0]);
});
新版则完全由 createDataChannel + ondatachannel 自动完成,无需手动指定端口。这段代码与新版的关键区别在于:旧版依赖 offerer.onconnection 事件在连接建立后才 createDataChannel,且 createDataChannel 的调用时机受制于底层 SCTP 端口协商。该实现随 2013 年 4 月的破坏性变更被取代,仅作历史参考。
四、视频分享类 Demo:从零信令到 Socket.io / WebSocket 信令
README 索引表中的四类视频分享实验,展示了 WebRTC 信令层的不同演进形态。
4.1 简单视频聊天(client-side.html):纯本地 PeerConnection
client-side.html 是"Simple video chat"的源码,它演示了在同一页面内模拟两个 peer 的最简视频通话。代码核心在 offererPeer / answererPeer 两个函数:
function offererPeer(video_stream) {
offerer = new RTCPeerConnection(window.iceServers);
offerer.addStream(video_stream);
offerer.onaddstream = function (event) {
offererToAnswerer.src = URL.createObjectURL(event.stream);
offererToAnswerer.play();
};
offerer.onicecandidate = function (event) {
if (!event || !event.candidate) return;
answerer.addIceCandidate(event.candidate);
};
offerer.createOffer(function (offer) {
offerer.setLocalDescription(offer);
answererPeer(offer, video_stream);
}, function() {}, mediaConstraints);
}
页面顶部通过兼容层统一了当时各浏览器的 API 前缀:
window.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
navigator.getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
window.URL = window.webkitURL || window.URL;
ICE 服务器使用 stun:23.21.150.121,mediaConstraints 设置 OfferToReceiveAudio/Video: true 以支持双向音视频。这个 Demo 的定位是让初学者看懂"两个 RTCPeerConnection 对象在同页面内完成完整握手"的流程,页面同样标注了"published on 2013 / out-dated"。
4.2 Socket.io 信令(client-side-socket-io.html):PubNub 上的 Socket.io
client-side-socket-io.html 是"Sharing video - using socket.io for signaling"的源码,它把信令(SDP 与 ICE candidate 的交换)搬到 Socket.io 通道上,页面标题写明这是 "Simple client-side WebRTC Experiment using Socket.io over PubNub"。
代码结构揭示了几点关键设计:
- 唯一频道标识:通过
uniqueToken()生成随机 UUID 作为频道名,避免不同会话互相串扰:
function uniqueToken() {
var s4 = function () {
return Math.floor(Math.random() * 0x10000).toString(16);
};
return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4();
}
var uniqueChannel = uniqueToken();
- SDP 分片传输:由于早期 PubNub Socket.io 通道对单条消息长度有限制,代码把 JSON 序列化后的 SDP 切成三部分(
firstPart/secondPart/thirdPart)分开发送,接收方凑齐三片后再JSON.parse重组:
function sendsdp(sdp, by) {
sdp = JSON.stringify(sdp);
var part = parseInt(sdp.length / 3);
var firstPart = sdp.slice(0, part),
secondPart = sdp.slice(part, sdp.length - 1),
thirdPart = '';
if (sdp.length > part + part) {
secondPart = sdp.slice(part, part + part);
thirdPart = sdp.slice(part + part, sdp.length);
}
socket.send({ by: by, firstPart: firstPart });
socket.send({ by: by, secondPart: secondPart });
socket.send({ by: by, thirdPart: thirdPart });
}
-
Peer 抽象:该页引入仓库中的 RTCPeerConnection-v1.4.js(页面通过
/lib/RTCPeerConnection-v1.4.js引用),用onOfferSDP/onAnswerSDP/onICE/onRemoteStream回调 +attachStream/offerSDP配置对象的方式封装了 offerer/answerer 两侧逻辑,比原生 API 更易用。ICE candidate 通过JSON.stringify序列化后经 socket 转发,接收端JSON.parse后addICE。 -
协商流程:socket 的
connect事件触发create1stPeer()(offerer);offer SDP 到达后延迟 400ms 执行create2ndPeer()(answerer),answer SDP 再回传完成握手。
4.3 WebSocket 信令与纯音频流
- client-side-websocket.html 是"Sharing video - using WebSockets for signaling"的源码,结构上与 Socket.io 版本一一对应,只是把信令通道从 Socket.io 换成原生 WebSocket,同样具备 SDP 分片、ICE 转发等逻辑。
- audio-only-streaming.html 是"Audio Only Streaming"的源码,展示只分享音频不分享视频的流配置,是音频会议类应用的起点。
- MediaStreamTrack.getSources.html 对应 "MediaStreamTrack.getSources" 实验,用于枚举当前机器的摄像头、麦克风等媒体输入源,是设备选择类 UI 的基础。
五、进阶实验:屏幕共享、多流附加与流切换(重协商)
README 索引表把两个进阶实验放在了最前面,它们的源码也代表了更现代的 WebRTC 用法(Promise API、addTrack、getDisplayMedia)。
5.1 单条 PeerConnection 同时共享屏幕与音视频
screen-and-video-from-single-peer.html 是"Share screen and audio/video from single peer connection!"的源码,核心思想是在一条 PeerConnection 上附加两个流(多流附加,multi-streams attachment):
// attaching audio/video stream
if(video_stream) {
offerer.addTrack(video_stream.getTracks()[0], video_stream);
}
// attaching screen capturing stream
if(screen_stream) {
offerer.addTrack(screen_stream.getTracks()[0], screen_stream);
}
接收端通过 ontrack 事件区分两条流——由于 addTrack 的附加顺序固定,第一次触发 ontrack 拿到的是音视频流(绑到 video-stream),第二次拿到的是屏幕流(绑到 screen-stream):
var gotFirstMediaStream = false;
answerer.ontrack = function(event) {
event.stream = event.streams[0];
if (!gotFirstMediaStream) {
gotFirstMediaStream = true;
videoStream.srcObject = event.stream;
videoStream.play();
} else {
screenStream.srcObject = event.stream;
screenStream.play();
}
};
这个 Demo 使用了更现代的 Promise API(offerer.createOffer(mediaConstraints).then(...)、setRemoteDescription().then(...)),并在页面底部列出了要点:单向流(peer1 → peer2)、ontrack 触发两次、多流附加演示,同时说明同样的能力也可以通过"重协商(renegotiation)"流程实现,该能力由 RTCMultiConnection v1.3/v1.4 支持(对应仓库 RTCMultiConnection 模块)。
屏幕流的获取也展示了兼容策略:优先 navigator.getDisplayMedia,回退到 navigator.mediaDevices.getDisplayMedia,最后才用 getScreenId + getUserMedia 的屏幕捕获扩展方案。页面同时注明:Chrome ≤ 71 需要安装 Screen-Capturing 扩展(仓库中的 Chrome-Extensions/Screen-Capturing.js 即为此场景设计)。
5.2 运行时流切换(Renegotiation)
switch-streams.html 是"Switch streams from screen-sharing to audio+video. (Renegotiation)"的源码,演示在不重建 PeerConnection 的前提下,把已建立的连接从屏幕共享流切换为摄像头音视频流。
核心逻辑在 offererPeer/answererPeer 中,通过 switching 标志控制:
function offererPeer(e) {
if (!e.switching) offerer = new RTCPeerConnection(window.iceServers);
if (old_stream) offerer.removeStream(old_stream);
offerer.addStream(e.stream);
...
}
function answererPeer(offer, stream, switching) {
if (!switching) answerer = new RTCPeerConnection(window.iceServers);
if (old_stream) answerer.removeStream(old_stream);
answerer.addStream(stream);
...
}
页面上的"Switch Stream to Audio+Video"按钮触发切换流程:
document.getElementById('switch-stream').onclick = function() {
this.disabled = true;
if(old_stream) old_stream.stop();
getUserMedia(function(stream) {
console.log('audio+video stream', stream);
offererPeer({
stream: stream,
switching: true
});
});
};
关键步骤可归纳为三步:
- 停止旧流:
old_stream.stop()停止屏幕共享流(代码还提供了MediaStream.prototype.getVideoTracks/getAudioTracks/stop的兼容 polyfill); - 移除旧流、附加新流:
removeStream(old_stream)后addStream(新流),并复用原有 RTCPeerConnection 对象(switching为 true 时不重新 new); - 重新协商:再次
createOffer/setLocalDescription→ answerersetRemoteDescription/createAnswer→ offerersetRemoteDescription,完成新一轮 SDP 交换。注意switching为 true 时跳过 ICE candidate 的二次交换(if (e.switching) return;),因为底层传输连接仍在。
这个实验揭示了重协商(Renegotiation)的核心价值:媒体流可以在连接生命周期内动态增删,而无需重建传输层,是 WebRTC 会议中"先共享屏幕再切换到摄像头"类功能的教科书级示例。
5.3 远端流录制
demos/remote-stream-recording.html 属于"remote-stream-recording"实验,与仓库中的 MediaStreamRecorder 模块配合,演示把远端收到的媒体流录制成本地文件——这是 WebRTC 应用"会议录制"能力的最简原型,仓库中更完整的实现可见 Record-Entire-Meeting 与 ffmpeg 目录。
六、浏览器支持与许可
README 给出的浏览器支持情况如下:
| 浏览器 | 支持渠道 |
|---|---|
| Firefox | Stable / Aurora / Nightly |
| Google Chrome | Stable / Canary / Beta / Dev |
| Internet Explorer / IE | 需 Chrome Frame 插件 |
需要说明的是:这张表格反映的是 2013 年前后(实验发布当时)的浏览器生态。IE 通过 Chrome Frame 插件运行、Firefox 的 Aurora/Nightly 渠道、Chrome 的 Canary/Beta/Dev 渠道如今都已成为历史。当年实验依赖的 webkitRTCPeerConnection、mozGetUserMedia 等带前缀 API 也已被标准化 API 取代。当前环境下运行这些实验,建议使用最新版 Chrome/Firefox 并改用标准 RTCPeerConnection 与 Promise API;现代实现参考可对照本文第五节的 screen-and-video-from-single-peer.html,它已采用新式写法。
许可方面,README 声明:WebRTC Experiments & Demos 以 MIT 许可证 发布,版权归原作者 Muaz Khan(2013)。仓库根目录的 LICENSE 文件即该许可证全文,意味着你可以自由复制、修改和再分发这些实验代码,只需保留版权声明。
七、从 Demo 到生产:复用路径与注意事项
综合原文档"即插即用"的定位和上述源码分析,如果你想把这些实验能力落地到自己的项目,可遵循以下路径:
- 数据通道(文本/文件传输):参考 client-side-datachannel.html 的双端收发结构,用标准
createDataChannel+ondatachannel重写;文件传输的成熟封装可直接使用仓库的 FileBufferReader 模块。 - 音视频通话:从 client-side.html 的单页握手理解流程后,把信令替换为仓库 server.js 提供的 Socket.io 信令服务,或多参考 socketio-over-nodejs 目录的完整实现。
- 屏幕共享与流切换:直接对照 switch-streams.html 的重协商模式(
removeStream+addStream+ 重新createOffer),并用getDisplayMedia替代已过时的扩展方案;桌面端扩展参考 Chrome-Extensions/Screen-Capturing.js。 - 学习建议:demos 目录刻意保持了"单文件、零依赖"的风格,是最适合通读源码的入门素材;需要生产级功能时,再向仓库中的 RTCMultiConnection、RecordRTC 等成熟库迁移。
最后提醒:demos 目录所有实验页面均在页面顶部标注了 "out-dated (published on 2013)" 的明确警示,本指南中的早期 API 代码请以理解原理为主、以文中标明的现代等价实现为准进行改写。
更多推荐
所有评论(0)