React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React WebRTC实时视频播放

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

作者:Cxiaomu

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

一、简介

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

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

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

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

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

二、什么是WebRTC

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

它的价值在于:

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

三、整体架构

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

  1. 页面层
  1. 状态层
  1. WebRTC 层
  1. 渲染层

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

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

这里需要注意:

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

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 连接的起点:

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

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

2. 创建 Offer

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

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

可以把这两句理解成:

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

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),
  });
}

其实是:

也就是说,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 来说,这里最重要的是:

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 最关键的一条分界线:

这里的处理方式很实用:

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 做了四件事:

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

Service层

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

  1. API Service

负责:

  1. WebRTC Service

负责:

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

视频组件

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

  1. VideoTileCard
  1. CameraVideoPlayer

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

生命周期

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

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

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

六、视频显示原理

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

video.srcObject = stream;

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

为什么这样就能播放?

因为:

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

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

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

七、连接销毁与资源释放

使用 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 语义上做了两件事:

接口封装在 API 层:

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

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

八、踩坑总结

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

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

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

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

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

2. 连接失败后的重试

const SHARED_SESSION_RETRY_DELAYS_MS = [800, 1600, 3200];

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

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

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

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

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

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

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

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

九、完整链路总结

先看一张简化时序图。

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

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

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

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

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

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

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

/webrtc

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

它发生在:

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

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

它和 WebRTC 的关系是:

/webrtc/close

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

它发生在:

发出去的关键信息是:

返回值层面:

它和 WebRTC 的关系是:

/status

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

它通常发生在:

返回的信息更偏运行态:

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

它和 WebRTC 的关系是:

/ws

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

它通常发生在:

它和 WebRTC 的关系是:

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

您可能感兴趣的文章:
阅读全文