使用 WebRTC 构建视频回声测试应用程序:视频流和字节之旅

本文分享译自 WebRTC for Developers 博客作者 Olivier Anguenot。在使用 WebRTC 构建一个视频回声测试应用程序时的视频流和字节之旅!全文如下:

在此之前,我从未真正处理过视频流和字节,这意味着我无需操作视频媒体本身。我一直依赖库或框架来完成这项工作。

我没有媒体技能,也不是视频专家。

但最近,我尝试使用 WebRTC 构建一个视频回声测试应用程序,以获得视频流的质量测量结果,然后尝试从服务器录制视频。

为此,我使用了node-datachannel 库。

本文总结了我的发现,以及如果您采用这项技术并掌握媒体处理方法,它将为您带来的各种可能性。

因此,不要指望找到一份完整的指南,而是要了解我是如何尝试构建这一应用程序的,以及我所面临的困难。

Node-Datachannel:Node.js 中的 WebRTC

我一直在寻找一种使用 WebRTC 和 Node.js 构建(视频)回声测试应用程序的方法,而 node-datachannel 似乎是正确的选择。

该库是 C/C++ libdatachannel 库的 Node.js 绑定。

由于我熟悉 Node.js 环境,因此使用 Node.js 对我来说更得心应手。

起初,我在寻找一种在浏览器和服务器之间启动数据通道连接的方法,因为我需要做的只是检查连接是否建立并测量 RTT。

node-datachannel 可以做到这一点,因为它支持 JSEP 协议 RFC 8829 等,而且与浏览器兼容。

但由于该库还支持媒体传输,因此我也可以用它来传输音频和视频流。

这意味着我可以使用 Node.js 构建回声测试应用的服务器端,并在浏览器和服务器之间建立客户端-服务器端连接,以交换数据流。

构建视频回声测试应用程序

视频回声测试(echo-test)应用程序是一个简单的应用程序,它从浏览器捕获视频流,然后将其发送到服务器,服务器再将其发送回浏览器。最后,浏览器会在不同的 HTML 元素中再次显示视频流。

要创建视频回声测试应用,我需要以下设备:

  • 一个 Node.js 服务器来处理信号,然后是一个对等连接来处理数据通道和媒体传输。
  • 捕获视频流并将其发送到服务器的前端应用程序。

呼叫初始化

调用流程如下:

  • 浏览器调用服务器的 REST API,利用服务器端事件技术(Server Side Eventstechnology)启动持久连接。该 SSE 通道用于随时向客户端发送消息,而无需客户端轮询服务器。
  • 浏览器调用服务器的 REST API,要求服务器启动新的 echo 测试。
使用 WebRTC 构建视频回声测试应用程序:视频流和字节之旅
Echo Test

协商

服务器收到启动新 echo-test 的请求后,就会使用节点数据通道实例化一个新的对等连接,并将提议和相关候选连接发送给浏览器。

export const createNewCall = (user) => {
  // create the peer connection
  user.pc = new nodeDataChannel.PeerConnection(user.id, {iceServers: ['stun:stun.l.google.com:19302']});

  // create the video track
  let video = new nodeDataChannel.Video('video', 'SendRecv');
  video.addH264Codec(98);
  user.pc.addTrack(video);

  // Create a datachannel
  user.dc = user.pc.createDataChannel(user.id);

  // Send the offer and candidates to the browser using SSE
  user.pc.onLocalDescription((sdp, type) => {
    user.sse.write('event: offer\n');
    user.sse.write(`data: ${JSON.stringify({type, sdp})}\n\n`);
  });
  user.pc.onLocalCandidate((candidate, mid) => {
    user.sse.write('event: candidate\n');
    user.sse.write(`data: ${JSON.stringify({candidate, mid})}\n\n`);
  });

}

在浏览器方面,没有什么特别之处,浏览器会执行以下步骤:

  • 它创建一个新的对等连接,并设置要约和收到的候选者、
  • 添加本地视频轨迹,生成答案和候选者并发送回服务器、
  • 使用 REST API 将答案和候选人发送至服务器
// Server-side. When receiving the answer
export const answer = (user, description) => {
  user.pc.setRemoteDescription(description.sdp, description.type);
}

// Server-side. When receiving a candidate
export const addCandidate = (user, msg) => {
  user.pc.addRemoteCandidate(msg.candidate, msg.sdpMid);
}

这导致以下调用流程:

使用 WebRTC 构建视频回声测试应用程序:视频流和字节之旅
Echo Test

注意:如果只创建了媒体传输,我无法成功连接。我还必须创建一个数据通道。

In call

如果协商成功,服务器和浏览器就处于呼叫状态。

可以通过 ICE 状态或对等连接的连接状态检查连接状态。

// Server-side. ICE state should be 'completed'
user.pc.onIceStateChange((state) => {
  if (state === 'completed') {
    // connection is ok
  }
});

// Server-side. Connection state should be 'connected'
user.pc.onStateChange((state) => {
    if (state === 'connected') {
      // connection is ok
    }
  });

或者,还可以检查媒体和数据通道传输的状态。

// Server-side. Data-channel is open
user.dc.onOpen(() => {
  // The data-channel has been opened successfully;
});

// Server-side. Video stream is flowing
const videoTrack = user.pc.addTrack(video);

let sessionVideo = new nodeDataChannel.RtcpReceivingSession();
videoTrack.setMediaHandler(sessionVideo);

videoTrack.onOpen(() => {
  // Video track has been opened successfully
});

最后一步是将接收到的视频流发送回浏览器。在这里,我复制了收到的 RTP 数据包。

trackVideo.onMessage((message) => {
    // Send the video stream back to the browser
    trackVideo.sendMessageBinary(message);
  });

无需做更多。在浏览器方面,应该可以接收并显示新的视频轨道。

// Browser-side. When receiving the video stream
pc.onTrack((track) => {
  remoteVideo.srcObject = track.streams[0];
});
使用 WebRTC 构建视频回声测试应用程序:视频流和字节之旅
Echo Test

措施

现在连接已经建立,我可以测量 RTT 和丢包率:

  • 从服务器端: 节点数据通道提供 RTT 和数据通道上交换的流量。但媒体传输无法获得这些统计数据。我正在与该库的作者讨论这一点,以确保我的实现不存在问题。
  • 在浏览器中,我使用 Webrtc-metrics 库直接从浏览器获取媒体统计信息。

目前,我的回声测试应用程序可以正常工作,但显示的质量却没有达到预期水平。即使服务器在本地运行(零延迟),视频流也被限制在 300 kbits/s,但统计数据似乎仍然显示带宽受限……

在使用 node_datachannel 时,我发现了这个基于 WebRTC DataChannel 的应用程序,它允许测试连接是否丢失数据包 Packet Loss Test。我更新了自己的样本,使用不可靠的数据通道进行了一些测试,结果让我大吃一惊,在通过通道和 Turn 服务器发送二进制数据时,很容易就会丢失一些数据包。

录制视频流

下一步是在服务器端录制视频流。

这也是我目前的极限所在,因为我没有操作视频流和字节的真正技能。

到目前为止,除了更换视频流的背景,我还从未操作过视频流本身。

而且我必须承认,我在视频编解码器、容器和格式的丛林中有点迷失方向。

因此,这部分工作仍在进行中。我现在分享的是我目前所做的工作。我需要更长的时间才能理解并最终完成它。

使用 Stream API

我尝试的第一件事是使用 Stream API 获取所有数据包,并将这些部分保存到文件中。

但是,拥有 H264 或 VP8 RTP 数据包并不意味着可以直接将它们写入文件。事实上,您可以这样做;但您将无法播放该视频文件,因为数据没有正确编码并封装在容器中。

不过,使用 Stream API 是获取数据并对其进行操作的好方法,因为您可以访问大量数据,而不必一次性将整个数据加载到内存中。

// Server-side
const videoStream = new Readable({
  read() {
  }
});

const parseRtpHeader = (msg) => {
    let version = (msg[0] >> 6);
    let padding = (msg[0] & 0x20) !== 0;
    let extension = (msg[0] & 0x10) !== 0;
    let csrcCount = msg[0] & 0x0F;
    let marker = (msg[1] & 0x80) !== 0;
    let payloadType = msg[1] & 0x7F;
    let sequenceNumber = msg.readUInt16BE(2);
    let timestamp = msg.readUInt32BE(4);
    let ssrc = msg.readUInt32BE(8);
    let csrcs = [];
    for (let i = 0; i < csrcCount; i++) {
      csrcs.push(msg.readUInt32BE(12 + 4 * i));
    }
    return {
      version,
      padding,
      extension,
      csrcCount,
      marker,
      payloadType,
      sequenceNumber,
      timestamp,
      ssrc,
      csrcs,
      length: 12 + 4 * csrcCount
    };
  };

const videoWritableStream = fs.createWriteStream(filePath);

// Server-side. When receiving the video stream
trackVideo.onMessage((message) => {
  const header = parseRtpHeader(message);
  const H264Payload = message.slice(header.length);
  videoStream.push(H264Payload);
});

// If you need to deal with data on the road
videoStream.on('data', (chunk) => {
  // Write to the file using the writable stream
  videoWritableStream.write(chunk);
});

videoStream.on('end', () => {
  // do something when the stream ends
  videoWritableStream.end();
  videoWritableStream.on('finish', () => {
    console.log(`Video saved to ${filePath}`);
  })
});

这里实际上并不需要使用 Stream API,因为视频流已经是流了。也就是说,我认为我成功访问了 H264 有效负载。

但提取 H264 负载只是将视频流保存为可播放格式的第一步,然后,您需要解包、处理 NAL 单元……然后将视频流编码为 MP4 或 WebM 等容器格式。

我尝试使用Fluent-ffmpeg来帮助我完成此管道,但我从未成功获得有效的视频文件。我的文件始终是空的,也许错过了什么。

ffmpeg()
  .input(videoStream)
  .inputFormat('h264')
  .output(videoWritableStream)
  .outputFormat('mp4')
  .on('end', () => {
    console.log('Video saved');
  });

注意:Chrome 124将引入 WebSocketStream API

使用 GStreamer

我尝试的第二件事是使用 GStreamer 来录制视频流。

为此,我将创建一个 UDP 套接字并将视频流发送到特定端口。

// Server-side
import dgram from "dgram";
const udpSocket = dgram.createSocket('udp4');

trackVideo.onMessage((message) => {
  udpSocket.send(message, 5000, "127.0.0.1", (err) => {
    if (err) {
      console.error(err);
    }
  });
});

然后使用 GStreamer 接收视频流并将其记录到文件中。

$ gst-launch-1.0 -e udpsrc address=127.0.0.1 port=5000 caps="application/x-rtp" ! queue ! rtph264depay ! h264parse ! mp4mux ! filesink location=./video.mp4

这次效果不错。我可以将视频流录制到文件中,然后回放(感谢 Murat 提供的命令😅)。

但在这里,正如你所看到的,所有工作都是由 GStreamer 管道完成的。我只需将视频 RTP 数据包发送到 UDP 插口即可。

使用 Node 原生模块

我最后尝试使用 Node 原生模块录制视频流。

因此,我没有从命令行启动 GStreamer,而是尝试直接从 Node.js 应用程序中使用它。

请记住,我从未为 Node.js 写过 C++ 代码或原生模块。

但我还是尝试了一下。

为此,我需要:

  • 一个实现 Node-API 的 C++ 文件,用于创建本地模块,并导出 JavaScript 代码可访问的函数。这部分应加载 GStreamer 库,并创建录制视频流的管道。
  • 使用 binding.gyp 文件将 C++ 文件编译为共享库。
  • 修改 package.json,以便在进行 npm 安装时编译该库。

我成功编写了一个模块,可以启动 GStreamer 管道,但不是我所期望的那个…

事实上,我遇到了两个问题:

  • 首先,GStreamer 管道在主线程上运行,因此会阻塞 Node.js 事件循环。
  • 其次,我需要存储一个处理程序,以便在需要时停止管道。

我找到了关于 Node-API 的文档https://nodejs.github.io/node-addon-examples/,它为我的问题提出了解决方案。

但我还需要花更多时间来理解如何实现,不过它向我展示了如何编写本地模块。

结论

要了解如何处理视频流和字节并非易事。这需要良好的媒体和编解码器知识。

由于 WebRTC 为开发人员处理媒体提供了越来越多的可能性,因此更好地理解这部分内容会很有帮助。我希望在 Node.js 生态系统中看到新的库和工具。

即使您不致力于创建新的编解码器或优化视频,您可能仍然需要操作视频流,例如,从 IP 摄像头捕获视频并将其显示在 WebRTC 应用程序中。

在这个过程中,我发现了很多将来可能需要更好理解的东西。

最后一件事与 libdatachannel 和 node-datachannel 有关: 我发现这个库非常强大,为 Node.js 提供了很多可能性。

我希望它们能继续发展,并在未来得到越来越多的应用。

译自:https://www.webrtc-developers.com/a-journey-on-video-streams-and-bytes

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/46874.html

(0)

相关推荐

发表回复

登录后才能评论