使用 WebRTC 和 React 从画布流式传输视频

使用WebRTC,您可以从canvas直播视频。本文提供了一个分步指南,向您展示如何实现。我们使用LiveKit的WebRTC堆栈来构建一个用于发送canvas视频的实时应用程序。查看完整代码

VTuber示例应用Gif

许多人知道WebRTC是支持网页视频聊天的技术。这对于像Google Meet这样的应用非常有用,但它可用于构建的远不止简单的电话会议。

事实上,您可以在canvas上放置的任何内容(种类繁多!)都可以作为视频发送到WebRTC会话中。

在我们深入细节之前,请注意这里介绍的技术通常适用于任何堆栈。我们使用LiveKit来简化发布流程。请遵循此迷你教程,了解如何设置您自己的LiveKit项目。

使用 captureStream() 捕获 Canvas

WebRTC基于MediaStreams的概念,它就是……呃……媒体流。为了将canvas发送到会话中,我们需要从canvas获取一个MediaStream。幸运的是,使用HTMLCanvasElement.captureStream() API可以非常轻松地实现。

让我们创建一个简单的canvas,在上面画一个圆圈,然后从中创建一个MediaStream

export const CircleCanvasPublisher() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const mediaStreamRef = useRef<MediaStream | null>(null);
  
  // Draw a circle
  useEffect(() => {
    if(!canvasRef.current) return;
    const ctx = canvasRef.current.getContext("2d");
    ctx.beginPath();
    ctx.arc(canvasRef.current.width / 2, canvasRef.current.height / 2, 50, 0, 2 * Math.PI);
    ctx.fillStyle = "blue";
    ctx.fill();
  }, []);
  
  // Create MediaStream
  useEffect(() => {
    if(mediaStreamRef.current || !canvasRef.current) return;

    // Create 30fps MediaStream
    mediaStreamRef.current = canvasRef.current.captureStream(30);
  }, [])

  return (
    <canvas width={300} height={300} ref={canvasRef} />
  )
}

在上面的代码片段中,我们

  • 创建了一个canvas并存储了对它的引用
  • 在canvas上画了一个小圆圈(何乐而不为?)
  • 以每秒30帧的速度从canvas创建了一个MediaStream并存储了对它的引用

现在我们有了MediaStream。剩下的就是将其发布到您的会话中。使用LiveKit,这只需要几行代码

// At the top of the component
const { localParticipant } = useLocalParticipant()

// In the useEffect that creates the MediaStream
const publishedTrack = mediaStreamRef.current.getVideoTracks()[0]
localParticipant.publishTrack(publishedTrack, {
  name: "video_from_canvas",
  source: Track.Source.Unknown
})

将它们组合在一起

瞧!将canvas作为视频发送就是这么简单。我们在 WebGL canvas 上使用了相同的技术来制作我们的 VTuber 流媒体应用

这是包含使用LiveKit创建会话的附加代码的完整代码

import { useEffect, useRef, useCallback } from "react";
import { Track } from "livekit-client";
import { useLocalParticipant } from "@livekit/components-react";

export const CircleCanvasPublisher() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const mediaStreamRef = useRef<MediaStream | null>(null);
  const { localParticipant } = useLocalParticipant()
  
  // Draw a circle
  useEffect(() => {
    if(!canvasRef.current) return;
    const ctx = canvasRef.current.getContext("2d");
    ctx.beginPath();
    ctx.arc(canvasRef.current.width / 2, canvasRef.current.height / 2, 50, 0, 2 * Math.PI);
    ctx.fillStyle = "blue";
    ctx.fill();
  }, []);
  
  // Create MediaStream
  useEffect(() => {
    if(mediaStreamRef.current || !canvasRef.current) return;

    // Create 30fps MediaStream
    mediaStreamRef.current = canvasRef.current.captureStream(30);
    
    // Publish the canvas video
    const publishedTrack = mediaStreamRef.current.getVideoTracks()[0]
    localParticipant.publishTrack(publishedTrack, {
      name: "video_from_canvas",
      source: Track.Source.Camera
    })
  }, [])

  return (
    <canvas width={300} height={300} ref={canvasRef} />
  )
}

@/components/CircleCanvasPublisher.tsx

import { LiveKitRoom } from "@livekit/components-react";
import { CircleCanvasPublisher } from "@/components/CircleCanvasPublisher";

type RoomProps {
  token: string
  wsUrl: string
}

export const Room = ({token, wsUrl}: RoomProps) => {
  return (
    <LiveKitRoom token={token} serverUrl={wsUrl} connect={true}>
      <CircleCanvasPublisher />
    </LiveKitRoom>
  )
}

@/components/Room.tsx

加入我们

您现在应该拥有在WebRTC会话中发布canvas媒体所需的一切。如果您对此帖子有任何反馈,或者最终使用或受其启发构建了很酷的东西,请加入LiveKit Slack社区告诉我们。我们很高兴听到大家的声音!