拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Piik:基于WebRTC的P2P屏幕共享,观众零安装浏览器即看

Piik:基于WebRTC的P2P屏幕共享,观众零安装浏览器即看

这个标题一看就让人想点进去:GitHub 上又出现了一个 Star 数暴涨的开源项目,主打 P2P 屏幕共享,观众端零安装就能看。先给结论:如果你正在做远程演示、在线教学、临时协作这类事情,这个叫 Piik 的开源项目值得你花半小时跑通一遍。它真正厉害的地方不是“又一个屏幕共享工具”,而是把 WebRTC 的 P2P 能力和极低的上手门槛结合在了一起。本文会从它的核心原理讲起,然后带你完成环境搭建、启动服务、发起演示、观众加入观看的完整流程,最后再聊聊它在生产环境里的适用边界和坑。

1. 这篇文章真正要解决的问题

先说说屏幕共享这件事本身。日常工作中,我们最常见的屏幕共享方案大概是这几类:

  • 腾讯会议、Zoom、钉钉这类视频会议软件的共享屏幕功能;
  • TeamViewer、AnyDesk 这类远程控制工具;
  • 把画面录成视频,或者推流到 B 站、YouTube 再让别人看。

这些方案各有各的适用场景,但如果你只是想让别人“看一眼”你电脑上的某个操作,它们都偏重了。要么需要对方安装客户端,要么需要登录账号,要么延迟和画质不可控,要么搭建推流链路太复杂。

Piik 选择的切入点是:发起方在本机启动一个 WebRTC 信令服务,观众通过浏览器直接访问页面就能观看屏幕共享,不需要安装任何软件,不需要注册账号,观看端几乎零门槛。这种体验非常接近“我开了一个直播间,你点开链接就能看”,但背后走的是 P2P 传输,不是传统直播的服务器转播架构。

这篇文章适合谁读?

  • 前端开发者,尤其是对 WebRTC、Canvas 捕获、信令交互感兴趣的;
  • 经常需要做远程演示、培训、技术支持的人;
  • 在局域网或可控公网环境下做轻量级协作的团队。

读完这篇文章,你会理解 Piik 的架构和关键代码路径,能自己把它跑起来,也知道它适合哪些场景、不适合哪些场景。

2. Piik 是什么?为什么它值得关注

Piik 是一个基于 WebRTC 的 P2P 屏幕共享开源项目。按照项目当前公开的信息,它在 GitHub 上已经获得了 733 个 Star,核心卖点有三个:

  • 观众无需安装任何软件,只需要浏览器;
  • 传输走 P2P 点对点通道,不经过中心化媒体服务器;
  • 演示者端提供屏幕捕获、窗口选择、标注等能力。

这里要先解释几个关键概念,避免你在看源码和文档时卡住。

2.1 WebRTC 是什么

WebRTC(Web Real-Time Communication)是一套浏览器原生的实时通信协议和 API。它允许两个浏览器之间直接传输音视频数据,不需要经过中间媒体服务器。

WebRTC 的核心过程是:先通过信令服务器交换 SDP(Session Description Protocol)和 ICE 候选信息,然后两端尝试建立直接连接。连接建立后,音视频数据走的是 P2P 通道,信令服务器就不再参与媒体数据转发了。

Piik 项目里的信令服务就是用 Node.js 写的,浏览器端通过 WebSocket 和它通信,完成“观众找到演示者并交换连接信息”这个动作。

2.2 P2P 和传统直播架构的差别

传统直播架构里,推流端把音视频推到服务器,服务器转码或直接转发给所有观众。观众越多,服务器带宽成本越高,延迟也受服务器位置影响。

P2P 架构里,演示者把屏幕画面直接传给每个观众,服务器只在连接建立过程中做“牵线”工作。观众数量少的时候延迟更低,服务器带宽成本也更低。

但 P2P 也有天然约束:如果观众数量很大,或者观众和演示者之间网络路径复杂,可能会导致上行带宽耗尽或者连接不稳定。这个问题后面在最佳实践部分会再展开。

2.3 为什么说“观众无需安装即可观看”这件事很重要

很多远程协作工具的痛点不在功能,而在安装和登录。

你可以在微信里给人发一个链接,对方点开就能看,这是非常顺畅的体验。Piik 的做法是:演示者启动服务后,把页面 URL 发给观众,观众用 Chrome、Edge 或 Firefox 打开就能看到画面。

从产品体验角度,这大大降低了接收方的使用门槛。从技术角度,这意味着 Piik 必须在浏览器端做完整的 WebRTC 客户端逻辑,同时服务端要足够轻量,让普通开发者能快速部署。

3. 环境准备与前置条件

在跑通 Piik 之前,需要先准备好环境。以下版本信息请以项目实际情况为准,因为你 clone 到的版本可能已经更新,本文重点演示的是通用思路。

3.1 基础环境

依赖用途建议
Node.js运行信令服务建议使用 Node.js 16 及以上版本
npm 或 yarn安装依赖npm 随 Node.js 一起安装
浏览器发起演示和观看Chrome / Edge / Firefox
Git拉取项目代码无特别版本要求

本地环境建议用 macOS 或 Linux,Windows 也可以,但要注意防火墙设置。

3.2 拉取项目代码

git clone https://github.com/YOUR_NAME/piik.git cd piik

注意:这里的仓库地址是示例,请以你实际看到的 GitHub 仓库地址为准。clone 下来后先看一下 README,确认项目结构和启动方式。

3.3 安装依赖

npm install

如果网络环境不佳,可以配置 npm 镜像,但不要用与合规安全无关的方式,建议直接使用官方源或公司内部镜像。

安装完成后,看下package.json,确认启动脚本。通常会有类似这样的结构:

{ "scripts": { "start": "node server.js", "dev": "nodemon server.js" } }

3.4 验证 Node.js 版本

node -v npm -v

如果版本过低,建议先升级 Node.js,因为 WebRTC 相关依赖可能对 Node 版本有要求。

4. 核心流程拆解:一次屏幕共享是怎么发生的

Piik 的完整工作流程可以拆成五个阶段。理解这个流程,比直接看代码更重要,因为后续排错基本都围绕这几个节点展开。

4.1 阶段一:演示者启动 Web 服务

演示者在本地启动 Node.js 服务。这个服务有两个职责:

  • 托管前端页面,让观众能访问;
  • 提供 WebSocket 信令通道,用于演示者和观众之间的连接协商。

启动后,服务会监听某个端口,比如 3000。演示者访问页面时,浏览器会请求摄像头和屏幕捕获权限。

这一步的关键点是让演示者的浏览器和观众不在同一台机器时也能访问,所以需要把端口暴露到局域网或公网。局域网演示时,演示者把本机 IP 和端口发给观众即可。

4.2 阶段二:浏览器捕获屏幕画面

演示者点击“开始共享”后,浏览器调用getDisplayMedia()方法获取屏幕画面。

const stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: 'always' }, audio: true });

这个 API 会弹出浏览器原生授权框,用户可以选择共享整个屏幕、某个窗口或某个浏览器标签页。

获取到的MediaStream对象包含视频轨道和可选的音频轨道。这是后续一切操作的数据源。

4.3 阶段三:信令交换

双方通过 WebSocket 交换 SDP 和 ICE 候选信息。

演示者创建RTCPeerConnection,把屏幕流添加到 PeerConnection 中,然后创建 offer。

const peerConnection = new RTCPeerConnection(config); stream.getTracks().forEach(track => peerConnection.addTrack(track, stream)); const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); socket.send(JSON.stringify({ type: 'offer', sdp: offer }));

观众收到 offer 后,创建自己的RTCPeerConnection,设置远端描述,然后创建 answer 发回去。

这中间还可能交换 ICE candidate。ICE 协商的目的是让两端找到一条可以连通的数据路径,可能走内网,也可能走中继。

4.4 阶段四:P2P 连接建立

当双方都完成 SDP 和 ICE 交换后,浏览器会自动尝试建立 P2P 连接。连接建立后,屏幕共享数据开始直接传输。

这个阶段的核心事件是peerconnection.onconnectionstatechange,当连接状态变为connected时,说明 P2P 通道已打通。

4.5 阶段五:观众渲染画面

观众的浏览器从 P2P 通道收到视频轨道后,把它赋值给一个<video>元素即可。

const remoteVideo = document.getElementById('remote-video'); remoteVideo.srcObject = event.streams[0]; remoteVideo.play();

观众看到的画面就是演示者屏幕的实时画面,延迟一般在几百毫秒以内级别(实际值受网络影响)。

5. 完整示例与代码实现

下面我们用一个最小示例来跑通 Piik 的核心链路。这个示例包含服务端和浏览器端,代码尽量精简,方便你理解。

5.1 服务端:WebSocket 信令服务器

// 文件路径:src/server.js const http = require('http'); const WebSocket = require('ws'); const fs = require('fs'); const path = require('path'); const server = http.createServer((req, res) => { let filePath = req.url === '/' ? '/index.html' : req.url; filePath = path.join(__dirname, '../public', filePath); fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404); res.end('Not Found'); return; } res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(data); }); }); const wss = new WebSocket.Server({ server }); const rooms = new Map(); wss.on('connection', (ws) => { console.log('new websocket connection'); ws.on('message', (message) => { const data = JSON.parse(message.toString()); if (data.type === 'join') { // 观众加入某个房间 ws.room = data.roomId; if (!rooms.has(ws.room)) { rooms.set(ws.room, []); } rooms.get(ws.room).push(ws); console.log(`client joined room ${ws.room}`); } if (data.type === 'offer' || data.type === 'answer' || data.type === 'candidate') { // 转发信令给房间里的其他客户端 const clients = rooms.get(ws.room) || []; clients.forEach(client => { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(message.toString()); } }); } }); ws.on('close', () => { if (ws.room && rooms.has(ws.room)) { rooms.set(ws.room, rooms.get(ws.room).filter(client => client !== ws)); console.log(`client left room ${ws.room}`); } }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Piik demo server running at http://localhost:${PORT}`); });

这个服务端只做两件事:托管静态页面,转发 WebSocket 信令。媒体数据不经过这里,所以服务器带宽压力很小。

注意,当有多个观众同时加入时,上面的转发逻辑会把 offer 广播给房间里的所有其他客户端。如果房间里有三个人,逻辑会变得混乱。实际项目中通常会让信令服务器明确区分“演示者”和“观众”,并维护房间状态。这个简化版本只为演示 WebRTC 信令流程,生产环境建议参考 Piik 源码中的房间管理逻辑。

5.2 浏览器端:演示者逻辑

// 文件路径:public/presenter.js const socket = new WebSocket(`ws://${location.host}`); const startButton = document.getElementById('start-share'); const videoElement = document.getElementById('local-video'); let peerConnection; let roomId = 'demo-room'; async function createPeerConnection(stream) { const config = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }; peerConnection = new RTCPeerConnection(config); stream.getTracks().forEach(track => peerConnection.addTrack(track, stream)); peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.send(JSON.stringify({ type: 'candidate', candidate: event.candidate, roomId })); } }; return peerConnection; } startButton.addEventListener('click', async () => { const stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: 'always' }, audio: true }); videoElement.srcObject = stream; videoElement.play(); await createPeerConnection(stream); socket.onopen = () => { socket.send(JSON.stringify({ type: 'join', roomId })); }; socket.onmessage = async (event) => { const data = JSON.parse(event.data); if (data.type === 'answer' && peerConnection) { await peerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp)); } if (data.type === 'candidate' && peerConnection) { await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); } }; const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); socket.send(JSON.stringify({ type: 'offer', sdp: offer, roomId })); });

这段代码在点击按钮后完成四件事:

  1. 获取屏幕流;
  2. 创建RTCPeerConnection并添加屏幕轨道;
  3. 加入房间并向观众发送 offer;
  4. 处理来自观众的 answer 和 candidate。

5.3 浏览器端:观众逻辑

// 文件路径:public/viewer.js const socket = new WebSocket(`ws://${location.host}`); const viewButton = document.getElementById('view-share'); const remoteVideo = document.getElementById('remote-video'); let peerConnection; let roomId = 'demo-room'; viewButton.addEventListener('click', () => { socket.onopen = () => { socket.send(JSON.stringify({ type: 'join', roomId })); }; }); socket.onmessage = async (event) => { const data = JSON.parse(event.data); if (data.type === 'offer') { const config = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }; peerConnection = new RTCPeerConnection(config); peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.send(JSON.stringify({ type: 'candidate', candidate: event.candidate, roomId })); } }; peerConnection.ontrack = (event) => { remoteVideo.srcObject = event.streams[0]; remoteVideo.play(); }; await peerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp)); const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); socket.send(JSON.stringify({ type: 'answer', sdp: answer, roomId })); } if (data.type === 'candidate' && peerConnection) { await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); } };

观众端不需要授权摄像头或屏幕,只需要创建RTCPeerConnection、响应 offer、渲染远端视频流。

5.4 页面骨架

<!-- 文件路径:public/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Piik 屏幕共享演示</title> </head> <body> <h1>Piik P2P 屏幕共享演示</h1> <button id="start-share">开始共享屏幕</button> <button id="view-share">作为观众观看</button> <video id="local-video" autoplay muted playsinline></video> <video id="remote-video" autoplay playsinline></video> <script src="/presenter.js"></script> <script src="/viewer.js"></script> </body> </html>

这里只是演示页面,实际 Piik 项目会有更完善的事件处理和用户界面。

5.5 运行与验证

node src/server.js

启动后在浏览器打开http://localhost:3000。这时你有两种选择:

  • 在同一个浏览器页签里点“开始共享屏幕”;
  • 打开第二个浏览器窗口或另一台设备访问同一地址,点“作为观众观看”。

如果一切正常,第二个窗口中会实时显示第一个窗口的屏幕画面。在同一个浏览器里测试时,注意 Chrome 对前端权限策略的限制可能不允许在当前页签同时作为演示者和观众,建议用两个不同的浏览器或设备测试。

6. 运行结果与效果验证

上面的示例无法覆盖 Piik 的全部功能,但它能验证 WebRTC 核心链路是否通畅。如果你想验证真实项目的效果,建议先快速跑一遍官方仓库里的示例,再结合下面的验证标准判断是否成功。

6.1 判断成功的标准

检查项预期结果
演示者点击开始共享浏览器弹出屏幕共享授权框
选择共享整个屏幕本地 video 元素显示屏幕画面
观众点击观看remote-video 元素显示演示者屏幕
WebSocket 控制台日志能看到 join、offer、answer、candidate 消息顺序出现
延迟画面延迟在可接受范围内,鼠标移动基本实时

6.2 如果画面卡住或黑屏

第一步先看浏览器控制台:

  • 有没有getDisplayMedia权限错误;
  • 有没有setRemoteDescription或addIceCandidate报错;
  • 有没有 WebSocket 断开连接。

第二步,在演示者端查看 ICE 连接状态:

peerConnection.onconnectionstatechange = () => { console.log('Connection state:', peerConnection.connectionState); };

如果状态一直停留在new或failed,说明两端无法建立 P2P 连接。最常见的原因是双方网络无法直接互通,或者 STUN 服务器不可用。

7. 常见问题与排查思路

问题现象可能原因排查方式解决方案
观众页面打不开服务没有监听正确地址,或防火墙拦截检查服务监听地址;在演示者机器访问 localhost:3000 确认使用 0.0.0.0 监听;关闭防火墙或开放端口
点击共享无反应浏览器不支持 getDisplayMedia,或未走 HTTPS查看控制台报错;检查页面地址是否为 localhost 或 https使用 Chrome/Edge;HTTPS 要求请参考项目文档
WebSocket 连接失败端口被占用或反向代理配置错误查看 server 日志;用curl测试 WebSocket 握手更换端口或检查代理规则
观众看到黑屏但有声音视频轨道没有正确添加到 PeerConnection检查 addTrack 调用;查看 remote 流是否有 video track确认屏幕流的 video track 不为空
画面延迟高上行带宽不足或观众点过多查看网络占用;观察 ICE 连接类型是否为 relay限制观众数量;使用性能更好的上行网络;考虑混合传输
offer 广播给多个观众后画面错乱房间逻辑把所有消息都广播观察服务器转发规则参考 Piik 源码实现演示者/观众身份区分

这里特别提醒:如果你把服务部署在一个需要 HTTPS 的域名下,浏览器可能拒绝非安全上下文中的getDisplayMedia。本地localhost通常会放行,但远程访问时必须配置 HTTPS。

8. 最佳实践与工程建议

如果只是体验 Piik,前面的步骤已经足够。但如果要把类似方案用到实际项目里,下面这些建议会帮你少踩坑。

8.1 明确观众规模和网络边界

Piik 这种纯 P2P 方案最适合的是几十人以内的小规模演示。当观众数量变大时,演示者的上行带宽会成为瓶颈。假设屏幕共享码率在 2Mbps 左右,10 个观众就是 20Mbps 上行,家用宽带的通常上行带宽撑不住这种压力。

在实际业务里,更稳妥的做法是:

  • 观众少、互动频繁:使用 P2P;
  • 观众多、单向直播:使用 SFU(Selective Forwarding Unit)服务器转播;
  • 两者结合:P2P 为主,自动降级到服务器转播。

所以,看到 Piik 说“P2P、零安装”不要直接套到大规模直播场景,这个判断对后续架构选型很重要。

8.2 信令服务必须做身份验证和房间管理

WebSocket 信令服务如果让任何人都能 join 和转发 offer,很容易被恶意刷流量或干扰。生产环境至少要做到:

  • 演示者创建房间时生成随机 token;
  • 观众凭 token 进入房间;
  • 信令消息带上校验信息;
  • 服务端对房间人数做上限控制。

8.3 做好 ICE 兜底:部署 TURN 服务器

WebRTC 理论上能 P2P,但在复杂网络下经常需要 TURN 中继。如果观众的 NAT 太严格或者企业网络限制 UDP,P2P 会失败。

建议在正式环境部署 coturn 等 TURN 服务:

# coturn 配置片段,实际部署时按需调整 listening-port=3478 tls-listening-port=5349 realm=example.com server-name=example.com fingerprint lt-cred-mech user=recorder:your-password

然后把 TURN 地址加入 ICE 配置:

const config = { iceServers: [ { urls: 'stun:stun.example.com:3478' }, { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'password' } ] };

没有 TURN,你在企业内网演示时大概率会遇到连接失败。这是 WebRTC 项目最常见的坑。

8.4 注意安全与隐私

屏幕共享本身有很强的隐私属性。只要演示者打开共享,观众就能看到屏幕全部内容。生产项目应该提供:

  • 共享前二次确认弹窗;
  • 显示“正在共享”的醒目提示;
  • 一键停止共享的按钮;
  • 服务端日志记录谁在什么时间加入了哪个房间。

另外,不要在设计上让观众能够获取演示者的设备信息、IP 地址或其他敏感数据。WebRTC 的 ICE 协商本身会暴露一些网络信息,如果业务上有隐私要求,需要做额外处理。

8.5 前端稳定性处理

屏幕共享期间,演示者可能切换窗口、锁屏、断网,这些都会导致视频轨道中断。建议监听:

  • track.onended:用户停止共享时触发;
  • navigator.connection.onchange:网络切换时触发;
  • peerConnection.onconnectionstatechange:连接断开的统一入口。

在这些事件里给观众端一个明确的提示,比如“演示者已停止共享”“连接断开,正在重连”等,避免观众看到黑屏后不知所措。

9. 为什么说 Piik 在这个赛道“暂无敌手”

回到标题里那个判断:Piik 在这个赛道是不是真的暂无敌手?

我的看法是,这个结论需要限定条件。从“P2P + 零安装 + 开源 + 上手快”这个组合来看,Piik 确实把屏幕共享的门槛降到了一个很低的位置。它没有传统会议软件的账号体系,没有安装包分发成本,没有中心化服务器的带宽费用,非常适合临时演示和轻量协作。

但“无敌手”更多是对体验和定位的判断,而不是对技术的绝对评价。专业直播场景、大规模在线课堂、需要录制回放的场景,仍然需要更重型的产品。Piik 的价值不在于替代腾讯会议或 Zoom,而在于给开发者提供了一个足够简单、可二次开发的 P2P 屏幕共享范式。

如果你正好需要做一个轻量级的远程演示工具,或者想在项目里体验 WebRTC 的完整链路,从 Piik 下手是个不错的起点。跑通它之后,再去读源码里的房间管理、信号处理和前端交互设计,你会对 WebRTC 工程化有更具体的认识。

最后提醒一句:如果只是临时给同事演示一下屏幕,直接用系统自带的会议软件可能更省事。但如果你在意的是“观众无需安装、零门槛访问、代码完全可控”,那 Piik 值得你花点时间折腾一遍。

返回列表