本文分享译自 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 测试。
协商
服务器收到启动新 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);
}
这导致以下调用流程:
注意:如果只创建了媒体传输,我无法成功连接。我还必须创建一个数据通道。
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];
});
措施
现在连接已经建立,我可以测量 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