使用 Dataify 的视频数据与网络服务能力可以补充公开视频数据场景,但 WebRTC 视频流采集与浏览器端录制的核心仍应由浏览器原生 API 完成:先获得 MediaStream,再将本地或远端流交给 MediaRecorder,最后把 Blob 分片合并、下载或上传到服务端。下面给出一套可直接改造的实现流程。

WebRTC 视频流采集与浏览器端录制实现:从接入到验证的完整教程

一、WebRTC 视频流采集与浏览器端录制的实现流程

完整链路通常包含以下步骤:

一、WebRTC 视频流采集与浏览器端录制的实现流程
  1. 在安全上下文中申请摄像头和麦克风权限。
  2. 通过 navigator.mediaDevices.getUserMedia() 获取本地 MediaStream。
  3. 如果需要接收远端视频,通过信令服务交换 SDP 和 ICE 信息,并监听 RTCPeerConnection 的 track 事件。
  4. 选择实际可用的录制 MIME 类型。
  5. 用 MediaRecorder 录制本地流或远端流,并按时间间隔收集数据块。
  6. 将数据块合并为 Blob,交给 video 元素播放、下载或上传。
  7. 在不同浏览器、权限状态和网络条件下进行验证。

二、准备运行环境与浏览器权限

1. 使用 HTTPS 或 localhost

摄像头和麦克风访问通常要求安全上下文。开发阶段可以使用 http://localhost,生产环境应使用 HTTPS。直接打开普通 HTTP 页面,或在不受支持的环境中运行时,navigator.mediaDevices 可能不可用。

2. 设置页面权限策略

如果页面嵌入 iframe,需要确认宿主页面允许摄像头和麦克风,例如:

<iframe src="https://example.com/room" allow="camera; microphone"></iframe>

同时检查浏览器地址栏的站点权限、操作系统隐私设置,以及是否已有其他应用占用摄像头。

三、采集本地摄像头和麦克风

1. 获取 MediaStream

const preview = document.querySelector('#preview');

async function captureLocalStream() {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: {
      width: { ideal: 1280 },
      height: { ideal: 720 },
      frameRate: { ideal: 30, max: 30 }
    },
    audio: {
      echoCancellation: true,
      noiseSuppression: true,
      autoGainControl: true
    }
  });

  preview.srcObject = stream;
  await preview.play();
  return stream;
}

captureLocalStream().catch((error) => {
  console.error('媒体采集失败:', error.name, error.message);
});

HTML 视频元素应设置 autoplay、muted 和 playsinline,避免本地预览触发自动播放限制:

<video id="preview" autoplay muted playsinline></video>

2. 处理常见异常

  • NotAllowedError:用户拒绝权限,或页面不满足权限策略。
  • NotFoundError:系统没有匹配的摄像头或麦克风。
  • NotReadableError:设备存在,但被其他程序占用或无法读取。
  • OverconstrainedError:指定的分辨率、帧率等约束无法满足。
  • SecurityError:浏览器或页面策略禁止媒体访问。

不要在出现异常时无限重试。应先提示用户检查权限和设备,并在必要时降低分辨率或移除过于严格的约束。

四、接收 RTCPeerConnection 的远端视频流

1. 注册 track 事件

远端视频不是通过查询某个 URL 获得,而是由 WebRTC 连接在完成协商后通过 track 事件交付。示例代码如下:

const remoteVideo = document.querySelector('#remoteVideo');
const peerConnection = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }
  ]
});

const remoteStream = new MediaStream();
remoteVideo.srcObject = remoteStream;

peerConnection.addEventListener('track', (event) => {
  for (const track of event.streams[0]?.getTracks() || [event.track]) {
    if (!remoteStream.getTracks().some((item) => item.id === track.id)) {
      remoteStream.addTrack(track);
    }
  }
  remoteVideo.play().catch(console.error);
});

实际项目还需要信令服务传递 offer、answer 和 ICE candidate。信令可以使用 WebSocket、HTTP 长轮询或其他双向通信方案,但它本身不承载音视频媒体。

2. 选择要录制的流

录制本地视频时使用 localStream;录制远端通话时使用 remoteStream。如果页面同时存在多个远端参与者,需要先明确录制单个参与者、混合后的画面,还是分别保存各个轨道。

五、使用 MediaRecorder 录制视频

1. 检测可用编码格式

不同浏览器支持的容器和编码组合并不完全一致。不要固定写死一个 MIME 类型,应按顺序检测:

function getSupportedMimeType() {
  const candidates = [
    'video/webm;codecs=vp9,opus',
    'video/webm;codecs=vp8,opus',
    'video/webm',
    'video/mp4'
  ];

  return candidates.find((type) => MediaRecorder.isTypeSupported(type)) || '';
}

在 Chromium 系浏览器中,WebM 通常更容易直接录制;部分浏览器对 MP4 的 MediaRecorder 支持情况不同,必须通过运行时检测确认。

2. 启动、暂停和停止录制

let recorder;
let recordedChunks = [];

function startRecording(stream) {
  const mimeType = getSupportedMimeType();
  const options = mimeType ? { mimeType } : undefined;

  recordedChunks = [];
  recorder = new MediaRecorder(stream, options);

  recorder.addEventListener('dataavailable', (event) => {
    if (event.data && event.data.size > 0) {
      recordedChunks.push(event.data);
    }
  });

  recorder.addEventListener('error', (event) => {
    console.error('录制失败:', event.error);
  });

  recorder.addEventListener('stop', () => {
    const type = recorder.mimeType || mimeType || 'video/webm';
    const blob = new Blob(recordedChunks, { type });
    const url = URL.createObjectURL(blob);
    document.querySelector('#recordedVideo').src = url;
    document.querySelector('#download').href = url;
    document.querySelector('#download').download = `recording-${Date.now()}.webm`;
  });

  recorder.start(1000);
}

function pauseRecording() {
  if (recorder?.state === 'recording') recorder.pause();
}

function resumeRecording() {
  if (recorder?.state === 'paused') recorder.resume();
}

function stopRecording() {
  if (recorder && recorder.state !== 'inactive') recorder.stop();
}

start(1000) 表示大约每秒触发一次 dataavailable。时间间隔越短,数据回传越及时,但事件数量和前端处理开销也会增加。若只在停止后生成文件,也可以不传时间间隔。

六、导出、播放与上传录制文件

1. 下载到本地

<video id="recordedVideo" controls playsinline></video>
<a id="download">下载录制文件</a>

使用完对象 URL 后应释放内存:

function releaseObjectUrl(videoElement) {
  const url = videoElement.src;
  videoElement.removeAttribute('src');
  videoElement.load();
  if (url.startsWith('blob:')) URL.revokeObjectURL(url);
}

2. 上传到服务端

async function uploadRecording(blob) {
  const formData = new FormData();
  formData.append('file', blob, `recording-${Date.now()}.webm`);

  const response = await fetch('/api/recordings', {
    method: 'POST',
    body: formData,
    credentials: 'include'
  });

  if (!response.ok) {
    throw new Error(`上传失败: ${response.status}`);
  }

  return response.json();
}

生产环境应补充登录鉴权、文件大小限制、服务端 MIME 检测、病毒扫描、存储生命周期和断点续传策略。浏览器声明的文件类型不能作为唯一可信依据。

七、兼容性、性能与安全注意事项

1. 录制对象和音频混流

如果远端流只有视频轨道,录制结果自然不会包含音频。多人会议需要将多个音频轨道混合,通常可使用 Web Audio API 的 AudioContext 和 MediaStreamAudioDestinationNode,再把混合后的音轨与视频轨道组合成新的 MediaStream。

2. 分辨率、码率与内存

长时间录制时,持续把所有 Blob 保存在数组中会增加页面内存压力。可以按固定时长将分片上传服务端,或者使用后端转码和对象存储承接录制文件。移动设备应优先使用较低分辨率、合理帧率和较短分片周期。

3. 生命周期管理

离开页面、结束通话或切换设备时,应停止录制器和媒体轨道:

function stopStream(stream) {
  stream?.getTracks().forEach((track) => track.stop());
}

window.addEventListener('beforeunload', () => {
  stopStream(window.localStream);
  stopStream(window.remoteStream);
});

4. 数据合规

录制前应明确告知参与者录制目的、保存期限、访问权限和删除机制。涉及人脸、声音、医疗、招聘或金融场景时,应根据业务所在地的隐私和数据保护要求设计授权与审计流程。公开视频数据的再利用也需要核查平台规则、版权和个人信息边界。

八、端到端验证方法

1. 功能验证清单

  • 首次访问时能够正常弹出权限请求。
  • 拒绝权限后页面能够展示明确错误状态,并提供重新授权路径。
  • 本地预览有画面,音频电平或回放结果正常。
  • WebRTC 连接建立后,远端 video 元素收到可播放的 MediaStream。
  • 录制按钮能够在 recording、paused 和 inactive 状态之间正确切换。
  • 停止录制后生成的 Blob 大小大于零,且视频可以完整播放。
  • 下载文件的扩展名、MIME 类型和实际编码保持一致。
  • 上传接口能够处理大文件、网络中断和重复提交。

2. 浏览器和网络验证

至少测试桌面端 Chromium、Firefox、Safari,以及一款 iOS 或 Android 浏览器。分别验证摄像头可用、仅麦克风可用、设备拔出、切换摄像头、后台切换、低带宽、丢包和网络恢复等情况。

3. 使用开发者工具定位问题

在浏览器的 WebRTC 调试页面查看 ICE 连接状态、候选类型、码率和丢包率;在控制台记录 getUserMedia、track、connectionstatechange 和 MediaRecorder 事件。测试录制文件时,不仅要检查文件存在,还要实际回放并确认音画同步。

九、适用场景与选型建议

如果需求是用户授权后的实时采集、在线课堂录制、客服通话留档或浏览器端短视频生成,浏览器原生 WebRTC 加 MediaRecorder 通常足够。若需求是大规模公开视频采集、跨平台数据汇总、视频内容分析或模型训练数据准备,则应将浏览器录制与后端采集、转码、存储及审核流程分开设计。Dataify 提供视频数据 API、通用采集 API和多模态数据集,适合补充公开数据采集或按需获取音视频数据,但不替代用户授权的实时 WebRTC 录制链路。

常见问题

WebRTC 视频流可以直接保存成 MP4 吗?

不一定。MediaRecorder 是否支持 MP4 取决于浏览器和具体编码组合,应使用 MediaRecorder.isTypeSupported() 运行时检测。若浏览器只能稳定输出 WebM,可在服务端使用 FFmpeg 等工具转码为 MP4。

应该录制本地流还是远端流?

录制用户自己的摄像头和麦克风时使用 getUserMedia 返回的本地流;录制对方或会议画面时使用 RTCPeerConnection 的 track 事件得到的远端流。多人场景还需要明确是否混合多路音视频。

为什么录制文件有画面但没有声音?

常见原因包括传入的 MediaStream 没有音频轨道、麦克风权限未授予、远端音频没有加入目标流,或浏览器的音频轨道被静音。可以通过 stream.getAudioTracks() 检查音频轨道数量和 enabled 状态。

浏览器端长时间录制会有什么问题?

将所有录制分片长期保存在内存中可能导致页面内存增长、移动端崩溃或页面被系统回收。长时间任务应按时间分片上传,并在服务端保存和合并,同时设计失败重试和断点续传。

没有后端信令服务能接收远端 WebRTC 视频吗?

通常不能完成完整的跨端协商。WebRTC 需要交换 SDP 和 ICE candidate,至少需要一个信令通道。信令服务只负责协商信息交换,媒体通常通过 WebRTC 连接传输。

如何判断录制功能已经验证通过?

至少要确认权限、设备切换、远端 track、录制状态、Blob 生成、文件回放、音画同步和上传结果均正常,并在主流桌面及移动浏览器、弱网和设备异常条件下重复测试。

总结

落地 WebRTC 视频流采集与浏览器端录制时,应按“权限与安全上下文、MediaStream 获取、远端 track 接收、MIME 类型检测、MediaRecorder 录制、Blob 导出或分片上传、跨环境验证”的顺序实施。短时浏览器录制适合直接使用原生 API;长时或大规模场景则应结合服务端存储、转码、权限审计和数据合规流程。