WebRTC-Experiment demos 目录实战指南:即插即用的 WebRTC 实验与 Demo 全解析

【免费下载链接】WebRTC-Experiment WebRTC, WebRTC and WebRTC. Everything here is all about WebRTC!! 【免费下载链接】WebRTC-Experiment 项目地址: https://gitcode.com/gh_mirrors/we/WebRTC-Experiment

导读

本指南基于 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 传入;answerer setRemoteDescription 后 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 路线有三个显著不同点:

  1. 接收端通道获取方式不同:Chrome 的 answerer 也需要显式 createDataChannel;而 Firefox 的 answerer 靠 ondatachannel 事件被动接收 offerer 创建的通道(event.channel)。这正是 SDP 协商语义差异的体现——通道只由 offerer 侧创建,对端通过事件回调获得引用。
  2. binaryType = 'blob':设置二进制数据的接收类型为 Blob,从而支持发送/接收 Blob 与文件。
  3. 必须附带媒体流: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"。

代码结构揭示了几点关键设计:

  1. 唯一频道标识:通过 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();
  1. 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 });
}
  1. 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。

  2. 协商流程: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
        });
    });
};

关键步骤可归纳为三步:

  1. 停止旧流:old_stream.stop() 停止屏幕共享流(代码还提供了 MediaStream.prototype.getVideoTracks/getAudioTracks/stop 的兼容 polyfill);
  2. 移除旧流、附加新流:removeStream(old_stream) 后 addStream(新流),并复用原有 RTCPeerConnection 对象(switching 为 true 时不重新 new);
  3. 重新协商:再次 createOffer/setLocalDescription → answerer setRemoteDescription/createAnswer → offerer setRemoteDescription,完成新一轮 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 给出的浏览器支持情况如下:

浏览器支持渠道
FirefoxStable / Aurora / Nightly
Google ChromeStable / 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 到生产:复用路径与注意事项

综合原文档"即插即用"的定位和上述源码分析,如果你想把这些实验能力落地到自己的项目,可遵循以下路径:

  1. 数据通道(文本/文件传输):参考 client-side-datachannel.html 的双端收发结构,用标准 createDataChannel + ondatachannel 重写;文件传输的成熟封装可直接使用仓库的 FileBufferReader 模块。
  2. 音视频通话:从 client-side.html 的单页握手理解流程后,把信令替换为仓库 server.js 提供的 Socket.io 信令服务,或多参考 socketio-over-nodejs 目录的完整实现。
  3. 屏幕共享与流切换:直接对照 switch-streams.html 的重协商模式(removeStream + addStream + 重新 createOffer),并用 getDisplayMedia 替代已过时的扩展方案;桌面端扩展参考 Chrome-Extensions/Screen-Capturing.js。
  4. 学习建议:demos 目录刻意保持了"单文件、零依赖"的风格,是最适合通读源码的入门素材;需要生产级功能时,再向仓库中的 RTCMultiConnection、RecordRTC 等成熟库迁移。

最后提醒:demos 目录所有实验页面均在页面顶部标注了 "out-dated (published on 2013)" 的明确警示,本指南中的早期 API 代码请以理解原理为主、以文中标明的现代等价实现为准进行改写。

【免费下载链接】WebRTC-Experiment WebRTC, WebRTC and WebRTC. Everything here is all about WebRTC!! 【免费下载链接】WebRTC-Experiment 项目地址: https://gitcode.com/gh_mirrors/we/WebRTC-Experiment

Logo

邀请您加入社区

更多推荐