10、屏幕共享接收端:创建远程视频元素、处理track事件、自动播放策略

好,咱们接着聊。上一章我们把发送端的逻辑理清楚了,这一章轮到接收端了。

说实话,接收端看起来比发送端简单,但坑一点都不少。我最早做屏幕共享时,就栽在自动播放策略上——明明代码写对了,画面就是出不来。后来才发现,是浏览器把播放给拦截了。

这一章,我们就来搞定三件事:创建远程视频元素、处理 track 事件、应对自动播放策略。这三步走完,接收端就能正常显示共享画面了。

10.1 创建远程视频元素

接收端要显示画面,得先有一个 <video> 元素。这个元素可以是 HTML 里写死的,也可以是 JS 动态创建的。

我个人习惯用动态创建。为什么呢?因为屏幕共享可能有多个接收端,或者一个页面里同时有摄像头视频和屏幕共享视频。动态创建更灵活,也好管理。

来看代码:

// 创建视频元素
const remoteVideo = document.createElement('video');
remoteVideo.id = 'remote-screen-' + Date.now();
remoteVideo.autoplay = true;
remoteVideo.playsInline = true;
remoteVideo.style.width = '100%';
remoteVideo.style.maxWidth = '1280px';
remoteVideo.style.border = '1px solid #ccc';
remoteVideo.style.borderRadius = '8px';

// 挂到 DOM 上
const container = document.getElementById('video-container');
container.appendChild(remoteVideo);

// 返回元素引用,后面要用
return remoteVideo;

这里有几个关键点:

  • autoplay:必须设为 true,否则视频不会自动播放。但注意,这只是告诉浏览器“我想自动播放”,能不能成功,还得看浏览器的自动播放策略。
  • playsInline:iOS Safari 上必须加。不加的话,视频会全屏播放,用户体验很差。
  • 样式:给个边框和圆角,视觉上更友好。宽度用百分比,适配不同屏幕。
小技巧: 如果你需要同时显示多个共享流(比如多个标签页),可以用 id 加上时间戳来区分。这样后续操作特定视频元素时,直接通过 id 查找就行。

10.2 处理 track 事件

视频元素准备好了,接下来就是把远程的媒体流塞进去。

在 WebRTC 中,接收端通过 RTCPeerConnectionontrack 事件来获取远程流。这个事件会在每次收到新的 track 时触发。

我记得第一次做这个时,以为 ontrack 只会触发一次。后来发现,如果发送端中途切换了屏幕共享源(比如从整个屏幕切换到某个窗口),ontrack 会再次触发,而且会带上新的流。

所以,处理逻辑要考虑到流替换的情况。

标准写法如下:

// 假设 pc 是已经建立好的 RTCPeerConnection
// remoteVideo 是上一步创建的视频元素

pc.ontrack = function(event) {
  console.log('收到远程 track,类型:', event.track.kind);

  // 只处理 video track
  if (event.track.kind !== 'video') return;

  // 获取远程流
  const remoteStream = event.streams[0];

  if (remoteStream) {
    // 把流设置到视频元素上
    remoteVideo.srcObject = remoteStream;
    console.log('远程流已绑定到视频元素');
  } else {
    console.warn('没有收到远程流,尝试直接绑定 track');
    // 某些浏览器可能不提供 streams 数组
    // 可以用 MediaStream 手动包装
    const stream = new MediaStream();
    stream.addTrack(event.track);
    remoteVideo.srcObject = stream;
  }
};
重点: event.streams[0] 是大多数浏览器提供的标准方式。但有些旧版本浏览器可能不返回 streams 数组。保险起见,可以加一个 fallback,手动创建 MediaStream 并添加 track。

还有一个细节:track 的 muted 状态。当远程端暂停共享时,track 会变成 muted。你可以监听这个事件,在 UI 上给出提示。

event.track.onmuted = function() {
  console.log('远程共享已暂停');
  // 显示一个“对方已暂停共享”的提示
};

event.track.onunmuted = function() {
  console.log('远程共享已恢复');
  // 隐藏提示
};

这个功能虽然不是必须的,但能提升用户体验。我在做企业协作工具时加了这功能,用户反馈很好——至少他们知道是对方暂停了,而不是自己这边出了问题。

10.3 自动播放策略

好,到了最头疼的部分了。

自动播放策略,说白了就是浏览器为了防止网页自动播放声音打扰用户,制定的一套规则。对于视频元素,规则是这样的:

场景 是否允许自动播放 说明
视频没有音频 track ✅ 允许 屏幕共享通常没有音频,所以一般没问题
视频有音频 track,且用户已与页面交互 ✅ 允许 用户点击过页面后,自动播放就放开了
视频有音频 track,用户未交互 ❌ 禁止 浏览器会暂停播放,需要用户手动触发
静音状态下自动播放 ✅ 允许(部分浏览器) Chrome 允许静音自动播放,Safari 可能不行

屏幕共享的场景下,大多数时候是没有音频的。所以自动播放通常能成功。但如果你在共享屏幕的同时也共享了系统音频(这个后面章节会讲),那就要小心了。

我曾经遇到过一个 case:用户共享了某个标签页,那个标签页里有视频在播放,所以共享流里带了音频。结果接收端死活不自动播放。排查了半天,发现是自动播放策略在作怪。

解决方案有两种:

方案一:静音播放(推荐)

// 创建视频元素时,先静音
remoteVideo.muted = true;
remoteVideo.autoplay = true;

// 等用户交互后,再取消静音
document.addEventListener('click', function() {
  remoteVideo.muted = false;
}, { once: true });

这个方案的好处是:100% 能自动播放。因为静音视频不受自动播放策略限制。等用户点击页面后,再取消静音,体验上完全没问题。

方案二:检测播放状态,手动恢复

// 尝试自动播放
const playPromise = remoteVideo.play();

if (playPromise !== undefined) {
  playPromise.then(() => {
    // 自动播放成功
    console.log('自动播放成功');
  }).catch(error => {
    // 自动播放被阻止
    console.warn('自动播放被阻止,需要用户交互', error);
    // 显示一个“点击播放”的按钮
    showPlayButton();
  });
}

这个方案更精细。它利用了 play() 方法返回的 Promise。如果自动播放被阻止,Promise 会 reject,这时候你再显示一个播放按钮让用户手动点击。

注意: 不要依赖 autoplay 属性一定能生效。它只是一个“请求”,浏览器有权拒绝。一定要做好 fallback 处理。

10.4 完整流程与 SVG 图解

好了,我们把这三步串起来,看看接收端的完整流程:

  1. 创建 RTCPeerConnection,设置 ICE 候选者和 SDP 交换逻辑
  2. 创建 <video> 元素,设置 autoplayplaysInline
  3. 监听 ontrack 事件,将远程流绑定到视频元素
  4. 处理自动播放策略,确保画面能正常显示

下面这张图展示了接收端的核心逻辑:

屏幕共享接收端核心流程 创建视频元素 document.createElement('video') autoplay / playsInline 建立 RTCPeerConnection ICE / SDP 交换 等待远程流到达 处理 ontrack 事件 event.streams[0] 绑定 srcObject 远程流是否有音频? 无音频 → 自动播放成功 autoplay 直接生效 有音频 → 可能被阻止 需要静音或用户交互 画面正常显示 ✅

从图上可以看得很清楚:接收端的核心就是三步走,中间有一个关键分支——有没有音频。这个分支决定了自动播放能不能直接成功。

10.5 避坑指南

最后,分享几个我踩过的坑:

  • 不要忘记 playsInline:iOS 上不加这个属性,视频会强制全屏播放,用户根本看不到共享画面。
  • srcObject 赋值时机:一定要在 ontrack 事件中赋值,不要提前赋值。提前赋值会导致视频元素处于“等待数据”状态,可能触发浏览器的自动播放拦截。
  • 多个 track 的处理:如果接收端同时接收了摄像头和屏幕共享,ontrack 会触发两次。记得用 event.track.kind 区分,分别绑定到不同的视频元素。
  • 页面可见性:如果用户切换到其他标签页,浏览器可能会暂停视频播放。可以监听 visibilitychange 事件,在页面重新可见时调用 remoteVideo.play() 恢复播放。
我的习惯: 在创建视频元素时,我会先把它 muted,等用户点击页面后再取消静音。这样既保证了自动播放成功,又不会在用户没准备好时突然出声。用户体验最好。

嗯,接收端的内容就这些。代码量不大,但细节不少。尤其是自动播放策略,一定要在开发阶段就考虑进去,别等到测试了才发现画面出不来。

下一章我们会聊一聊屏幕共享中的动态切换共享源——比如用户想从共享整个屏幕切换到共享某个窗口,怎么做到无缝切换。这个功能在实际产品中非常常见,也很有挑战性。


公众号:蓝海资料掘金营,微信deep3321