对近期接触到的直播相关知识进行梳理
M3U8 文件是 HLS (HTTP Live Streaming) 协议的核心组件,它本质上是一个播放列表文件,包含媒体流的结构信息。直播与视频聊天的对比:我觉得直播是通过流媒体技术把现场的图像和声音采集后分发出去,供观众在线围观,可以认为是开放的,一般没有人数限制;而视频聊天是点对点的线上交流,是基于UDP/TCP的实时传输协议实现的。流媒体是通过互联网实时传输音视频内容的技术,用户无需等待完
直播与点播的对比、直播与视频聊天的对比
直播传输的是视频流,而点播是播放的录制好的视频文件
直播与视频聊天的对比:我觉得直播是通过流媒体技术把现场的图像和声音采集后分发出去,供观众在线围观,可以认为是开放的,一般没有人数限制;而视频聊天是点对点的线上交流,是基于UDP/TCP的实时传输协议实现的。
当然,两者也是可以结合的,比如:在直播过程中跟某个观众连麦。
视频流全链路技术
一、视频流媒体基础概念
-
流媒体本质
流媒体是通过互联网实时传输音视频内容的技术,用户无需等待完整下载即可开始观看,核心是将内容分割为小数据包顺序传输。 -
关键处理流程
推流:将视频流上传至服务器(如RTMP推送直播画面)
转码:改变视频编码格式/参数以适应不同终端(如H265转H264)
分发:将处理后的视频流通过多种协议传输给终端用户
二、核心协议与技术标准
- 传输协议
RTMP:Adobe推出的低延迟协议,适合推流但需Flash支持
HLS:苹果开发的基于HTTP的流媒体协议,使用M3U8索引文件和TS分片
WebRTC:支持浏览器实时通信的开放标准,无需插件
RTSP:用于控制媒体服务器,常与RTP配合传输流数据 - 编码标准
H.264/AVC:最广泛兼容的编码格式,浏览器原生支持
H.265/HEVC:压缩效率提升50%,但需要终端解码支持
VP9/AV1:开源编码格式,适合Web环境但编码复杂度高
三、视频处理关键技术方案
- 转码实现方式
云端转码:利用云服务(如阿里云、腾讯云)处理,无需自建基础设施
本地转码:使用FFmpeg等工具自主处理,灵活但资源消耗大
# FFmpeg转码示例:生成HLS流
ffmpeg -i input.mp4 -hls_time 10 -hls_list_size 0 output.m3u8
- 转码技术原理
解封装:分离容器中的音视频流(如MP4中的H264视频和AAC音频)
解码:将压缩数据还原为原始帧(YUV视频/PCM音频)
处理:缩放、裁剪、滤镜等操作
编码:按目标格式重新压缩(如H264转H265)
封装:将流重新打包为目标容器格式(如TS分片) - 播放器技术选型
H5 Video:原生video标签,兼容性有限但无需插件
Video.js:开源HTML5播放器,支持HLS/DASH等协议
JWPlayer:商业级解决方案,功能全面但需付费
四、典型架构设计方案
- 轻量级解决方案
[摄像头] --RTSP--> [Nginx+RTMP模块] --HLS--> [浏览器]
↑
[FFmpeg转码]
- 企业级视频平台
[设备] --GB28181--> [Liveweb汇聚平台] --WebRTC/FLV--> [多终端]
├─ 云端录像存储
├─ AI分析接入
└─ 多协议转换:ml-citation{ref="1" data="citationList"}
五、实践建议与优化方向
协议选择:优先考虑HLS+WebRTC组合,平衡兼容性与延迟
转码策略:根据终端能力动态选择输出格式(如移动端用H264)
性能优化:使用硬件加速(如Intel QSV/NVIDIA NVENC)提升转码速度
直播demo的实现小结
1. 基于rtmp和http-flv的直播链路
[obs推流软件] --RTMP--> [流媒体服务] --HLS--> [VLC播放器或flv.js]
-
流媒体服务:在gitee上找到了一个基于netty的java版rtmp服务器 https://gitee.com/kabuzhu/srsj
具体代码逻辑在后续研究。目前只是将其本地运行,充当流媒体服务。 -
配置obs的推流
媒体源可以是本地视频文件,也可以是视频采集设备等多种形式。 -
配置vlc的播放链接
-
也可以用flv.js在浏览器上播放
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script src="https://cdn.bootcdn.net/ajax/libs/flv.js/1.6.2/flv.min.js"></script>
<style>
body,center{
padding:0;
margin:0;
}
.v-container{
width:640px;
height:360px;
border:solid 1px red;
}
video{
width:100%;
height:100%;
}
</style>
</head>
<body>
<div class="v-container">
<video id="videoElement" muted autoplay="autoplay" preload="auto" controls="controls">
</video>
</div>
</body>
</html>
<script>
if (flvjs.isSupported()) {
var videoElement = document.getElementById('videoElement');
var flvPlayer = flvjs.createPlayer({
type: 'flv', // 指定视频类型,特别注意此处为flv
isLive: true, // 开启直播
hasAudio: false, //需要设置为false不然播放不了视频
cors: true, // 开启跨域访问
url: 'http://127.0.0.1:8762/myapp/stream.flv'
},
{
enableWorker: false, //不启用分离线程
enableStashBuffer: false, //关闭IO隐藏缓冲区
reuseRedirectedURL: true, //重用301/302重定向url,用于随后的请求,如查找、重新连接等。
autoCleanupSourceBuffer: true //自动清除缓存
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
// flvPlayer.stop();
}
</script>
2. 基于HLS协议的直播链路
- 理解m3u8文件
M3U8 文件是 HLS (HTTP Live Streaming) 协议的核心组件,它本质上是一个播放列表文件,包含媒体流的结构信息。在直播场景中,M3U8 文件确实像一个动态滑动的窗口,不断更新以反映最新的媒体分片
#EXTM3U
#EXT-X-VERSION:3 文件头标识, 用于标识文件类型和协议版本
#EXT-X-TARGETDURATION:13 目标时长,指所有分片的最大时长(向上取整)
#EXT-X-MEDIA-SEQUENCE:29 媒体序列号,当前播放列表的起始分片序号
#EXTINF:10.416667 分片信息,包含每个分片的时长和文件名
segment029.ts
#EXT-X-ENDLIST 结束标记(仅限点播)
点播 vs 直播的 M3U8 差异
点播包含所有分片信息,序列号固定为1,含有结束标记
直播中的分片信息是动态更新的,序列号会递增,无结束标记表示直播仍在继续
- 使用ffmpeg模拟ts文件和m3u8文件的生成
ffmpeg -re -stream_loop -1 -i /home/ftpuser/tc/data/trailer.mp4 -c:v copy -c:a copy -f hls -hls_time 10 -hls_list_size 5 -hls_flags delete_segments -hls_segment_filename "/home/ftpuser/tc/data/20250619/2005/success_trailer_prefix-%04d.ts" -hls_base_url "http://192.168.0.37:9091/tc/data/20250619/2005/" /home/ftpuser/tc/data/20250619/2005/trailer_playlist.m3u8
- 编写shell脚本,自己来根据ts文件动态生成m3u8文件
由于获取ts文件的时长需要依赖ffmpeg,所以需要事先安装。另外,该脚本将hls_base_url的前缀部分作为了入参。
你可以根据自己的需求编写脚本。
#!/bin/bash
set -euo pipefail
# 检查参数
if [ $# -ne 1 ]; then
echo "Usage: $0 <hls_base_url>"
exit 1
fi
hls_base_url="$1"
base_dir="/home/ftpuser/tc/data"
# 获取日期函数
get_date() {
date -d "$1" +%Y%m%d
}
# 获取TS时长函数 - 使用ffprobe
get_duration() {
local file="$1"
local default_duration=10.000000
local duration
# 使用ffprobe尝试获取时长
if duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null); then
# 检查获取到的duration是否为数字
if [[ "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
printf "%.6f" "$duration"
return 0
fi
fi
# 如果上面获取失败,则使用默认时长
printf "%.6f" "$default_duration"
return 0
}
# 状态存储
declare -A stream_status # key: dir:prefix -> "last_seq last_timestamp ended"
# 主循环
while true; do
# 获取监控目录(前一天和当天)
watch_dirs=(
"$base_dir/$(get_date 'yesterday')/2005"
"$base_dir/$(get_date 'today')/2005"
)
current_time=$(date +%s)
for dir in "${watch_dirs[@]}"; do
# 跳过不存在的目录
[ -d "$dir" ] || continue
# 初始化目录流数组
declare -A prefix_files=()
# 扫描所有TS文件
while IFS= read -r file; do
filename=$(basename "$file")
# 解析前缀和序号
if [[ "$filename" =~ ^(.+)-([0-9]{4})\.ts$ ]]; then
prefix="${BASH_REMATCH[1]}"
sequence="${BASH_REMATCH[2]#0}" # 去除前导零
sequence="${sequence#0}" # 多次处理确保
sequence="${sequence#0}"
sequence=$((10#$sequence)) # 转为十进制
key="${dir}:${prefix}"
# 初始化数组元素
if [ -z "${prefix_files[$key]:-}" ]; then
prefix_files["$key"]=""
fi
prefix_files["$key"]="${prefix_files[$key]} $sequence:$file"
fi
done < <(find "$dir" -maxdepth 1 -type f -name '*-[0-9][0-9][0-9][0-9].ts' 2>/dev/null)
# 处理每个流
for key in "${!prefix_files[@]}"; do
dir="${key%:*}"
prefix="${key#*:}"
m3u8_file="${dir}/${prefix}.m3u8"
# 初始化流状态
if [ -z "${stream_status[$key]:-}" ]; then
stream_status[$key]="0 0 0" # last_seq, last_time, ended
fi
# 解析状态
read -r last_seq last_timestamp ended <<< "${stream_status[$key]}"
# 确保时间戳是数字
last_timestamp=${last_timestamp:-0}
last_seq=${last_seq:-0}
ended=${ended:-0}
# 跳过已结束的流
if [ "$ended" -eq 1 ]; then
continue
fi
# 收集有效TS文件(已写完的)
valid_files=()
max_seq=0
for item in ${prefix_files[$key]}; do
seq="${item%%:*}"
file="${item#*:}"
# 检查文件是否正在写入
if ! lsof -w -- "$file" >/dev/null 2>&1; then
valid_files+=("$seq:$file")
if [ "$seq" -gt "$max_seq" ]; then
max_seq="$seq"
fi
fi
done
# 按序号排序
mapfile -t sorted_files < <(printf '%s\n' "${valid_files[@]}" | sort -t: -k1,1n)
# 处理新文件
new_files=()
for item in "${sorted_files[@]}"; do
seq="${item%%:*}"
file="${item#*:}"
if [ -n "$seq" ] && [ -n "$last_seq" ] && [ "$seq" -gt "$last_seq" ]; then
new_files+=("$item")
fi
done
# 更新M3U8文件
if [ "${#new_files[@]}" -gt 0 ] || [ "$last_seq" -eq 0 ]; then
# 读取现有分片(如果存在)
existing_segments=()
media_sequence=0
if [ -f "$m3u8_file" ]; then
# 正确读取现有分片信息(EXTINF行和TS行)
while IFS= read -r line1 && IFS= read -r line2; do
if [[ "$line1" == "#EXTINF:"* && "$line2" == *".ts" ]]; then
existing_segments+=("$line1" "$line2")
fi
done < <(awk '/#EXTINF:/{getline line; print $0; print line}' "$m3u8_file")
fi
# 添加新分片到现有分片列表
for item in "${new_files[@]}"; do
seq="${item%%:*}"
file="${item#*:}"
duration=$(get_duration "$file")
rel_path="${dir#$base_dir/}"
ts_url="$hls_base_url/$rel_path/$(basename "$file")"
existing_segments+=("#EXTINF:$duration," "$ts_url")
done
# 只保留最后5个分片(10行:5个EXTINF + 5个TS)
if [ "${#existing_segments[@]}" -gt 10 ]; then
existing_segments=("${existing_segments[@]:${#existing_segments[@]}-10}")
fi
# 如果媒体序列号未设置,计算最小序列号
if [ "$media_sequence" -eq 0 ]; then
# 确保数组至少有一个完整的分片(EXTINF + URL)
if [ "${#existing_segments[@]}" -ge 2 ]; then
# 尝试从第一个分片URL中提取序列号
first_ts_url="${existing_segments[1]}"
if [[ "$first_ts_url" =~ -([0-9]{4})\.ts$ ]]; then
sequence_str=${BASH_REMATCH[1]}
media_sequence=$((10#$sequence_str))
else
media_sequence=1
fi
else
# 没有分片,使用默认序列号
media_sequence=1
fi
fi
# 生成M3U8内容
tmp_file=$(mktemp)
{
echo "#EXTM3U"
echo "#EXT-X-VERSION:3"
echo "#EXT-X-TARGETDURATION:15"
echo "#EXT-X-MEDIA-SEQUENCE:$media_sequence"
# 输出所有分片
for ((i=0; i<${#existing_segments[@]}; i++)); do
echo "${existing_segments[$i]}"
done
} > "$tmp_file"
# 替换原文件
mv -f "$tmp_file" "$m3u8_file"
chmod 644 "$m3u8_file"
# 更新状态
last_seq="$max_seq"
last_timestamp="$current_time"
stream_status["$key"]="$last_seq $last_timestamp 0"
fi
# 检查超时 - 确保时间戳是有效的数字
if [ -n "$last_timestamp" ] && [ "$last_timestamp" -gt 0 ] &&
[ "$((current_time - last_timestamp))" -ge 25 ]; then
# 添加ENDLIST标记
if [ -f "$m3u8_file" ] && ! grep -q '#EXT-X-ENDLIST' "$m3u8_file"; then
echo "#EXT-X-ENDLIST" >> "$m3u8_file"
chmod 644 "$m3u8_file"
fi
stream_status["$key"]="$last_seq $last_timestamp 1"
fi
done
done
# 每秒执行一次
sleep 1
done
- 使用hls.js进行直播的观看
<!DOCTYPE html>
<html>
<head>
<title>HLS live</title>
<link href="https://vjs.zencdn.net/7.20.3/video-js.css" rel="stylesheet">
</head>
<body>
<h1>HLS live</h1>
<video
id="live-player"
class="video-js vjs-default-skin vjs-big-play-centered"
controls
preload="auto"
width="1280"
height="720"
data-setup='{}'
>
<source src="http://192.168.0.37:9091/tc/data/20250619/2005/playlist.m3u8" type="application/x-mpegURL">
</video>
<script src="https://vjs.zencdn.net/7.20.3/video.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/videojs-contrib-hls@5.15.0/dist/videojs-contrib-hls.min.js"></script>
<script>
// 自动重连逻辑
const player = videojs('live-player');
player.on('error', () => {
console.log('播放错误,尝试重新加载...');
setTimeout(() => {
player.src({src: 'http://192.168.0.37:9091/tc/data/20250619/2005/playlist.m3u8', type: 'application/x-mpegURL'});
player.play();
}, 3000);
});
// 每30秒刷新播放列表
setInterval(() => {
player.trigger('loadstart');
}, 30000);
</script>
</body>
</html>

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。
更多推荐
所有评论(0)