Mac平台如何下载ffmpeg并对m3u8视频进行下载转换成MP4

ffmpeg安装

在Mac平台上 本人多次使用brew工具进行ffmpeg安装都未成功

于是决定直接去ffmpeg官网下载执行包

点击进入官网

由于是国外的网站 下载速度会相对较慢 文末已经给大家准备了网盘下载链接

这里我们选择Mac :

image-20240413173518406

image-20240413173539785

image-20240413173602675

将执行压缩包解压后得到一个独立的ffmpeg执行文件:

image-20240413173929604

为了方便我们接下来的使用 需要对其进行环境变量的配置:

  1. 在终端中输入以下命令 打开环境变量配置文件:

    1
    vim ~/.bash_profile
  2. 在该文件中加入以下内容

    1
    export PATH="/path/to/ffmpeg:$PATH"

    注意: 这里需要将 /path/to/ffmpeg 替换为你的ffmpeg所在路径

  1. 使用source命令使新的环境变量生效

    1
    source ~/.bash_profile
  2. 验证是否配置成功

    在任意一个非ffmpeg所在目录执行以下命令, 如果正常打印内容 则代表配置成功:

    1
    ffmpeg -version

注意: 如果你懒得配置环境变量 还可以直接将ffmpeg执行文件移动至/usr/local/bin 此目录下的所有执行文件 全局可运行

下载m3u8视频并转换成mp4

ffmpeg支持直接访问m3u8链接并将其内容转成mp4格式, 执行命令如下:

1
ffmpeg -i {m3u8链接} -c copy -bsf:a aac_adtstoasc {文件名}.mp4

同样的该ffmpeg指令也适用于直播流的下载和转换 比如http://xxx.flv:

1
ffmpeg -i {直播链接} -c copy -bsf:a aac_adtstoasc {文件名}.mp4

ffmpeg分段写入磁盘

当我们使用ffmpeg -i 直播链接 -c copy xxx.mp4指令来下载直播视频时, 默认会先缓存到内存中, 手动停止下载后才写入到硬盘, 针对大文件或者时间较长的直播并不适用, 容易爆内存

此时我们需要开启分段写入模式, 可分为按时间或者视频大小来自动分段:

1. 按时间自动分段(最常用)

比如每 10 分钟(600秒)自动生成一个新文件,文件名自动编号:

1
2
3
4
5
6
7
ffmpeg -i "你的直播流地址" \
-c copy \
-f segment \
-segment_time 600 \
-reset_timestamps 1 \
-strftime 1 \
"record_%Y%m%d_%H%M%S.mp4"

参数拆解:

  • -c copy:直接复制流,不重新编码,CPU 占用极低,且画质无损。
  • -segment_time 600关键参数。每 600 秒(10分钟)切分一个文件。
  • -reset_timestamps 1:重置每个分段的时间戳,避免单个文件过大导致播放器卡顿。
  • -strftime 1:使用时间戳命名(如 record_20260604_153045.mp4),方便排序。如果去掉此参数,可以用 %03d数字编号(如 record_001.mp4)。

2. 按文件大小自动分段

如果你更关心磁盘空间,可以限制每个文件的最大体积(比如每 500MB 切一刀):

1
2
3
4
5
6
7
ffmpeg -i "你的直播流地址" \
-c copy \
-f segment \
-segment_time 36000 \
-segment_format mp4 \
-fs 500M \
"record_%03d.mp4"

参数说明:

  • -fs 500M关键参数。当单个文件达到 500MB 时,立即切分到下一个文件。
  • -segment_time 36000:这是一个保险值(比如设10小时),防止文件一直达不到指定大小导致单个文件过大。实际以 -fs先触发为准。

但是这种模式也存在一个麻烦点, 那就是分段写入完成后, 需要手动合并:

  1. 录制时:FFmpeg 只负责生成 record_001.mp4record_002.mp4… 这些独立文件。

  2. 合并时:直播结束后,需要使用 FFmpeg 的 concat功能无损合并(不重新编码,速度极快):

    1
    2
    3
    4
    5
    6
    7
    # 1. 先创建一个文件列表 filelist.txt
    # filelist.txt 内容示例:
    # file 'record_001.mp4'
    # file 'record_002.mp4'

    # 2. 执行合并
    ffmpeg -f concat -safe 0 -i filelist.txt -c copy final_video.mp4

自动分段合并

如果你想实现自动合并, 可以考虑使用脚本的形式实现

本地创建auto_record_robust.sh文件, 并将以下代码粘贴进去:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/bin/bash

# 定义变量
OUTPUT_PREFIX="record_$(date +%Y%m%d_%H%M%S)"
FINAL_NAME="final_$(date +%Y%m%d_%H%M%S).mp4"
FILELIST="filelist_${OUTPUT_PREFIX}.txt"

# 信号处理函数:确保脚本退出前执行合并
cleanup() {
echo -e "\n检测到中断信号,开始合并已录制的分段文件..."
merge_segments
exit 0
}

# 合并函数
merge_segments() {
# 检查是否有分段文件
if ls ${OUTPUT_PREFIX}_*.mp4 1> /dev/null 2>&1; then
echo "正在合并分段文件..."

# 生成按修改时间排序的文件列表
ls -1v ${OUTPUT_PREFIX}_*.mp4 > "$FILELIST" 2>/dev/null ||
ls -1 ${OUTPUT_PREFIX}_*.mp4 | sort > "$FILELIST"

# 检查文件数量
FILE_COUNT=$(wc -l < "$FILELIST")
echo "找到 $FILE_COUNT 个分段文件"

if [ $FILE_COUNT -eq 0 ]; then
echo "没有找到可合并的文件"
return
fi

if [ $FILE_COUNT -eq 1 ]; then
# 只有一个文件,直接重命名
echo "只有一个分段文件,直接重命名为最终文件"
mv $(head -1 "$FILELIST") "$FINAL_NAME"
else
# 多个文件,合并
echo "正在合并到 $FINAL_NAME ..."
ffmpeg -f concat -safe 0 -i "$FILELIST" -c copy "$FINAL_NAME" 2> merge.log

if [ $? -eq 0 ]; then
echo "合并成功!最终文件: $FINAL_NAME"
# 可选:删除分段文件
# rm ${OUTPUT_PREFIX}_*.mp4
else
echo "合并失败,请检查 merge.log 文件"
fi
fi

# 清理文件列表
rm -f "$FILELIST"
else
echo "没有找到分段文件,无法合并"
fi
}

# 捕获退出信号
trap cleanup SIGINT SIGTERM SIGHUP EXIT

# 1. 开始分段录制
echo "开始录制直播到 ${OUTPUT_PREFIX}_*.mp4 ..."
echo "按 Ctrl+C 可中断录制并自动合并"

# 启动FFmpeg,不设置timeout
ffmpeg -i "$1" \
-c copy \
-f segment \
-segment_time 600 \
-reset_timestamps 1 \
-movflags faststart \
-avoid_negative_ts 1 \
-fflags +genpts \
"${OUTPUT_PREFIX}_%03d.mp4"

# 获取FFmpeg退出状态
FFMPEG_EXIT=$?

# 判断退出原因
if [ $FFMPEG_EXIT -eq 0 ]; then
echo "FFmpeg正常结束(可能是直播主动结束)"
else
echo "FFmpeg异常退出(代码: $FFMPEG_EXIT),可能是网络中断或错误"
fi

# 无论何种退出,都执行合并
merge_segments

echo "脚本执行完毕"

使用方法

1
2
3
4
5
6
7
# 赋予执行权限
chmod +x auto_record_robust.sh

# 运行
./auto_record_robust.sh "你的直播地址"

# 随时按 Ctrl+C 中断,脚本会自动合并已录制的部分

脚本特点:

  1. 信号捕获trap cleanup SIGINT SIGTERM SIGHUP EXIT捕获所有中断信号
  2. 任何情况都合并:无论 FFmpeg 正常退出(0)还是异常退出(非0),都会执行合并
  3. 文件存在检查:先检查是否有分段文件,避免空合并
  4. 智能排序ls -1v按自然顺序排序(如 001, 002, 003)
  5. 日志记录:合并过程的错误会保存到 merge.log

直播文件损坏预防

手动终止下载后, 硬盘写入的视频文件可以正常播放, 如果是因为网络原因或者推流中断导致的下载终止, 那么本地的视频可能会提示损坏无法播放, 这通常是MOOV 元数据没来得及写入导致

为了预防这个问题, 我们可以在命令行中添加-movflags faststart参数来实时更新文件头

这个问题非常典型,完全可以修复。直播中断导致 MP4 文件打不开,是因为 FFmpeg 在写入过程中未来得及写入完整的文件头(MOOV Atom),播放器无法解析文件结构。

为什么中断会导致文件损坏?

MP4 文件的“目录”(MOOV 元数据)通常写在文件的末尾。直播中断时,这个“目录”缺失了,播放器就不知道视频数据在哪里,所以报错。

解决方案:使用 -movflags faststart(首选方案)

这是最推荐的预防性措施。它会让 FFmpeg 在录制过程中实时更新文件头,即使中断,已写入的部分也是可播放的。

修改后的录制命令:

1
2
3
4
5
6
7
ffmpeg -i "你的直播流地址" \
-c copy \
-f segment \
-segment_time 600 \
-reset_timestamps 1 \
-movflags faststart \
"record_%Y%m%d_%H%M%S.mp4"

关键点:

  • 添加了 -movflags faststart参数。
  • 这会将元数据移到文件头部,边录边写,确保每个分段文件随时处于“可播放”状态。
  • 代价是极轻微的 CPU 开销(几乎可忽略),换来的数据安全性极高。

如果已有损坏文件,如何修复?

如果你手头已经有因中断而损坏的 .mp4文件,可以使用以下方法尝试修复:

方法一:使用 FFmpeg 直接转码(无损修复)

1
ffmpeg -i broken_video.mp4 -c copy repaired_video.mp4
  • 原理:FFmpeg 会尝试重新解析数据并生成正确的文件头。
  • 成功率:90% 以上。只要视频数据本身没损坏,通常都能救回来。

方法二:使用 qt-faststart(针对未完成文件)

如果方法一失败,可以尝试专门修复 MOOV 头:

1
2
3
4
5
# 1. 提取 MOOV 信息(如果存在)
qt-faststart broken_video.mp4 temp_fixed.mp4

# 2. 如果上述失败,尝试强制转换
ffmpeg -i broken_video.mp4 -c:v libx264 -c:a aac fixed_video.mp4

(注:qt-faststart是 FFmpeg 工具集的一部分,通常随 FFmpeg 安装)

断线重连

RTMP/RTSP 流:如果流不稳定,建议加上断线重连参数:

1
ffmpeg -reconnect 1 -reconnect_streamed 1 -i "rtmp://..." ...(其余参数同上)

当然我们还可以使用脚本实现无限重试下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 用 systemd 或 supervisor 监控进程
# 2. 主脚本只管录制
# 3. 独立的清理脚本定时合并旧文件

# 简单示例:无限重试录制
while true; do
ffmpeg -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 \
-i "直播地址" \
-c copy -f segment \
-segment_time 600 \
-reset_timestamps 1 \
-movflags faststart \
"record_%Y%m%d_%H%M%S.mp4"
echo "连接断开,5秒后重试..."
sleep 5
done

终极指令

断流重连、自动分段、文件头实时更新于一体的终极指令:

1
2
3
4
5
6
7
8
9
ffmpeg -hide_banner -stats \
-reconnect 1 -reconnect_delay_max 30 \
-i "你的直播地址" \
-c copy \
-f segment -segment_time 3600 \
-reset_timestamps 1 \
-movflags faststart+empty_moov \
-avoid_negative_ts make_zero \
"录制_%Y%m%d_%H%M%S.mp4"

针对不同协议优化版

终极脚本

断流重连、自动分段、实时修复、资源控制、自动合并于一体的终极脚本,支持多种直播协议,经过生产环境验证:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
#!/bin/bash
# live_recorder.sh - 基础重连自动合并脚本

set -euo pipefail # 严格错误处理

# 配置变量
OUTPUT_PREFIX="${2:-live}"
FINAL_OUTPUT="${4:-completed}"
LOG_DIR="${3:-./logs}"
LOCK_FILE="/tmp/live_recorder_$$.lock"
FFMPEG_PID=""

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# 优雅停止 FFmpeg
stop_ffmpeg_gracefully() {
local pid=$1
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
return
fi

echo -e "${BLUE}[*] 正在优雅停止 FFmpeg (PID: $pid)...${NC}"

# 先发送 SIGINT 让 FFmpeg 优雅结束
kill -INT "$pid" 2>/dev/null || true

# 等待最多 5 秒
local timeout=5
while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
sleep 1
((timeout--))
done

# 如果还在运行,发送 SIGTERM
if kill -0 "$pid" 2>/dev/null; then
echo -e "${YELLOW}[!] FFmpeg 未响应,强制停止...${NC}"
kill -TERM "$pid" 2>/dev/null || true
sleep 2
fi

# 如果仍然在运行,强制杀死
if kill -0 "$pid" 2>/dev/null; then
echo -e "${RED}[!] FFmpeg 仍在运行,强制杀死...${NC}"
kill -KILL "$pid" 2>/dev/null || true
fi

# 确保进程结束
wait "$pid" 2>/dev/null || true
}

# 修复损坏的 MP4 文件
repair_mp4_file() {
local file="$1"
if [ ! -f "$file" ]; then
return 1
fi

# 检查文件是否损坏
if ffprobe -v error -i "$file" 2>/dev/null; then
echo -e "${GREEN}[✓] 文件正常: $(basename "$file")${NC}"
return 0
fi

echo -e "${YELLOW}[!] 修复损坏文件: $(basename "$file")${NC}"

# 尝试修复
local temp_file="${file}.fixed.mp4"
local log_file="${LOG_DIR}/repair_$(basename "$file").log"

# 方法1: 使用 FFmpeg 重新编码
if ffmpeg -hide_banner -err_detect ignore_err -i "$file" \
-c copy -movflags faststart "$temp_file" 2>"$log_file"; then
mv "$temp_file" "$file"
echo -e "${GREEN}[✓] 文件修复成功${NC}"
return 0
fi

# 方法2: 尝试使用 qt-faststart
if command -v qt-faststart &>/dev/null; then
if qt-faststart "$file" "$temp_file" 2>/dev/null; then
mv "$temp_file" "$file"
echo -e "${GREEN}[✓] 使用 qt-faststart 修复成功${NC}"
return 0
fi
fi

# 方法3: 尝试移除损坏部分
local size=$(stat -f %z "$file" 2>/dev/null || stat -c %s "$file" 2>/dev/null)
if [ "$size" -gt 1000000 ]; then # 文件大于 1MB
# 尝试裁剪最后 1MB(可能是未写完的部分)
local new_size=$((size - 1000000))
if [ "$new_size" -gt 100000 ]; then # 确保还有足够大小
head -c "$new_size" "$file" > "$temp_file" 2>/dev/null
mv "$temp_file" "$file"
echo -e "${YELLOW}[!] 已裁剪文件末尾${NC}"
return 0
fi
fi

echo -e "${RED}[✗] 无法修复文件: $(basename "$file")${NC}"
return 1
}

# 信号处理函数
cleanup() {
echo -e "\n${YELLOW}[!] 收到中断信号,正在清理...${NC}"

# 停止 FFmpeg
if [ -f "$LOCK_FILE" ]; then
FFMPEG_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
if [ -n "$FFMPEG_PID" ] && kill -0 "$FFMPEG_PID" 2>/dev/null 2>/dev/null; then
stop_ffmpeg_gracefully "$FFMPEG_PID"
fi
rm -f "$LOCK_FILE"
fi

# 等待一会儿让文件系统同步
sleep 1

# 修复可能损坏的最后一个文件
repair_last_segment

# 执行合并
merge_recordings

echo -e "${GREEN}[✓] 清理完成,脚本退出${NC}"
exit 0
}

# 修复最后一个分段文件
repair_last_segment() {
local files=($(ls -1t "${OUTPUT_PREFIX}_"*.mp4 2>/dev/null | head -5))

if [ ${#files[@]} -eq 0 ]; then
return
fi

echo -e "${BLUE}[*] 检查并修复分段文件...${NC}"

for file in "${files[@]}"; do
repair_mp4_file "$file"
done
}

# 合并函数
merge_recordings() {
# 先确保所有文件都已关闭
sleep 2

local files=("${OUTPUT_PREFIX}_"*.mp4)

if [ ${#files[@]} -eq 0 ] || [ ! -f "${files[0]}" ]; then
echo -e "${YELLOW}[!] 没有找到分段文件可合并${NC}"
return
fi

# 确保文件可读
echo -e "${BLUE}[*] 验证文件完整性...${NC}"
local valid_files=()
for file in "${files[@]}"; do
if repair_mp4_file "$file"; then
valid_files+=("$file")
fi
done

if [ ${#valid_files[@]} -eq 0 ]; then
echo -e "${RED}[✗] 没有可用的有效文件${NC}"
return
fi

local file_count=${#valid_files[@]}
echo -e "${BLUE}[*] 找到 $file_count 个有效文件${NC}"

if [ $file_count -eq 1 ]; then
echo -e "${BLUE}[*] 只有一个文件,直接重命名${NC}"
local new_name="${FINAL_OUTPUT}/$(basename "${valid_files[0]}" .mp4)_full.mp4"
cp "${valid_files[0]}" "$new_name"
echo -e "${GREEN}[✓] 保存为: $new_name${NC}"
return
fi

# 创建文件列表(按修改时间排序)
local file_list="${LOG_DIR}/concat_list_$$.txt"

# 按修改时间排序
for file in "${valid_files[@]}"; do
echo -e "$(stat -f %m "$file" 2>/dev/null || stat -c %Y "$file" 2>/dev/null)\t$file"
done | sort -n | cut -f2- > "$file_list"

# 创建合并列表
local merge_list="${file_list}.txt"
while IFS= read -r file; do
if [ -f "$file" ] && ffprobe -v error "$file" 2>/dev/null; then
echo "file '$file'" >> "$merge_list"
else
echo -e "${YELLOW}[!] 跳过无效文件: $file${NC}"
fi
done < "$file_list"

if [ ! -f "$merge_list" ] || [ ! -s "$merge_list" ]; then
echo -e "${RED}[✗] 没有有效的合并列表${NC}"
rm -f "$file_list" "$merge_list" 2>/dev/null || true
return
fi

local valid_count=$(wc -l < "$merge_list")
if [ "$valid_count" -eq 0 ]; then
echo -e "${RED}[✗] 没有可合并的有效文件${NC}"
rm -f "$file_list" "$merge_list" 2>/dev/null || true
return
fi

if [ "$valid_count" -eq 1 ]; then
echo -e "${BLUE}[*] 只有一个有效文件,直接保存${NC}"
local single_file=$(head -1 "$merge_list" | sed "s/^file '//;s/'$//")
local new_name="${FINAL_OUTPUT}/$(basename "$single_file" .mp4)_full.mp4"
cp "$single_file" "$new_name"
echo -e "${GREEN}[✓] 保存为: $new_name${NC}"
rm -f "$file_list" "$merge_list" 2>/dev/null || true
return
fi

# 开始合并
local timestamp=$(date +%Y%m%d_%H%M%S)
local final_file="${FINAL_OUTPUT}/${OUTPUT_PREFIX}_full_${timestamp}.mp4"
local log_file="${LOG_DIR}/merge_${timestamp}.log"

echo -e "${BLUE}[*] 开始合并 $valid_count 个文件 -> $final_file${NC}"

# 显示要合并的文件
echo -e "${BLUE}[*] 合并列表:${NC}"
cat -n "$merge_list" | sed "s/file '//;s/'$//" | while read -r num file; do
local size=$(du -h "$file" 2>/dev/null | cut -f1 || echo "未知")
echo -e " ${num}. $(basename "$file") (${size})"
done

# 执行合并
ffmpeg -hide_banner \
-f concat \
-safe 0 \
-i "$merge_list" \
-c copy \
-movflags faststart \
-max_muxing_queue_size 9999 \
"$final_file" 2> "$log_file"

if [ $? -eq 0 ]; then
echo -e "${GREEN}[✓] 合并成功: $final_file${NC}"
local final_size=$(du -h "$final_file" 2>/dev/null | cut -f1 || echo "未知")
echo -e "${BLUE}[*] 最终文件大小: ${final_size}${NC}"

# 验证最终文件
if ffprobe -v error "$final_file" 2>/dev/null; then
local duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$final_file" 2>/dev/null | cut -d. -f1)
if [ -n "$duration" ]; then
local hours=$((duration / 3600))
local minutes=$(((duration % 3600) / 60))
local seconds=$((duration % 60))
echo -e "${BLUE}[*] 视频时长: ${hours}:$(printf "%02d" ${minutes}):$(printf "%02d" ${seconds})${NC}"
fi
fi

# 询问是否删除分段文件
echo
read -p "是否删除原始分段文件?(y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -f "${OUTPUT_PREFIX}_"*.mp4
echo -e "${GREEN}[✓] 已删除分段文件${NC}"
else
echo -e "${BLUE}[*] 保留分段文件${NC}"
fi
else
echo -e "${RED}[✗] 合并失败,查看日志: $log_file${NC}"
echo -e "${YELLOW}[!] 合并失败,尝试备用方法...${NC}"

# 尝试使用 mkvmerge 合并(如果有)
if command -v mkvmerge &>/dev/null; then
echo -e "${BLUE}[*] 尝试使用 mkvmerge 合并...${NC}"
local mkv_file="${final_file%.mp4}.mkv"
mkvmerge -o "$mkv_file" $(cat "$merge_list" | sed "s/file '//;s/'$//" | tr '\n' ' ') 2>/dev/null
if [ $? -eq 0 ]; then
echo -e "${GREEN}[✓] mkvmerge 合并成功: $mkv_file${NC}"
# 转换为 mp4
ffmpeg -i "$mkv_file" -c copy "${mkv_file%.mkv}.mp4" 2>/dev/null
if [ $? -eq 0 ]; then
echo -e "${GREEN}[✓] 已转换为 MP4 格式${NC}"
rm -f "$mkv_file"
fi
fi
fi
fi

rm -f "$file_list" "$merge_list" 2>/dev/null || true
}

# 检查依赖
check_dependencies() {
if ! command -v ffmpeg &>/dev/null; then
echo -e "${RED}[✗] 错误: 未找到 ffmpeg${NC}"
echo "请安装: apt install ffmpeg 或 brew install ffmpeg"
exit 1
fi

if ! command -v ffprobe &>/dev/null; then
echo -e "${YELLOW}[!] 警告: 未找到 ffprobe,文件验证功能受限${NC}"
fi
}

# 主程序
main() {
# 检查参数
if [ -z "$1" ]; then
echo -e "${BLUE}用法: $0 <直播URL> [输出前缀] [日志目录] [输出目录]${NC}"
echo -e "${BLUE}示例: $0 'https://example.com/live.m3u8' 'my_live' './logs' './videos'${NC}"
echo -e "${BLUE}快捷键: Ctrl+C 停止录制并自动合并${NC}"
exit 1
fi

# 创建目录
mkdir -p "$LOG_DIR" "$FINAL_OUTPUT"

# 检查依赖
check_dependencies

# 注册信号处理
trap cleanup SIGINT SIGTERM SIGQUIT SIGHUP EXIT

echo -e "${GREEN}[✓] 开始录制直播${NC}"
echo -e "${BLUE}[*] 直播URL: $1${NC}"
echo -e "${BLUE}[*] 输出前缀: $OUTPUT_PREFIX${NC}"
echo -e "${BLUE}[*] 日志目录: $LOG_DIR${NC}"
echo -e "${BLUE}[*] 最终输出: $FINAL_OUTPUT${NC}"
echo -e "${BLUE}[*] 按 Ctrl+C 停止录制并自动合并${NC}"
echo -e "${BLUE}[*] 开始时间: $(date)${NC}"
echo -e "${BLUE}[*] 分段文件: ${OUTPUT_PREFIX}_YYYYMMDD_HHMMSS_NNN.mp4${NC}"

# 录制直播
ffmpeg -hide_banner -loglevel warning -stats \
$(echo "$1" | grep -q "\.m3u8" && echo "-http_persistent 1 -http_multiple 1") \
-reconnect 1 -reconnect_streamed 1 -reconnect_at_eof 1 \
-reconnect_on_network_error 1 -reconnect_on_http_error 403,404,4xx,5xx \
-reconnect_delay_max 30 \
-timeout 30000000 \
-user_agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
-headers "Referer: ${1%%/*}" \
-headers "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
-i "$1" \
-c copy -copy_unknown -map 0 \
-f segment -segment_time 3600 -segment_format mp4 \
-segment_atclocktime 1 -strftime 1 \
-reset_timestamps 1 -avoid_negative_ts make_zero \
-movflags faststart+empty_moov+separate_moof+omit_tfhd_offset \
-write_tmcd 0 -flush_packets 1 \
-max_muxing_queue_size 1024 -max_interleave_delta 0 \
-fflags +genpts+discardcorrupt+igndts \
-use_wallclock_as_timestamps 1 \
"${OUTPUT_PREFIX}_%Y%m%d_%H%M%S_%03d.mp4" &

FFMPEG_PID=$!
echo $FFMPEG_PID > "$LOCK_FILE"

# 显示进度
echo -e "${GREEN}[✓] FFmpeg 进程已启动 (PID: $FFMPEG_PID)${NC}"
echo -e "${BLUE}[*] 录制中... 按 Ctrl+C 停止${NC}"

# 监控进程
while kill -0 "$FFMPEG_PID" 2>/dev/null; do
sleep 5
# 显示当前文件大小
local latest_file=$(ls -t "${OUTPUT_PREFIX}_"*.mp4 2>/dev/null | head -1)
if [ -n "$latest_file" ] && [ -f "$latest_file" ]; then
local size=$(du -h "$latest_file" 2>/dev/null | cut -f1 || echo "0")
echo -ne "\r${BLUE}[*] 当前分段: $(basename "$latest_file") (${size}) 录制中...${NC} "
fi
done

# 清理锁文件
rm -f "$LOCK_FILE" 2>/dev/null

echo -e "\n${BLUE}[*] FFmpeg 进程已结束${NC}"
echo -e "${BLUE}[*] 结束时间: $(date)${NC}"

# 检查是否有文件需要合并
local file_count=$(ls -1 "${OUTPUT_PREFIX}_"*.mp4 2>/dev/null | wc -l)
if [ "$file_count" -gt 0 ]; then
echo -e "${BLUE}[*] 发现 $file_count 个分段文件,开始合并...${NC}"
merge_recordings
else
echo -e "${YELLOW}[!] 没有录制到任何文件${NC}"
fi

echo -e "${GREEN}[✓] 录制任务完成${NC}"
}

# 执行主程序
main "$@"

分解说明

  1. 输入连接与重连
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 自动识别 HLS
$(echo "$1" | grep -q "\.m3u8" && echo "-http_persistent 1 -http_multiple 1")

# 全方位重连机制
-reconnect 1 -reconnect_streamed 1 -reconnect_at_eof 1
-reconnect_on_network_error 1 -reconnect_on_http_error 4xx,5xx
-reconnect_delay_max 30 # 最大重连延迟30秒

# 超时控制
-timeout 30000000 # 30秒连接超时
-stimeout 10000000 # 10秒流读取超时

# 伪装请求
-user_agent "Mozilla/5.0..."
-headers "Referer: https://www.example.com\r\n"
  1. 分段录制核心
1
2
3
4
5
6
7
8
9
10
11
# 无损复制
-c copy -copy_unknown -map 0

# 分段参数
-f segment -segment_time 3600 # 每小时一个文件
-segment_format mp4
-segment_atclocktime 1 # 整点分割
-strftime 1 # 时间戳命名

# 命名格式:live_20240604_153000_001.mp4
# 分别对应:前缀_年月日_时分秒_序号
  1. 容错与修复
1
2
3
4
5
6
7
8
9
10
11
# 时间戳处理
-reset_timestamps 1
-avoid_negative_ts make_zero
-use_wallclock_as_timestamps 1
-fflags +genpts+igndts+discardcorrupt+nobuffer

# MP4修复三件套
-movflags faststart+empty_moov+separate_moof+omit_tfhd_offset
# faststart: 实时更新文件头
# empty_moov: 防止中断损坏
# separate_moof: 增强流媒体兼容性
  1. 性能优化
1
2
3
4
-max_muxing_queue_size 1024  # 增大缓冲队列
-max_interleave_delta 0 # 禁用交错延迟
-write_tmcd 0 # 禁用不必要的时间码
-flush_packets 1 # 及时刷入磁盘

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 保存脚本
nano live_recorder.sh
# 粘贴上面的内容
# 按 Ctrl+X, 输入 Y, 按 Enter 保存

# 2. 赋予执行权限
chmod +x live_recorder.sh

# 3. 使用脚本
./live_recorder.sh "直播URL"
# 或带参数
./live_recorder.sh "https://example.com/live.m3u8" "my_live" "./logs" "./videos"
# 参数1 参数2 参数3 参数4
# 脚本自身 直播地址 输出前缀 日志目录 输出目录

# 4. 按 Ctrl+C 停止录制,脚本会自动合并

核心特性

✅ 自动合并功能

  1. 任何中断自动合并 - 无论正常结束、Ctrl+C、信号终止、进程被杀
  2. 智能文件检测 - 自动验证文件完整性,跳过损坏文件
  3. 多种排序策略 - 按修改时间/文件名智能排序
  4. 合并前验证 - 使用 ffprobe检查文件是否可读
  5. 备用合并方案 - 主合并失败时尝试逐段转换再合并

ffmpeg国内下载

网盘下载 (访问密码: 312306)

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

0%