docker+ffmpeg+nginx+rtmp 拉取摄像机视频

1、构造程序容器镜像

app.py

bash 复制代码
import subprocess
import json
import time
import multiprocessing
import socket

def check_rtmp_server(host, port, timeout=5):
    try:
        with socket.create_connection((host, port), timeout):
            print(f"RTMP server at {host}:{port} is available.")
            return True
    except Exception as e:
        print(f"RTMP server at {host}:{port} is unavailable: {e}")
        return False

def push_rtsp_to_nginx(rtsp_url, rtmp_url, transport_protocol="tcp"):
    host, port = "127.0.0.1", 1935  # RTMP 服务器地址和端口
    while True:
        if not check_rtmp_server(host, port):
            print("RTMP server not ready. Retrying in 5 seconds...")
            time.sleep(5)
            continue

        print("RTMP server is ready. Waiting 5 seconds before starting the stream...")
        time.sleep(5)  # 等待 RTMP 服务器完全准备好

        ffmpeg_command = [
            "ffmpeg",
            "-loglevel", "quiet",
            "-rtsp_transport", transport_protocol,
            "-i", rtsp_url,
            "-flags", "low_delay",
            "-fflags", "nobuffer",
            "-bufsize", "5000k",
            "-c:v", "copy",
            "-c:a", "copy",
            "-an", 
            "-f", "flv",
            rtmp_url
        ]

        try:
            print(f"Attempting to push stream from {rtsp_url} to {rtmp_url} using {transport_protocol}")
            process = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

            for line in process.stderr:
                print(line.decode('utf-8').strip())

            process.wait()
            print(f"FFmpeg process exited with code {process.returncode}")

        except Exception as e:
            print(f"Error occurred: {e}")

        finally:
            if process:
                process.terminate()
                print("FFmpeg process terminated.")

        print("Retrying in 5 seconds...")
        time.sleep(5)

def monitor_streams(config_path):
    with open(config_path, 'r') as f:
        config = json.load(f)

    processes = []
    for camera in config['cameras']:
        transport_protocol = camera.get('transport', 'tcp')
        p = multiprocessing.Process(
            target=push_rtsp_to_nginx,
            args=(camera['input'], camera['output'], transport_protocol)
        )
        p.start()
        processes.append(p)

    try:
        while True:
            time.sleep(10)
    except KeyboardInterrupt:
        print("Stopping streams...")
        for p in processes:
            p.terminate()
            p.join()

if __name__ == "__main__":
    config_path = "config.json"
    monitor_streams(config_path)

config.json

bash 复制代码
{
  "cameras": [
    {
      "id": 1,
      "input": "rtsp://admin:xxx@10.91.49.251:554/video1",
      "output": "rtmp://127.0.0.1:8080/stream_1/stream_1",
      "transport": "tcp"
    },
    {
      "id": 2,
      "input": "rtsp://admin:xxx@10.91.49.23:554/video1",
      "output": "rtmp://127.0.0.1:8080/stream_2/stream_2",
      "transport": "tcp"
    }
  ]
}

nginx.conf

bash 复制代码
worker_processes auto;  # 可以根据服务器性能调整工作进程数
rtmp_auto_push on;
rtmp_auto_push_reconnect 1s;  # 配置重连间隔,确保流同步快速恢复

# 事件配置
events {
    worker_connections  2048;  # 提高单个进程的最大连接数
    multi_accept on;
    use epoll;  # 如果是 Linux 系统,启用 epoll 提高性能
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    tcp_nopush      on;  # 启用 TCP_NODELAY,减少网络包碎片
    tcp_nodelay     on;
    keepalive_timeout  65;  # 保持连接时间,可以根据需要调整

    server {
        listen 80;
        server_name localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }

        # 提供 HLS 播放服务
        location /hls/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_1/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_2/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_3/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_4/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_5/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }


        # 启用统计页面
        location /stat {
            rtmp_stat all;       # 显示所有流的统计信息
            rtmp_stat_stylesheet stat.xsl;  # 加载 XSL 样式
        }

        # 提供 `stat.xsl` 文件的静态路径
        location /stat.xsl {
            root /usr/local/nginx/html;  # 你可以根据需要修改路径
        }
        
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }
    }
}

# RTMP 配置块,放在 http 配置之外
rtmp {
    server {
        listen 1935;  # RTMP 推流端口
        chunk_size 4096;  # 增加 chunk 大小

        application hls {
            live on;
            record off;
            hls on;
            hls_path /tmp/hls;
            hls_fragment 3s;
        }
    }
    server {
        listen 8080;  # 另外一个 RTMP 推流端口
        chunk_size 4096;  # 增加 chunk 大小

        application stream_1 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_1;
            hls_fragment 3s;
        }
        application stream_2 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_2;
            hls_fragment 3s;
        }
        application stream_3 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_3;
            hls_fragment 3s;
        }
        application stream_4 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_4;
            hls_fragment 3s;
        }
        application stream_5 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_5;
            hls_fragment 3s;
        }
    }
}

entrypoint.sh

bash 复制代码
#!/bin/bash
# 启动 nginx 和其他服务
/usr/local/nginx/sbin/nginx
python3 /app/app.py

dockerfile

bash 复制代码
FROM ubuntu:20.04

# 设置非交互模式避免构建时的交互提示
ENV DEBIAN_FRONTEND=noninteractive

# 更新源并安装依赖
RUN apt-get update && apt-get install -y \
    curl \
    unzip \
    build-essential \
    libpcre3-dev \
    zlib1g-dev \
    libssl-dev \
    python3-pip \
    ffmpeg && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 创建工作目录并复制应用程序
WORKDIR /tmp

# 复制本地的 NGINX 和 RTMP 模块压缩包到容器内
COPY nginx-1.27.3.tar.gz /tmp/nginx-1.27.3.tar.gz
COPY nginx-rtmp.zip /tmp/nginx-rtmp.zip

# 解压并安装 NGINX 和 RTMP 模块
RUN tar zxvf nginx-1.27.3.tar.gz && \
    unzip nginx-rtmp.zip && \
    cd nginx-1.27.3 && \
    ./configure --with-http_ssl_module --add-module=../nginx-rtmp-module-master && \
    make && make install && \
    cd .. && rm -rf nginx-1.27.3 nginx-1.27.3.tar.gz nginx-rtmp.zip nginx-rtmp-module-master

# 安装 Python 库
RUN pip3 install --no-cache-dir ffmpeg-python

# 设置入口脚本
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# 创建工作目录并复制应用程序
WORKDIR /app
COPY app.py /app/app.py
COPY config.json /app/config.json

# 复制 NGINX 配置文件
COPY nginx.conf /usr/local/nginx/conf/nginx.conf

# 复制stat文件
COPY stat.xsl /usr/local/nginx/html/stat.xsl

#设置stat.xsl文件权限
RUN chmod 644 /usr/local/nginx/html/stat.xsl

# 开放端口
EXPOSE 1935 8080 80

# 启动脚本,运行 NGINX 和推流服务
CMD ["/entrypoint.sh"]

另外还需要nginx和nginx-rtmp的包自行下载

构建容器

bash 复制代码
 docker build -t nginx-ffmpeg-server .

2、运行

bash 复制代码
 docker run -d -p 80:80 -p 1935:1935 -p 8080:8080 --name rtmp nginx-ffmpeg-server

另一种方法测试

app.py

bash 复制代码
import av
import json
import requests
import multiprocessing
import time
import xml.etree.ElementTree as ET


def fetch_rtmp_stats(url):
    """
    从 RTMP 统计页面获取统计数据 (XML 格式)
    """
    try:
        response = requests.get(url, timeout=5)
        if response.status_code == 200:
            return ET.fromstring(response.text)
        else:
            print(f"[ERROR] Failed to fetch stats. HTTP {response.status_code}")
            return None
    except Exception as e:
        print(f"[ERROR] Could not fetch RTMP stats: {e}")
        return None


def push_rtsp_to_rtmp(rtsp_url, rtmp_url, transport_protocol="tcp"):
    """
    从 RTSP 拉流并直接推送到 RTMP(无需重新编码)。
    """
    try:
        print(f"[INFO] Starting stream: {rtsp_url} -> {rtmp_url}")
        input_container = av.open(
            rtsp_url,
            options={"rtsp_transport": transport_protocol, "timeout": "5000000"}
        )
        output_container = av.open(rtmp_url, mode="w", format="flv")

        video_stream = None
        audio_stream = None
        for stream in input_container.streams:
            if stream.type == "video" and not video_stream:
                video_stream = output_container.add_stream(template=stream)
            elif stream.type == "audio" and not audio_stream:
                audio_stream = output_container.add_stream(template=stream)

        for packet in input_container.demux(video_stream, audio_stream):
            if packet.dts is None:
                continue
            packet.stream = video_stream if packet.stream.type == "video" else audio_stream
            output_container.mux(packet)

    except Exception as e:
        print(f"[ERROR] Failed to push stream: {rtsp_url} -> {rtmp_url}: {e}")
        raise  # 重新抛出异常,通知调用方进程失败
    finally:
        if 'input_container' in locals():
            input_container.close()
        if 'output_container' in locals():
            output_container.close()
        print(f"[INFO] Stopped pushing stream: {rtsp_url} -> {rtmp_url}")


def check_stream_status(stats_url, stream_name, retries=3, check_interval=5):
    """
    检查指定流的状态,最多尝试 `retries` 次,返回流是否正常
    """
    failure_count = 0
    for _ in range(retries):
        stats = fetch_rtmp_stats(stats_url)
        if stats is None:
            print("[WARNING] Could not fetch RTMP stats. Retrying...")
            failure_count += 1
        else:
            # 查找当前流状态
            stream_nodes = stats.findall(f".//stream[name='{stream_name}']")
            is_active = stream_nodes and all(node.find("active") is not None for node in stream_nodes)
            if is_active:
                print(f"[INFO] Stream {stream_name} is active.")
                return True  # 流正常
            else:
                print(f"[WARNING] Stream {stream_name} is inactive.")
                failure_count += 1

        time.sleep(check_interval)  # 等待几秒后再次尝试

    # 如果检查了所有次数后都失败了,才认为流异常
    if failure_count == retries:
        print(f"[ERROR] Stream {stream_name} is inactive after {retries} checks.")
        return False  # 流不正常

    return True  # 如果至少有一次成功,则认为流正常


def monitor_streams(config_path, stats_url, check_interval=60):
    """
    定期从 /stat 页面获取流状态,如果流异常则尝试恢复已有推流进程,
    不成功则重新启动新推流进程。
    """
    with open(config_path, "r") as f:
        config = json.load(f)

    processes = {}

    # 初始化:先启动所有流的推流进程
    for camera in config.get("cameras", []):
        stream_name = camera["stream"]
        rtsp_url = camera["input"]
        rtmp_url = camera["output"]
        transport_protocol = camera.get("transport", "tcp")

        print(f"[INFO] Starting initial stream for {stream_name}...")
        new_proc = multiprocessing.Process(target=push_rtsp_to_rtmp, args=(rtsp_url, rtmp_url, transport_protocol))
        new_proc.start()
        processes[stream_name] = new_proc

    # 定期检查流状态
    while True:
        for camera in config.get("cameras", []):
            stream_name = camera["stream"]
            rtsp_url = camera["input"]
            rtmp_url = camera["output"]
            transport_protocol = camera.get("transport", "tcp")

            # 检查流状态,如果流异常,则进行处理
            print(f"[INFO] Checking status of stream {stream_name}...")
            if not check_stream_status(stats_url, stream_name):
                print(f"[WARNING] Stream {stream_name} is inactive. Attempting recovery...")

                # 如果进程存在并且存活,尝试恢复推流
                if stream_name in processes:
                    proc = processes[stream_name]
                    if proc.is_alive():
                        print(f"[INFO] Attempting to recover existing process for stream {stream_name}.")
                        try:
                            # 尝试恢复推流并再次检查流状态
                            proc.join(timeout=1)  # 检查现有进程的状态
                        except Exception as e:
                            print(f"[ERROR] Existing process for stream {stream_name} failed: {e}")
                            proc.terminate()  # 终止失败的进程
                            proc.join()
                            print(f"[INFO] Terminated failed process for stream {stream_name}.")
                            proc = None
                        # 再检查两次流状态,如果流仍然异常,则重新启动新进程
                        recovery_attempts = 2
                        for _ in range(recovery_attempts):
                            if check_stream_status(stats_url, stream_name):
                                print(f"[INFO] Stream {stream_name} has recovered.")
                                break
                            print(f"[WARNING] Stream {stream_name} is still inactive after recovery attempt.")
                            time.sleep(5)

                        # 如果仍然失败,则重新启动进程
                        if not check_stream_status(stats_url, stream_name):
                            print(f"[ERROR] Stream {stream_name} could not be recovered. Restarting...")
                            proc = None
                    else:
                        print(f"[INFO] Existing process for stream {stream_name} is not alive. Restarting.")
                        proc = None
                else:
                    proc = None

                # 如果没有存活的进程或进程不可用,启动新的推流进程
                if proc is None:
                    print(f"[INFO] Starting a new process for stream {stream_name}.")
                    new_proc = multiprocessing.Process(target=push_rtsp_to_rtmp, args=(rtsp_url, rtmp_url, transport_protocol))
                    new_proc.start()
                    processes[stream_name] = new_proc
            else:
                print(f"[INFO] Stream {stream_name} is healthy. No action needed.")

        time.sleep(check_interval)


if __name__ == "__main__":
    CONFIG_PATH = "config.json"
    STATS_URL = "http://127.0.0.1/stat"
    monitor_streams(CONFIG_PATH, STATS_URL)

config.json

bash 复制代码
{
  "cameras": [
    {
      "id": 1,
      "stream": "stream_1",
      "input": "rtsp://admin:xxx@10.91.49.251:554/video1?rtsp_transport=tcp",
      "output": "rtmp://127.0.0.1:8080/stream_1/stream_1",
      "transport": "tcp"
    },
    {
      "id": 2,
      "stream": "stream_2",
      "input": "rtsp://admin:xxx@10.91.49.23:554/video1?rtsp_transport=tcp",
      "output": "rtmp://127.0.0.1:8080/stream_2/stream_2",
      "transport": "tcp"
    }
  ]
 }

entrypoint.sh

bash 复制代码
#!/bin/bash
# 启动 NGINX
/usr/local/nginx/sbin/nginx

# 启动 Python 推流服务
python3 /app/app.py

nginx.conf

bash 复制代码
worker_processes auto;  # 可以根据服务器性能调整工作进程数
rtmp_auto_push on;
rtmp_auto_push_reconnect 1s;  # 配置重连间隔,确保流同步快速恢复

# 事件配置
events {
    worker_connections  2048;  # 提高单个进程的最大连接数
    multi_accept on;
    use epoll;  # 如果是 Linux 系统,启用 epoll 提高性能
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    tcp_nopush      on;  # 启用 TCP_NODELAY,减少网络包碎片
    tcp_nodelay     on;
    keepalive_timeout  65;  # 保持连接时间,可以根据需要调整

    server {
        listen 80;
        server_name localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }

        # 提供 HLS 播放服务
        location /hls/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_1/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_2/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_3/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_4/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }

        location /stream_5/ {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;  # 对应 hls_path 根目录
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;  # 允许跨域请求(如果需要)
        }


        # 启用统计页面
        location /stat {
            rtmp_stat all;       # 显示所有流的统计信息
            rtmp_stat_stylesheet stat.xsl;  # 加载 XSL 样式
        }

        # 提供 `stat.xsl` 文件的静态路径
        location /stat.xsl {
            root /usr/local/nginx/html;  # 你可以根据需要修改路径
        }
        
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }
    }
}

# RTMP 配置块,放在 http 配置之外
rtmp {
    server {
        listen 1935;  # RTMP 推流端口
        chunk_size 4096;  # 增加 chunk 大小

        application hls {
            live on;
            record off;
            hls on;
            hls_path /tmp/hls;
            hls_fragment 3s;
        }
    }
    server {
        listen 8080;  # 另外一个 RTMP 推流端口
        chunk_size 4096;  # 增加 chunk 大小

        application stream_1 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_1;
            hls_fragment 3s;
        }
        application stream_2 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_2;
            hls_fragment 3s;
        }
        application stream_3 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_3;
            hls_fragment 3s;
        }
        application stream_4 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_4;
            hls_fragment 3s;
        }
        application stream_5 {
            live on;
            record off;
            hls on;
            hls_path /tmp/stream_5;
            hls_fragment 3s;
        }
    }
}

dockerfile

bash 复制代码
FROM ubuntu:20.04

# 设置非交互模式,避免构建时的交互提示
ENV DEBIAN_FRONTEND=noninteractive

# 更新源并安装依赖
RUN apt-get update && apt-get install -y \
    curl \
    unzip \
    build-essential \
    libpcre3-dev \
    zlib1g-dev \
    libssl-dev \
    python3-pip \
    python3-dev \
    pkg-config \
    libavformat-dev \
    libavcodec-dev \
    libavdevice-dev \
    libavutil-dev \
    libswscale-dev \
    libswresample-dev \
    libopencv-dev && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 创建工作目录并复制 NGINX 和 RTMP 模块压缩包到容器内
WORKDIR /tmp
COPY nginx-1.27.3.tar.gz /tmp/nginx-1.27.3.tar.gz
COPY nginx-rtmp.zip /tmp/nginx-rtmp.zip

# 解压并安装 NGINX 和 RTMP 模块
RUN tar zxvf nginx-1.27.3.tar.gz && \
    unzip nginx-rtmp.zip && \
    cd nginx-1.27.3 && \
    ./configure --with-http_ssl_module --add-module=../nginx-rtmp-module-master && \
    make && make install && \
    cd .. && rm -rf nginx-1.27.3 nginx-1.27.3.tar.gz nginx-rtmp.zip nginx-rtmp-module-master

# 安装 Python 库
RUN pip3 install --no-cache-dir \
    opencv-python-headless \
    av \
    requests \
    numpy

# 设置入口脚本
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# 创建工作目录并复制应用程序
WORKDIR /app
COPY app.py /app/app.py
COPY config.json /app/config.json

# 复制 NGINX 配置文件
COPY nginx.conf /usr/local/nginx/conf/nginx.conf

# 复制 stat 文件
COPY stat.xsl /usr/local/nginx/html/stat.xsl

# 设置 stat.xsl 文件权限
RUN chmod 644 /usr/local/nginx/html/stat.xsl

# 开放端口
EXPOSE 1935 8080 80

# 启动脚本,运行 NGINX 和推流服务
CMD ["/entrypoint.sh"]
相关推荐
namelijink8 小时前
docker-compose部署下Fastapi中使用sqlalchemy和Alembic
adb·docker·fastapi
达子6669 小时前
笔记-使用ffmpeg产生rtsp视频流,然后用进行VLC播放
windows·笔记·ffmpeg
默凉10 小时前
docker GPU安装
docker
田振靓10 小时前
Ubuntu 上安装 Docker
ubuntu·docker
Gavino.10 小时前
记录将springboot的jar包和lib分离,使用docker-compose部署
spring boot·docker·jar
dessler11 小时前
Docker-网络&跨主机通信
linux·运维·docker
cuijiecheng201812 小时前
音视频入门基础:MPEG2-PS专题(5)——FFmpeg源码中,解析PS流中的PES流的实现
ffmpeg·音视频
PyAIGCMaster13 小时前
docker学习记录:创建python环境,使用vscode连接远程docker环境,使其访问同为docker镜像的mongodb的设置。
学习·docker·容器
IT搬砖攻城狮13 小时前
超完整Docker学习记录,Docker常用命令详解
运维·docker·容器·容器化部署