提示

返回博客列表

24 小时自动录制直播——用 FFmpeg + Python 实现无人值守直播录像

24 小时自动录制直播——用 FFmpeg + Python 实现无人值守直播录像

你关注的主播每晚 8 点开播,但你经常加班错过。或者你需要监控一个 24 小时摄像头流,每隔 1 小时存一个文件。直播录制和普通视频下载不一样——你永远不知道直播什么时候结束,流随时可能断开,文件大小不可预测。这篇文章教你用 FFmpeg 和 Python 搭建一套自动直播录制系统:定时检测开播 → 自动开始录制 → 按时间/大小分段存储 → 断开自动重连 → 录制完成通知。

TL;DR:FFmpeg 录制直播流核心命令:ffmpeg -i "直播URL" -c copy -f segment -segment_time 3600 output_%03d.mp4-c copy 不重新编码(CPU 占用极低),-f segment 按时间自动分段。Python 脚本负责:用 yt-dlp 检测直播状态 → 开播后启动 FFmpeg 录制 → 监控 FFmpeg 进程 → 断流自动重连 → 录制完成后触发通知。

目录

一、直播录制和普通下载的区别

普通下载(点播):
  文件大小已知 → Content-Length 有值
  下载一次完成 → 支持断点续传
  失败了重试就好 → 文件不会变

直播录制:
  文件大小未知 → 不知道主播会播多久
  需要持续写入 → 不能"断点续传"
  断了就断了 → 流断了数据就丢了
  实时性要求高 → 不能有大的缓冲延迟

直播的本质是一个永无止境的流。录制工具要做的就是:一直读,一直写,直到你让它停或者流断了。

二、FFmpeg 录制基础命令

2.1 最简录制

# 录制直播流,直接存为 MP4
ffmpeg -i "https://example.com/live/stream.m3u8" \
  -c copy output.mp4

-c copy 是关键——它不重新编码视频和音频,直接复制原始数据。CPU 占用几乎为零,画质无损。录几个小时对系统几乎没有压力。

2.2 常用直播流格式

平台 直播流格式 获取方式
B 站 FLV/HTTP-TS 网页抓包或 API 获取
YouTube HLS (m3u8) yt-dlp 提取
抖音 FLV 抓包获取
快手 FLV 抓包获取
Twitch HLS yt-dlp 提取
RTMP 推流 RTMP 直接连接 RTMP 地址
RTSP 摄像头 RTSP 摄像头 IP 地址

2.3 获取直播流地址

# 用 yt-dlp 获取 YouTube/B站 直播流地址
yt-dlp -g "https://www.youtube.com/watch?v=xxx"
# 输出:https://manifest.googlevideo.com/.../index.m3u8

# 然后把这个地址喂给 FFmpeg
ffmpeg -i "$(yt-dlp -g 'https://www.youtube.com/watch?v=xxx')" \
  -c copy output.mp4

三、自动分段:按时间/大小切割

3.1 按时间分段(推荐)

# 每 30 分钟切一个文件
ffmpeg -i "直播URL" \
  -c copy \
  -f segment \
  -segment_time 1800 \
  -segment_format mp4 \
  -reset_timestamps 1 \
  -strftime 1 \
  "live_%Y%m%d_%H%M%S.mp4"

参数说明:

参数 含义
-f segment 分段输出模式
-segment_time 1800 每段 1800 秒(30 分钟)
-segment_format mp4 分段文件格式
-reset_timestamps 1 每段的时间戳从 0 开始
-strftime 1 文件名使用时间格式

输出效果:

live_20260811_200000.mp4  ← 20:00 开始
live_20260811_203000.mp4  ← 20:30
live_20260811_210000.mp4  ← 21:00
...

3.2 按大小分段

# 每 500MB 切一个文件
ffmpeg -i "直播URL" \
  -c copy \
  -f segment \
  -segment_size 500M \
  -segment_format mp4 \
  "live_part_%03d.mp4"

3.3 同时按时间和大小分段

# 每 30 分钟或 500MB 切一个文件(先到为准)
ffmpeg -i "直播URL" \
  -c copy \
  -f segment \
  -segment_time 1800 \
  -segment_size 500M \
  -segment_format mp4 \
  "live_%03d.mp4"

3.4 为什么推荐分段

1. 文件可控:一个 4 小时直播如果存成单文件,可能 8GB+,分段后每段 1-2GB
2. 不怕中断:FFmpeg 崩溃了,已经写完的分段不受影响
3. 方便处理:分段后可以并行转码、上传、分享
4. 边录边看:已经写完的分段可以直接播放,不用等录制结束

四、Python 自动化脚本

#!/usr/bin/env python3
"""直播自动录制脚本"""

import subprocess
import os
import time
import signal
import json
import requests
from datetime import datetime
from pathlib import Path


class LiveRecorder:
    """直播录制器"""

    def __init__(self, output_dir='./recordings',
                 segment_time=1800, dingtalk_webhook=None):
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.segment_time = segment_time
        self.dingtalk_webhook = dingtalk_webhook
        self.process = None
        self.current_streamer = ''

    def get_stream_url(self, url):
        """获取直播流地址"""
        # 尝试用 yt-dlp 提取
        try:
            result = subprocess.run(
                ['yt-dlp', '-g', url],
                capture_output=True, text=True, timeout=30
            )
            if result.returncode == 0:
                stream_url = result.stdout.strip().split('\n')[0]
                return stream_url
        except Exception:
            pass

        # 如果 yt-dlp 失败,假设传入的已经是直播流地址
        return url

    def check_live_status(self, url):
        """检查是否在直播"""
        try:
            result = subprocess.run(
                ['yt-dlp', '--dump-json', '--playlist-end', '1', url],
                capture_output=True, text=True, timeout=30
            )
            if result.returncode == 0 and result.stdout.strip():
                info = json.loads(result.stdout)
                live_status = info.get('live_status', '')
                return live_status in ('is_live', 'is_upcoming')
        except Exception:
            pass
        return False

    def start_recording(self, stream_url, streamer_name='unknown'):
        """开始录制"""
        self.current_streamer = streamer_name

        # 生成文件名模板
        date_str = datetime.now().strftime('%Y%m%d')
        prefix = f"{streamer_name}_{date_str}"

        output_template = str(
            self.output_dir / f"{prefix}_%Y%m%d_%H%M%S.mp4"
        )

        cmd = [
            'ffmpeg',
            '-i', stream_url,
            '-c', 'copy',
            '-f', 'segment',
            '-segment_time', str(self.segment_time),
            '-segment_format', 'mp4',
            '-reset_timestamps', '1',
            '-strftime', '1',
            '-loglevel', 'warning',
            '-stats',
            output_template,
        ]

        print(f"[{datetime.now()}] 开始录制: {streamer_name}")
        self.notify(f"开始录制: {streamer_name}")

        self.process = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=1,
        )

        # 监控输出(捕获录制时长等信息)
        start_time = time.time()
        for line in self.process.stdout:
            # FFmpeg 的进度信息在 stderr
            pass

        self.process.wait()
        duration = time.time() - start_time

        if self.process.returncode == 0 or self.process.returncode == 255:
            # 0 = 正常退出, 255 = 被信号中断(正常)
            print(f"[{datetime.now()}] 录制结束,持续 {duration/60:.0f} 分钟")
            self.notify(
                f"录制结束: {streamer_name}\n"
                f"持续: {duration/60:.0f} 分钟"
            )
        else:
            print(f"录制异常退出,返回码: {self.process.returncode}")
            self.notify(f"录制异常: {streamer_name} (返回码 {self.process.returncode})")

    def stop_recording(self):
        """停止录制"""
        if self.process and self.process.poll() is None:
            print("正在停止录制...")
            self.process.send_signal(signal.SIGINT)
            try:
                self.process.wait(timeout=10)
            except subprocess.TimeoutExpired:
                self.process.kill()

    def notify(self, message):
        """发送通知"""
        if not self.dingtalk_webhook:
            return
        try:
            requests.post(self.dingtalk_webhook, json={
                'msgtype': 'text',
                'text': {'content': f'[直播录制] {message}'}
            }, timeout=5)
        except Exception:
            pass

    def monitor_and_record(self, url, streamer_name, check_interval=60):
        """循环监控:检测到开播就录制,结束后继续检测"""
        print(f"开始监控: {streamer_name} ({url})")

        while True:
            if self.check_live_status(url):
                # 开播了,获取流地址并开始录制
                try:
                    stream_url = self.get_stream_url(url)
                    self.start_recording(stream_url, streamer_name)
                except Exception as e:
                    print(f"录制出错: {e}")
                    time.sleep(check_interval)
            else:
                print(f"[{datetime.now().strftime('%H:%M')}] 未开播,{check_interval}秒后重试...")
                time.sleep(check_interval)


if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser(description='直播自动录制')
    parser.add_argument('url', help='直播页面 URL 或直播流地址')
    parser.add_argument('--name', default='live', help='主播/频道名称')
    parser.add_argument('--output', default='./recordings', help='输出目录')
    parser.add_argument('--segment', type=int, default=1800, help='分段时长(秒)')
    parser.add_argument('--interval', type=int, default=60, help='检测间隔(秒)')
    parser.add_argument('--webhook', help='钉钉 Webhook 通知地址')

    args = parser.parse_args()

    recorder = LiveRecorder(
        output_dir=args.output,
        segment_time=args.segment,
        dingtalk_webhook=args.webhook,
    )

    recorder.monitor_and_record(
        url=args.url,
        streamer_name=args.name,
        check_interval=args.interval,
    )

五、断流重连与异常处理

直播流断开是常态——主播网络波动、CDN 切换、平台维护都可能导致断流。

5.1 FFmpeg 内置重连

ffmpeg \
  -reconnect 1 \
  -reconnect_streamed 1 \
  -reconnect_delay_max 10 \
  -i "直播URL" \
  -c copy output.mp4
参数 含义
-reconnect 1 断开后自动重连
-reconnect_streamed 1 即使已经开始传输也重连
-reconnect_delay_max 10 最大重连等待 10 秒

5.2 Python 层面的重试

def record_with_retry(self, stream_url, streamer_name, max_retries=10):
    """带重试的录制"""
    for attempt in range(max_retries):
        try:
            self.start_recording(stream_url, streamer_name)
            break  # 正常结束
        except Exception as e:
            print(f"录制中断 (第 {attempt+1}/{max_retries} 次): {e}")

            if attempt < max_retries - 1:
                wait = min(30, 2 ** attempt)
                print(f"等待 {wait} 秒后重试...")
                time.sleep(wait)

                # 重新获取流地址(可能变了)
                try:
                    stream_url = self.get_stream_url(stream_url)
                except Exception:
                    pass

六、监控多个直播间

import threading

def monitor_multiple(config_file):
    """同时监控多个直播间"""
    with open(config_file, 'r', encoding='utf-8') as f:
        configs = json.load(f)

    threads = []
    for cfg in configs:
        recorder = LiveRecorder(
            output_dir=cfg.get('output', './recordings'),
            dingtalk_webhook=cfg.get('webhook'),
        )
        t = threading.Thread(
            target=recorder.monitor_and_record,
            args=(cfg['url'], cfg['name']),
            daemon=True,
        )
        t.start()
        threads.append(t)
        print(f"已启动监控: {cfg['name']}")

    # 保持主线程运行
    for t in threads:
        t.join()
// config.json
[
  {
    "name": "技术主播A",
    "url": "https://www.youtube.com/@streamerA/live",
    "output": "./recordings/streamerA",
    "webhook": "https://oapi.dingtalk.com/robot/send?access_token=xxx"
  },
  {
    "name": "游戏主播B",
    "url": "https://live.bilibili.com/12345",
    "output": "./recordings/streamerB"
  }
]

七、部署到服务器 24 小时运行

7.1 Docker 部署

FROM python:3.11-slim
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN pip install yt-dlp requests
COPY recorder.py .
CMD ["python", "recorder.py", "--config", "/config/config.json"]
# docker-compose.yml
services:
  recorder:
    build: .
    restart: unless-stopped
    volumes:
      - ./recordings:/recordings
      - ./config:/config
    environment:
      - TZ=Asia/Shanghai

7.2 资源消耗

录制一路 1080P 直播(-c copy 模式):
  CPU: ~2%(不重新编码)
  内存: ~50MB
  磁盘: ~1.5GB/小时
  带宽: ~3Mbps(取决于直播源码率)

同时录制 10 路直播:
  CPU: ~15%
  内存: ~500MB
  磁盘: ~15GB/小时

八、合规与温馨提示


直播录制这件事,搭好之后就两个字:省心。我有个树莓派在家里跑了半年,每天自动录几个技术主播的直播,第二天通勤路上看回放。FFmpeg 的 -c copy 模式几乎不消耗 CPU,一个树莓派同时录 4-5 路 1080P 直播毫无压力。

本文由 VidDown 技术博客原创发布。VidDown 桌面客户端内置了直播录制功能——输入直播间链接,自动检测开播状态并开始录制,支持分段存储和断流重连。访问 VidDown 了解更多。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部