【项目】京东直播

这几年直播太火了,H5端实现方案又是哪样,本文将与大家一起聊聊H5端视频直播中的基本流程和主要的技术点,包括但不限于前端技术。

任职【京东零售】期间负责的项目

一、大致实现流程

获取音视频流

采用getUserMedia进行音视频录制,它是 MediaStream API 的一部分,可用来请求用户的摄像头和麦克风权限,并获取到相应的媒体流。

代码示例

1
2
3
4
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(function(stream) {
// ...
})

建立 WebRTC 连接

音视频流获取到了,但需要实时呈现给用户端,这时需要使用 RTCPeerConnection 建立与服务器端的实时连接。

代码示例

1
2
3
4
5
6
7
8
9
// 定义 ICE(交互式连接建立)服务器的配置
var configuration = { iceServers: [{ urls: "stun:stun.example.org" }] };
// 使用配置创建一个新的 RTCPeerConnection 对象,用于建立与服务器的连接。
var peerConnection = new RTCPeerConnection(configuration);
// 遍历媒体流中的所有轨道(视频轨道和音频轨道)。
stream.getTracks().forEach(track => {
// 将每个轨道添加到 RTCPeerConnection 对象中,以便进行传输。
peerConnection.addTrack(track, stream);
});

将流传输到服务器

传输之前,我们得先准备一个信令(信令是指用于建立和维护对等连接所需的控制信息。信令服务器负责协调和传递这些信令消息,以便让通信双方能够建立对等连接)服务器。假设我们的信令服务器已通过websocket搭建完毕

启动连接

1
2
// 信令服务器的 WebSocket 连接
var signalingServer = new WebSocket('ws://example.com/signaling');

信令交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 信令服务器连接成功时触发的事件
signalingServer.onopen = function() {
// 创建一个 offer
peerConnection.createOffer()
.then(function(offer) {
// 再将这个 offer 设置为本地描述,以便后续发送给对方。
return peerConnection.setLocalDescription(offer);
})
.then(function() {
// 通过信令服务器的 WebSocket 连接将 offer 发送给服务器
signalingServer.send(JSON.stringify({ type: 'offer', data: peerConnection.localDescription }));
})
.catch(function(err) { });
};
// 从信令服务器接收到消息时触发的事件
signalingServer.onmessage = function(event) {
var message = JSON.parse(event.data);
// 如果消息的类型为 'answer',则表示我们收到了另一个客户端发送的 answer。
if (message.type === 'answer') {
// 我们将接收到的 answer 设置为远程描述。这将告诉我们的 peerConnection 对象如何处理对等连接的远程端。
peerConnection.setRemoteDescription(new RTCSessionDescription(message.data))
.catch(function(err) { });
}
};

服务器端接收并转码

服务器端接收到视频流后,可以使用流媒体处理工具,如 FFmpeg,将视频流转码为适合网络传输的格式(如 HLS 或 RTMP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const WebSocket = require('ws');
const { spawn } = require('child_process');
const http = require('http');
const fs = require('fs');

// 创建 WebSocket 服务器
const wss = new WebSocket.Server({ port: 8080 });

// 创建 FFmpeg 进程
const ffmpeg = spawn('ffmpeg', [
'-i', 'pipe:0', // 从 stdin 读取视频流
'-c:v', 'libx264', // 使用 H.264 编码器
'-hls_time', '10', // HLS 分片时长
'-hls_list_size', '6', // HLS 列表大小
'-hls_flags', 'delete_segments', // 删除旧的 HLS 分片
'-f', 'hls', // 使用 HLS 格式输出
'output.m3u8' // 输出文件名
]);

// 监听 WebSocket 连接
wss.on('connection', ws => {
// 监听 WebSocket 消息
ws.on('message', data => {
// 将接收到的消息写入 FFmpeg 的标准输入流
ffmpeg.stdin.write(data);
});
});

至此,在当前工作目录下会产生一个m3u8文件(一个包含 HLS 播放列表的文件)。它本身不是一个真实的视频文件,而是一个指向视频分段的索引文件,包含了一系列的视频片段的 URL,客户端会根据这些 URL 下载视频分段并按照顺序播放,从而实现流媒体的播放。

分发流

接着奏乐接着舞,此时需要将视频流分发给其他客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const WebSocket = require('ws');
const { spawn } = require('child_process');
const http = require('http');
const fs = require('fs');

// 创建 HTTP 服务器用于向客户端传输 HLS 视频流
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
const videoFile = fs.createReadStream('output.m3u8');
videoFile.pipe(res);
});

// 启动 HTTP 服务器
server.listen(8000, () => { });

客户端播放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>

<video id="video" controls autoplay></video>

document.addEventListener("DOMContentLoaded", function() {
if (Hls.isSupported()) {
var video = document.getElementById('video');
var hls = new Hls();
hls.loadSource('path/to/your/hls/video.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
video.play();
});
}
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = 'path/to/your/hls/video.m3u8';
video.addEventListener('loadedmetadata', function() {
video.play();
});
}
});

二、视频直播格式

  • .flv:Flash Video格式,常用于使用RTMP协议进行直播。
  • .m3u8:HLS(HTTP Live Streaming)播放列表文件,用于指导播放器获取HLS格式的直播流。
  • .ts:MPEG Transport Stream格式,用于存储HLS直播流中的视频分段。
  • .rtmp:RTMP协议的直播流地址,通常不是作为文件后缀存在,而是作为流媒体地址使用。

三、HLS延时问题

我们了解到了 HLS 协议将直播流切分成一段一段的小视频片段进行下载和播放时,我们可以做出以下推论:假设播放列表中包含 5 个 TS 文件,每个 TS 文件都包含 5 秒的视频内容,那么整体的延迟将达到 25 秒。这是因为当你观看这些视频时,主播已经录制并上传了视频内容,因此会产生这样的延迟。

当然,我们可以通过减少播放列表的长度和单个 TS 文件的大小来降低延迟。极端情况下,我们可以将播放列表的长度缩减到 1,并将 TS 文件的时长缩短至 1 秒。但是这样做会增加请求次数,加大服务器压力,同时在网络速度较慢时可能会导致更多的缓冲现象。

四、如何应对延时

先对比一下流媒体技术H5技术方案分析对比

协议 flv+mse flv+webAssembly+ffmpeg+webgl hls webRTC
传输层 http-flv/websocket-flv http-flv/websocket-flv http UDP协议
视频格式 fly fly ts文件 fMp4
延时 很高 很低
H5兼容性 PC H5/安卓H5 PC H5/安卓H5/iOS H5 PC H5/安卓H5/iOS H5 PC H5
服务器编程难易 简单 简单 中等 中等
cpu性能 良好(比较适合i0S H5)

这时我们发现,hls原来不是必选的方案,但该方案的确很大众,所以先带大家踩一下坑,再出发吧!

安卓H5实现

采用 flv 格式 + MediaSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// FLV 文件的 URL
const flvUrl = 'http://example.com/video.flv';

// 创建 video 元素
const videoElement = document.getElementById('videoPlayer');

// 创建 MediaSource 对象
const mediaSource = new MediaSource();

// 将 MediaSource 对象与 video 元素关联
videoElement.src = URL.createObjectURL(mediaSource);

// 监听 MediaSource 对象的 'sourceopen' 事件
mediaSource.addEventListener('sourceopen', function () {
// 创建 SourceBuffer 对象
const sourceBuffer = mediaSource.addSourceBuffer('video/flv');

// 下载 FLV 视频文件
fetch(flvUrl)
.then(response => response.arrayBuffer())
.then(data => {
// 将 FLV 数据传递给 SourceBuffer
sourceBuffer.appendBuffer(data);
})
.catch(error => {
console.error('Failed to fetch FLV video:', error);
});
});

IOS H5实现

尴尬的是,MediaSource在ios下支持并不乐观,否则一套代码已经结束了,继续把

最终采用 flv + webAssembly + ffmpeg + webgl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 加载 WebAssembly 模块
// 这里假设你已经编译好了包含 FFmpeg 的 WebAssembly 模块,并将其命名为 `ffmpeg.wasm`
const ffmpegModule = require('./ffmpeg.wasm');

// 加载并解码 FLV 文件
async function decodeFLV() {
// 加载 FLV 文件
const response = await fetch('example.flv');
const buffer = await response.arrayBuffer();

// 使用 FFmpeg 解码 FLV 文件
const result = ffmpegModule.ccall('decodeFLV', /* arguments */);

// 获取解码后的视频帧
const videoFrames = result.videoFrames;

// 使用 WebGL 渲染视频帧
const canvas = document.createElement('canvas');
const context = canvas.getContext('webgl');
// 渲染视频帧的逻辑...

// 播放视频帧
const videoPlayer = document.getElementById('videoPlayer');
videoPlayer.srcObject = canvas.captureStream();

// 播放视频
videoPlayer.play();
}

// 初始化
decodeFLV();

五、HLS 与 FLV 对比

FLV 格式相对于 HLS(HTTP Live Streaming)格式具有较小的延迟,主要是因为它的传输和处理方式不同,以及其设计目标的不同所致。

  • 传输方式:FLV 格式通常是通过基于 TCP 的 RTMP(Real-Time Messaging Protocol)传输的,而 RTMP 是一种实时性较高的流媒体传输协议,它可以更快地传输数据,并且对网络环境的变化更敏感。相比之下,HLS 使用的是基于 HTTP 的传输方式,其实时性较差,需要较长的缓冲时间。

  • 分片大小:FLV 格式通常使用较小的分片(chunk)大小来传输数据,这可以减少传输延迟,并且更容易实现实时性要求较高的场景。相比之下,HLS 的分片通常较大,因为它需要在每个分片中包含一定量的视频数据,这会增加传输延迟。

  • 播放器缓冲策略:FLV 播放器通常会采用较短的缓冲时间,以确保尽快地播放视频内容。相比之下,HLS 播放器通常会采用较长的缓冲时间,以减少网络波动和丢包带来的影响,这会增加播放延迟。

综上所述,FLV 格式相对于 HLS 格式具有较小的延迟,主要是因为它采用了实时性较高的传输方式,使用了较小的分片大小,并且播放器通常采用较短的缓冲时间。这使得 FLV 格式更适合实时性要求较高的场景,例如直播等。

六、知识拓展

1、什么是webAssembly

  • WebAssembly(简称为Wasm)是一种低级的、面向栈式虚拟机的编程语言,旨在提供一种通用的、可移植的、高效的二进制代码格式,以便在 Web 浏览器中运行。它是一种开放标准,被设计用于在 Web 浏览器中实现高性能的 Web 应用程序,同时也可以在其他环境中运行,如服务器端、桌面端等。
  • 通过使用 WebAssembly,开发者可以使用 C、C++、Rust 等语言编写高性能的代码,并将其编译为 WebAssembly 模块,然后在 Web 页面中调用这些模块,以实现各种复杂的计算和功能,例如游戏、图像处理、多媒体处理等。

2、什么是ffmpeg

  • FFmpeg 是一个开源的跨平台音视频处理工具集,包含了许多用于处理音频、视频和多媒体流的库和工具。
  • FFmpeg 提供了一系列命令行工具,可以进行音视频的录制、转码、裁剪、合并、解析、编解码等操作。它支持几乎所有流行的音视频格式,包括但不限于 MP4、AVI、MOV、FLV、MP3、AAC、H.264、H.265 等。因此,它被广泛用于音视频处理、媒体转换、流媒体服务、视频编辑等领域。

3、音视频为什么要编码与解码?

  • 更小的体积、更快的传输。
  • 以一个分辨率1920x1280,30FPS的视频为例:1920x1280x8x3=49766400bit=6220800byte~6.22MB;30帧/秒:186.6MB;每分钟:11GB;一部90分钟的高清电影,约是1000GB;如果是2K、4K、8K…?

4、视频数据能被大量压缩的原因

  • 统计结果表明:在连续的几帧图像中,一般只有 10% 以内的像素有差别,亮度的差值变化不超过 2%,而色度的差值变化只在1%以内。

【项目】京东直播
https://www.cccccl.com/20220701/项目/京东直播/
作者
Jeffrey
发布于
2022年7月1日
许可协议