在React中使用WebRTC实现实时视频播放的完整流程

 更新时间:2026年06月18日 09:52:47   作者:Cxiaomu  
本文介绍了在React中使用WebRTC实现实时视频播放的核心流程,包括连接建立、SDP协商、视频流接收与渲染等关键步骤,文章通过代码示例讲解的非常详细,需要的朋友可以参考下

一、简介

React 接入实时视频,最核心的问题在于:

前端怎样在浏览器里拿到一段持续到来的媒体流,并稳定地显示在页面上。

如果用 WebRTC 来做,这条链路通常会拆成几步:

  • React 页面触发连接
  • 浏览器创建 RTCPeerConnection
  • 前端发起 Offer
  • 服务端返回 Answer
  • 浏览器收到远端视频流
  • MediaStream 绑定到 <video>

本文章的重点在于通过几段真实代码示例,把 WebRTC 在 React 里如何工作的描述清楚。

文中会借几类代码片段来解释概念:

  • 页面入口
  • 视频容器
  • 播放组件
  • WebRTC 封装
  • 会话管理

二、什么是WebRTC

WebRTC 是浏览器原生提供的实时音视频通信能力。

它的价值在于:

  • 浏览器本身就能建立实时媒体连接
  • 前端不用额外安装播放器
  • 收到流之后可以直接交给 <video> 播放

所以当前端接入 WebRTC,关注点通常不是“怎么解码视频”,而是:

  • 怎么建立连接
  • 怎么完成协商
  • 怎么接收流
  • 怎么在 React 生命周期里管理这条连接

三、整体架构

把 React 接入 WebRTC 拆开看,通常会分成四层:

  1. 页面层
  • 决定什么时候发起连接
  • 决定展示哪一路视频
  1. 状态层
  • 保存当前连接状态
  • 保存当前选中的视频流
  1. WebRTC 层
  • 创建 RTCPeerConnection
  • 完成 Offer / Answer 协商
  • 接收远端轨道
  1. 渲染层
  • MediaStream 绑定给 <video>
  • 处理播放、暂停、静音和销毁

如果把 WebRTC 放进 React 页面里理解,通常会落成下面几类角色:

  • 页面入口 VideoPage -> VideoLayout
  • 页面主体 VideoStage -> MatrixView / SingleView
  • 状态管理 videoStore
  • 会话复用 liveSessionPool
  • WebRTC 客户端 webrtcClient
  • API 请求 videoApi

可以先把这条链路理解成:

这里需要注意:

  • 状态接口会返回 rtsp_source 字段,前端会读取它
  • 但服务端内部到底如何把 RTSP 变成 WebRTC,不在本次范围内

所以更适合的理解方式是:

React 页面通过 WebRTC 从视频服务拉流播放,状态接口会补充媒体来源和运行状态。

四、WebRTC核心流程

React 接入 WebRTC,核心还是那条经典链路:

创建连接 -> 创建 Offer -> SDP 协商 -> 设置 Answer -> 收流 -> 播放

1. 创建 RTCPeerConnection

连接初始化在 createCameraWebRtcSession 里完成:

const pc = new RTCPeerConnection({ iceServers: options.iceServers ?? [] });
pc.oniceconnectionstatechange = () => {
  console.info(
    `[webrtc] camera=${options.cameraId} ice=${pc.iceConnectionState}`,
  );
};
pc.addTransceiver("video", { direction: "recvonly" });

这一段代码,本质上是在处理 WebRTC 连接的起点:

  • 创建 RTCPeerConnection
  • 配置 iceServers
  • 声明当前连接只接收视频

这里用了 addTransceiver("video", { direction: "recvonly" }),说明前端的角色是“播放器”,不是“推流端”。

浏览器在这条链路里是接收端。

2. 创建 Offer

接下来是 WebRTC 里最关键的第一轮协商:

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

可以把这两句理解成:

  • createOffer():告诉浏览器“把当前这条连接需要协商的内容生成出来”
  • setLocalDescription():把这份协商信息登记为本地描述

从概念上说,这一步就是浏览器先把“我能怎么连”告诉对端。

3. SDP 协商

Offer 生成后,就进入 SDP 协商。

const answer = await startWebRtc(options.cameraId, {
  connection_id: connectionId,
  sdp: offer.sdp ?? "",
  type: "offer",
});

startWebRtc 的请求封装是一个普通的 API 方法:

export async function startWebRtc(
  cameraId: string,
  payload: CameraWebRtcOfferPayload,
): Promise<CameraWebRtcAnswer> {
  return requestTsVideo<CameraWebRtcAnswer>(`/${cameraId}/webrtc`, {
    method: "POST",
    body: JSON.stringify(payload),
  });
}

其实是:

  • 用 HTTP POST /webrtc 发送 Offer
  • 由服务端返回 Answer

也就是说,WebRTC 本身负责媒体连接,信令只是“把 Offer 和 Answer 交换出去”。这里选用的是 HTTP,而不是 WebSocket。

4. 获取 Answer

Offer 和 Answer 的结构在类型定义里写得很清楚:

export interface CameraWebRtcOfferPayload {
  connection_id: string;
  sdp: string;
  type: "offer";
}

export interface CameraWebRtcAnswer {
  connection_id: string;
  sdp: string;
  type: "answer";
}

对于理解 WebRTC 来说,这里最重要的是:

  • Offer 里带 connection_id
  • Offer 里带 sdp
  • type 明确是 "offer"
  • 服务端返回的就是同结构的 Answer

5. 建立连接

拿到 Answer 后,浏览器才真正知道“对端接受了什么协商结果”:

await pc.setRemoteDescription(normalizeAnswer(answer));

normalizeAnswer 的作用也很直接:

function normalizeAnswer(answer: CameraWebRtcAnswer): RTCSessionDescriptionInit {
  return {
    type: answer.type,
    sdp: answer.sdp,
  };
}

这一部分代码对应的,就是 WebRTC 协商闭环的完成。

6. 接收视频流

真正进入“播放视频”阶段,是从 pc.ontrack 开始的:

pc.ontrack = (event) => {
  const stream =
    event.streams[0] ??
    (event.track ? new MediaStream([event.track]) : null);
  if (stream) {
    options.onStream(stream);
  }
};

这就是理解 WebRTC 最关键的一条分界线:

  • 协商之前,前端还只是在建连接
  • ontrack 触发之后,前端才真正拿到了可播放的媒体流

这里的处理方式很实用:

  • 优先取 event.streams[0]
  • 如果没有,就用 event.track 手动包装成 MediaStream

7. 渲染 Video

拿到流之后,还差浏览器播放链路里的最后一步:把流交给 <video>

先看会话池怎么把流往外传:

const session = await createCameraWebRtcSession({
  cameraId: entry.cameraId,
  iceServers: getTsIceServers(),
  onStream: (stream) => {
    entry.stream = stream;
    clearStreamWaitTimer(entry);
    notify(entry);
  },
});

然后组件侧订阅这条流:

const unsubscribe = subscribeSharedCameraStream(
  runtimeCameraId,
  (snapshot: SharedCameraStreamSnapshot) => {
    setStream(snapshot.stream);
    if (snapshot.status === "idle" && !snapshot.connectionId) {
      clearWebRtcSessionState(runtimeCameraId);
      return;
    }
    setWebRtcSessionState(runtimeCameraId, {
      connectionId: snapshot.connectionId,
      status: snapshot.status,
    });
  },
);

最后由 CameraVideoPlayer 绑定给 <video>

if (stream) {
  video.muted = muted;
  if (video.srcObject !== stream) {
    video.srcObject = stream;
    video.removeAttribute("src");
  }
}

如果把这一整段 WebRTC 过程压缩成一行,就是:

RTCPeerConnection
→ createOffer()
→ setLocalDescription()
→ POST /webrtc
→ answer
→ setRemoteDescription()
→ ontrack
→ MediaStream
→ video.srcObject = stream

五、React里怎么落地 WebRTC

WebRTC 解决的是“连起来”,React 解决的是“把连接接进页面生命周期里”。

页面组件

页面入口非常轻:

export default function VideoPage() {
  return <VideoLayout />;
}

真正负责初始化的是 VideoLayout

useEffect(() => {
  if (!initialized) {
    void fetchData();
    return;
  }
  resumeLiveSessions();
}, [fetchData, initialized, resumeLiveSessions]);

useEffect(() => () => releaseAllPrewarmSessions(), []);

React 接实时连接时的处理基本链路:

  • 首次进入页面时初始化数据
  • 页面恢复时恢复会话
  • 页面卸载时释放资源

Hook

这里没有单独抽出一个 useWebRtc Hook。

更贴近 React 思维的做法,是把“订阅流”封装在离视频最近的组件里。

这个内部 Hook 做了四件事:

  • 订阅共享流
  • 把流存到组件状态
  • 把连接状态同步到全局 store
  • 在组件卸载时取消订阅

这类设计的价值在于:视频流本身就和组件是否挂载强相关。

Service层

从 React 接 WebRTC 的角度看,最容易讲清楚的拆法是两层:

  1. API Service
  • 一个独立的 API 封装层

负责:

  • startWebRtc()
  • closeWebRtc()
  • fetchCameraStatus()
  • fetchCameraVideos()
  1. WebRTC Service
  • webrtcClient
  • liveSessionPool

负责:

  • 创建 RTCPeerConnection
  • 完成 Offer / Answer 协商
  • 把流分发给页面
  • 管理共享连接、重试和销毁

这样拆,不是为了“结构漂亮”,而是为了让两类问题分开:

  • API 层只关心请求
  • WebRTC 层只关心连接
  • React 组件只关心显示

视频组件

视频部分再往下看,也可以拆成两层:

  1. VideoTileCard
  • 判断当前走实时流还是回放地址
  • 维护与会话池的订阅关系
  • 把视频数据交给播放器
  1. CameraVideoPlayer
  • 直接操作 <video>
  • 绑定 srcObject
  • 控制 playpausemuted
  • 回传播放进度

这种拆法对应的其实是 React 里很经典的职责分工:

  • 上层组件负责业务状态
  • 下层组件负责 DOM 和媒体元素

生命周期

把 React 生命周期和 WebRTC 生命周期一一对上,整条链路就会很好理解:

  1. VideoLayout 触发 fetchData()
  2. fetchData() 会预热会话,调用 prewarmCameraStreams()
  3. VideoTileCard 挂载后开始订阅共享流
  4. 收到 MediaStream 后,CameraVideoPlayer 绑定到 <video>
  5. 组件卸载后取消订阅;没有消费者时,会话池再延迟关闭连接

如果从“页面如何托住一条 WebRTC 连接”这个角度看,关系可以画成这样:

六、视频显示原理

如果只挑一句最能代表“浏览器开始播放 WebRTC 视频”的代码,就是:

video.srcObject = stream;

这句代码出现在播放器组件里。

为什么这样就能播放?

因为:

  • stream 是浏览器已经收到的 MediaStream
  • <video> 支持直接把 MediaStream 作为数据源
  • 一旦 srcObject 指向它,浏览器就会开始解码并渲染轨道内容

这个组件还补了几层细节处理:

  • 同步 muted
  • loadedmetadatacanplayplaying 时尝试 play()
  • 每 800ms 检查一次,避免视频元素停住不播

对应逻辑也写在播放器组件里。

这段代码顺手也说明了一个很实用的 React 经验:

  • 声明式组件管理状态
  • 命令式操作媒体 DOM

七、连接销毁与资源释放

使用 WebRTC 时很容易只盯着建连,但真正放进 React 页面里,释放资源同样重要。

这里可以把销毁路径分成三层。

1. 组件取消订阅

return () => {
  unsubscribe();
  setStream(null);
  if (getSharedCameraConsumerCount(runtimeCameraId) === 0) {
    clearWebRtcSessionState(runtimeCameraId);
  }
};

这一层对应的是最直接的 React 生命周期:

  • 组件卸载
  • 不再关心这条流
  • 先解除订阅

2. 会话池延迟关闭

function scheduleClose(entry: SharedCameraStreamEntry) {
  if (entry.closeTimer || entry.consumers > 0 || entry.sessionPromise) return;
  entry.closeTimer = setTimeout(() => {
    entry.closeTimer = null;
    if (entry.consumers === 0 && !entry.sessionPromise) {
      void closeSharedSession(entry);
    }
  }, SHARED_SESSION_RELEASE_DELAY_MS);
}

这一层对应的是 WebRTC 连接管理:

  • 不是一卸载就立刻关连接
  • 而是先看还有没有别的消费者
  • 没有的话再延迟回收

3. 真正关闭 PeerConnection 并通知服务端

close: async () => {
  pc.close();
  await closeWebRtc(options.cameraId, connectionId).catch(() => undefined);
},

这一步在 WebRTC 语义上做了两件事:

  • 浏览器本地 pc.close()
  • 请求 /webrtc/close 通知服务端清理连接

接口封装在 API 层:

return requestTsVideo<{ ok: boolean }>(`/${cameraId}/webrtc/close`, {
  method: "POST",
  body: JSON.stringify({ connection_id: connectionId }),
});

关闭代理路由也做了单独处理。
“浏览器侧关闭”和“服务端侧关闭”:

  • pc.close()
  • /webrtc/close

八、踩坑总结

真正让 WebRTC 难用的,通常不是 API 会不会写,而是链路跑起来以后为什么“像是连上了,但就是没画面”。

下面这几段代码,正好可以拿来解释 WebRTC 落地时的几个典型坑位。

1. 协商成功,但流迟迟不到

if (entry.stream || entry.consumers === 0) return;
if (entry.status !== "connected" && entry.status !== "connecting") return;
void restartSharedSession(entry);

这段逻辑解释的是一个非常典型的问题:

  • WebRTC 协商完成,不代表媒体流一定马上可用
  • 如果长时间收不到流,需要主动重建连接

2. 连接失败后的重试

const SHARED_SESSION_RETRY_DELAYS_MS = [800, 1600, 3200];

WebRTC 连接失败后,前端通常不能只报错,还要有节奏地重试。

放在 React 里看,这类重试逻辑最好收口在连接层,而不是散落在按钮点击和组件 effect 里。

3. 流到了,但视频元素没播起来

if (!paused && video.paused && video.srcObject) {
  tryPlay();
}

这类问题在浏览器媒体播放里非常常见:

  • 流已经拿到了
  • <video> 也已经绑定了
  • 但媒体元素因为某些状态没有真正开始播放

所以组件里加一个轻量重试是很实用的。

4. 状态接口和真实媒体状态不完全同步

  • 状态接口可能滞后于 WebRTC
  • 即使状态还没准备好,前端仍允许继续尝试拉流

这也是实时媒体页面里很典型的情况:

  • 业务状态和媒体状态不是完全同一拍
  • UI 不能简单把状态接口当成唯一真相

九、完整链路总结

先看一张简化时序图。

如果压缩成一句话,React 接入 WebRTC 的主线就是:

页面触发连接 -> RTCPeerConnection -> createOffer -> /webrtc -> answer -> ontrack -> video.srcObject = stream

如果再把页面管理、状态管理和识别通道也加进来,React 页面里的完整链路可以整理成:

页面初始化
↓
拉取摄像头与状态
↓
预热 WebRTC 会话
↓
创建 RTCPeerConnection
↓
createOffer()
↓
POST /webrtc
↓
返回 Answer
↓
setRemoteDescription()
↓
ontrack 收到 MediaStream
↓
video.srcObject = stream
↓
页面显示实时视频
↓
并行维护状态接口与 WebSocket 识别结果

这里要明确区分两条通道:

  • WebRTC:负责实时视频
  • WebSocket:负责识别结果推送,不负责 WebRTC 信令

从概念上说,这里并行存在两条通道:

  • WebRTC 连接实现
  • WebSocket 识别通道实现

十、信令、状态、WebSocket分别在做什么

/webrtc

这一部分代表的是 WebRTC 信令交换。

它发生在:

  • createOffer()setLocalDescription() 之后

前端发出去的核心内容是:

  • connection_id
  • sdp
  • type: "offer"

服务端回来的核心内容是:

  • connection_id
  • sdp
  • type: "answer"

它和 WebRTC 的关系是:

  • 负责交换 Offer / Answer
  • 不承载媒体流本身

/webrtc/close

这一部分对应的是连接销毁通知。

它发生在:

  • 会话关闭时

发出去的关键信息是:

  • connection_id

返回值层面:

  • 前端按 { ok: boolean } 解析

它和 WebRTC 的关系是:

  • 告诉服务端“这条连接可以清理了”

/status

这一部分不是信令,而是运行状态补充。

它通常发生在:

  • 初始化之后
  • 切换视频之后
  • 恢复会话之后

返回的信息更偏运行态:

  • camera_id
  • rtsp_source
  • capture_ready
  • stream_ready
  • models

响应结构在类型定义层里有明确声明。

它和 WebRTC 的关系是:

  • 不参与协商
  • 只补充状态和来源信息

/ws

这一部分也不是视频媒体链路,而是旁路消息通道。

它通常发生在:

  • 页面选中视频并建立识别通道时

它和 WebRTC 的关系是:

  • 负责识别结果推送
  • 不负责 Offer / Answer 信令

以上就是在React中使用WebRTC实现实时视频播放的完整流程的详细内容,更多关于React WebRTC实时视频播放的资料请关注脚本之家其它相关文章!

相关文章

  • React Ref Callback使用场景最佳实践详解

    React Ref Callback使用场景最佳实践详解

    这篇文章主要为大家介绍了React Ref Callback使用场景最佳实践详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • react hooks页面实时刷新方式(setInterval)

    react hooks页面实时刷新方式(setInterval)

    这篇文章主要介绍了react hooks页面实时刷新方式(setInterval),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • 基于React实现一个todo打勾效果

    基于React实现一个todo打勾效果

    这篇文章主要为大家详细介绍了如何基于React实现一个todo打勾效果,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-03-03
  • React diff算法的实现示例

    React diff算法的实现示例

    这篇文章主要介绍了React diff算法的实现示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-04-04
  • 详解create-react-app 2.0版本如何启用装饰器语法

    详解create-react-app 2.0版本如何启用装饰器语法

    这篇文章主要介绍了详解create-react-app 2.0版本如何启用装饰器语法,cra2.0时代如何启用装饰器语法呢? 我们依旧采用的是react-app-rewired, 通过劫持webpack cofig对象, 达到修改的目的
    2018-10-10
  • react中如何使用定义数据并监听其值

    react中如何使用定义数据并监听其值

    这篇文章主要介绍了react中如何使用定义数据并监听其值问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • React拆分窗格组件的两种方法

    React拆分窗格组件的两种方法

    这篇文章主要介绍了React拆分窗格组件的两种方法,使用第三方库react-split-pane适用于快速实现拆分窗格功能,并且对功能和样式的要求较为简单的场景,本文结合示例代码介绍的非常详细,需要的朋友可以参考下
    2023-07-07
  • React使用emotion写css代码

    React使用emotion写css代码

    这篇文章主要介绍了React如何使用emotion写css代码,帮助大家更好的理解和学习使用React,感兴趣的朋友可以了解下
    2021-04-04
  • React Fiber源码深入分析

    React Fiber源码深入分析

    Fiber 可以理解为一个执行单元,每次执行完一个执行单元,React Fiber就会检查还剩多少时间,如果没有时间则将控制权让出去,然后由浏览器执行渲染操作,这篇文章主要介绍了React Fiber架构原理剖析,需要的朋友可以参考下
    2022-11-11
  • react-native DatePicker日期选择组件的实现代码

    react-native DatePicker日期选择组件的实现代码

    本篇文章主要介绍了react-native DatePicker日期选择组件的实现代码,具有一定的参考价值,有兴趣的可以了解下
    2017-09-09

最新评论