你所热爱的,就是你的生活关于友链

使用 WebSocket 和 ffmpeg 直播 canvas

swwind#canvas#ffmpeg#websocket

注意:这篇文章已经发布超过 5 年,世界线的变动可能会导致故事走向不同的结局


最近一直在折腾 rtmp 直播的相关内容,想要用 electron 写一个直播的软件。

现在打算写一下我的实现方法。

在这之前,我们需要了解两个浏览器提供的 API: CanvasCaptureMediaStream 和 MediaRecorder API。

CanvasCaptureMediaStream 用于从 canvas 元素创建原始视频流。

MediaRecorder API 允许在浏览器中使用编解码器,将原始视频转换为可有效发送到服务器的 VP8,VP9 或 H.264 编码视频。

获取到视频之后,我们需要使用 WebSocket 将数据发送到后台,再在后台使用 ffmpeg 进行 rtmp 推流。

大致思路就是这样。

搭建 rtmp 服务器

网上教程太多了。。。

推荐个 gwuhaolin/livego,下载即用,不用装什么乱七八糟的 nginx。

启动之后可以直接 rtmp://localhost:1935/live/video 推流,之后用 VLC 播放器打开 rtmp://localhost:1935/live/video.flv 即可看到直播内容。

可以先试着用 ffmpeg 推一个视频看看效果: ffmpeg -re -i your_video.mp4 -vcodec libx264 -acodec aac -f flv rtmp://localhost:1935/live/video

不出意外一切正常。

前端

MediaStream 可以轻松的添加流轨道(Track)。

const ws = new WebSocket("ws://localhost:16547");

ws.addEventListener("open", (e) => {
  const mediaStream = new MediaStream();

  // 添加视频轨
  const video = document.querySelector("canvas").captureStream(30);
  video.getTracks().forEach(mediaStream.addTrack.bind(mediaStream));

  // 添加音频轨(From <audio> element)
  const audioContext = new AudioContext();
  const audioSrc = audioContext.createMediaElementSource(
    document.querySelector("audio"),
  );
  const destination = audioContext.createMediaStreamDestination();
  audioSrc.connect(destination);
  destination.stream
    .getTracks()
    .forEach(mediaStream.addTrack.bind(mediaStream));

  // 创建 MediaRecorder
  const mediaRecorder = new MediaRecorder(mediaStream, {
    mimeType: "video/webm;codecs=h264",
    audioBitsPerSecond: 44100, // 44.1kHz
    videoBitsPerSecond: 3000000, // 3000k 画质
  });

  mediaRecorder.addEventListener("dataavailable", (e) => {
    // 将数据发送到后台
    // 发送时 e.data 的类型是 Blob
    ws.send(e.data);
  });

  // 开始录制并每隔 1s 发送一次数据
  mediaRecorder.start(1000);
});

后端

后端采用 nodejs 编写,打算用 PHP 的请退群。

安装依赖:

npm install ws

同样也是短的不得了

#!/bin/node

const WebSocketServer = require("ws").Server;
const wss = new WebSocketServer({ port: 16547 }); // 随便开个端口

const { spawn } = require("child_process");

const RTMP_SERVER = "rtmp://localhost:1935/live/video";

wss.on("connection", (ws) => {
  const ffmpeg = spawn("ffmpeg", [
    // 从 stdin 中读入视频数据
    "-i",
    "-",

    // 视频转码
    // 由于视频已经是 H.264 编码,可以直接复制
    // 若需要转码则填 libx264
    "-vcodec",
    "copy",

    // 音频转码
    "-acodec",
    "aac",

    // 输出为 flv 格式
    "-f",
    "flv",

    // RTMP 服务器
    RTMP_SERVER,
  ]);

  ws.on("message", (msg) => {
    // 收到时 msg 的类型是 Buffer
    ffmpeg.stdin.write(msg);
  });

  ws.on("close", (e) => {
    // 断开链接即中断推流
    ffmpeg.kill("SIGINT");
  });
});

客户端

可以直接使用 DPlayerflv.js 来播放 HTTP-FLV 直播流。

照样短的不得了

<script src="flv.min.js"></script>
<script src="dplayer.min.js"></script>
<link rel="stylesheet" href="dplayer.min.css" />

<div id="dplayer"></div>
const dp = new DPlayer({
  container: document.getElementById("dplayer"),
  video: {
    url: "http://localhost:7001/live/video.flv",
  },
  live: true,
  autoplay: true,
});

后话

由于我最终使用 electron 包装,所以 WebSocket 这一步就免了。

然后唯一的问题就是 Blob 转 Buffer 了。

可以用 npm 上一个包 blob-to-buffer 解决。

参考资料

fbsamples/Canvas-Streaming-Example

以上。