首页/Python

Python抓取直播视频利用FFmpeg推送直播流到SRS,并通过Nginx开放WebRTC接口,给前端显示播放直播视频

本文介绍如何使用Ubuntu、Docker、SRS、Python、FFmpeg和Nginx搭建低延迟直播系统。Python负责推流,SRS负责把RTMP转换成WebRTC,Nginx负责HTTPS、反向代理和对外接口,前端通过接口获取播放地址并显示直播画面。

一、系统原理

完整链路如下:

RTSP摄像头、USB摄像头或本地视频
              ↓
        Python启动FFmpeg
              ↓
         H.264/AAC编码
              ↓
     RTMP推送到SRS 1935端口
              ↓
       SRS转换成WebRTC
              ↓
Nginx代理WHEP信令和Python开放接口
              ↓
        前端浏览器播放

Nginx只代理HTTPS接口和WebRTC信令,不负责WebRTC媒体编码。浏览器通过Nginx访问WHEP接口获得SDP信息后,视频数据仍然通过SRS的8000/UDP端口传输,因此该UDP端口必须对外开放。

SRS支持通过WHEP播放WebRTC流,其接口使用POST /rtc/v1/whep/,请求内容为SDP,成功时返回HTTP 201和SDP Answer。SRS官方也说明可以使用Nginx提供HTTPS代理。

二、服务器环境

建议环境:

操作系统:Ubuntu 22.04或24.04
服务器配置:2核CPU、2GB内存以上
域名:live.example.com

需要开放:

80/TCP       Nginx HTTP
443/TCP      Nginx HTTPS
1935/TCP     RTMP推流
8000/UDP     WebRTC媒体传输

19858080和Python接口端口只监听本机,不直接向公网开放。

安装基础环境:

sudo apt update
sudo apt install -y docker.io nginx python3 python3-venv ffmpeg
sudo systemctl enable --now docker nginx

三、配置SRS服务器

创建配置文件: #这个配置文件很重要,如果配置错误会影响推送的视频卡顿,延迟,和推送失败等问题

sudo mkdir -p /opt/srs/conf
sudo nano /opt/srs/conf/rtmp2rtc.conf

写入:

listen              1935;
max_connections     1000;
daemon              off;
srs_log_tank        console;

http_server {
    enabled         on;
    listen          8080;
    dir             ./objs/nginx/html;
}

http_api {
    enabled         on;
    listen          1985;
}

stats {
    network         0;
}

rtc_server {
    enabled         on;
    listen          8000;
    candidate       $CANDIDATE;
}

vhost __defaultVhost__ {
    rtc {
        enabled     on;

        # RTMP推流转换为WebRTC
        rtmp_to_rtc on;

        rtc_to_rtmp off;
    }

    http_remux {
        enabled     on;
        mount       [vhost]/[app]/[stream].flv;
    }
}

其中最重要的是:

rtmp_to_rtc on;

如果设置为off,RTMP虽然能够正常推送到SRS,但前端无法通过WebRTC播放。

启动SRS,将公网IP替换为服务器真实IPv4地址:

docker run -d \
  --name srs \
  --restart unless-stopped \
  -e CANDIDATE=服务器公网IP \
  -p 1935:1935 \
  -p 127.0.0.1:1985:1985 \
  -p 127.0.0.1:8080:8080 \
  -p 8000:8000/udp \
  -v /opt/srs/conf/rtmp2rtc.conf:/usr/local/srs/conf/rtmp2rtc.conf:ro \
  ossrs/srs:6 \
  ./objs/srs -c conf/rtmp2rtc.conf

查看运行状态:

docker ps
docker logs -f srs

SRS的candidate必须是浏览器能够访问的公网IP,否则SDP中可能返回错误地址,造成有推流记录但播放黑屏。

四、Python推送直播流

创建push_stream.py

import os
import subprocess
import time

input_url= 视频地址
headers_str = 请求视频的请求头字符串类型
srs_host = "服务器公网IP
stream_name =推送地址名称
rtmp_url= f"rtmp://{srs_host}:1935/live/{stream_name}"
command= ['ffmpeg', '-hide_banner', '-loglevel', 'warning', '-nostdin',

            '-rw_timeout', '15000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5',

            '-headers', headers_str, '-f', 'live_flv', '-i', input_url,

            '-map', '0:v:0', '-map', '0:a:0?', '-dn', '-sn',

            '-c:v', 'copy', '-c:a', 'copy',

            '-flvflags', 'no_duration_filesize', '-f', 'flv', rtmp_url, ]


while True:
    try:
        print("正在推流:", input_url)
        subprocess.run(command, check=True)

    except KeyboardInterrupt:
        print("推流已停止")
        break

    except subprocess.CalledProcessError:
        print("推流中断,3秒后重新连接")
        time.sleep(3)

最终推流地址为

```text
rtmp://服务器公网IP:1935/live/camera1

创建配置:

sudo nano /etc/nginx/sites-available/live.conf

写入:

server {
    listen 80;
    server_name live.example.com;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name live.example.com;

    ssl_certificate /etc/letsencrypt/live/live.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/live.example.com/privkey.pem;

    root /var/www/live;
    index index.html;

    # 前端直播接口
    location /api/live/ {
        proxy_pass http://127.0.0.1:9000;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # WebRTC WHEP信令接口
    location /rtc/ {
        proxy_pass http://127.0.0.1:1985;

        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_request_buffering off;
        proxy_buffering off;
        proxy_read_timeout 3600s;
    }

    # HTTP-FLV备用播放地址
    location /live/ {
        proxy_pass http://127.0.0.1:8080;

        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 3600s;

        add_header Cache-Control "no-cache";
    }

    # 前端页面
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Nginx的proxy_pass可以把外部HTTP请求转发到内部服务,直播响应还应关闭代理缓冲,避免Nginx缓存媒体数据。

启用配置:

sudo ln -s /etc/nginx/sites-available/live.conf \
/etc/nginx/sites-enabled/live.conf

sudo nginx -t
sudo systemctl reload nginx

七、前端调用接口播放WebRTC

浏览器打开搭建视频直播前端,利用flv或者webRTC播放方式搭建前端的:
然后建立WebRTC连接。视频默认静音以满足浏览器自动播放规则,用户可以在播放器中手动打开声音。

确认Python正在推送:

ffplay rtmp://服务器公网IP:1935/live/camera1

也可以测试HTTP-FLV:

https://live.example.com/live/camera1.flv

浏览器不能直接使用原生video标签播放FLV,需要配合flv.js播放器。

公网部署安全

示例接口允许公开访问。正式系统还应增加:

  • JWT或签名鉴权;
  • 播放地址有效期;
  • 推流密钥;
  • Nginx访问频率限制;
  • HTTPS证书;
  • 用户权限检查;
  • 直播流名称白名单。

总结

最终架构并不是使用Nginx代替SRS,而是让Nginx作为统一公网入口:

Python和FFmpeg:采集、编码、推流
SRS:接收RTMP并转换WebRTC
Nginx:HTTPS、反向代理、前端页面
浏览器:调用接口并播放WebRTC

requirements.txt安装的环境模块
spider_qie_video.py,代码样例
ubuntu_install_srs.sh ubuntu服务器安装srs流服务器的命令

RESOURCES

文章资源

代码文件

requirements

下载 · 260.0 B
requirements.txt
loguru
aiohttp
msgpack
beautifulsoup4
fastapi==0.115.6
uvicorn[standard]==0.32.1
httpx==0.27.2
gevent==24.2.1
Flask==3.0.3
ujson
aioredis==2.0.1
redis==5.0.7
jinja2==3.1.3
python-multipart==0.0.6
gunicorn==21.2.0
supervisor==4.2.5
typing-extensions==4.9.0
pytz
代码文件

spider_qie_video

下载 · 8.5 KB
spider_qie_video.py
import random
from datetime import datetime
import threading
import time
import math
import requests
from global_function_construct import definition_function
from config import global_Configuration
from logger_config import CreateLoggerHandler
log_path = f"{global_Configuration['root_path']}/error_file/qie_video.log"
logger_handler = CreateLoggerHandler().setup_logger(log_path=log_path)


class PaserGetQieVideo(definition_function):
    """
    初始化方法,用于设置直播间的相关参数和请求头信息
    参数:
        room_id: 直播间ID,用于构建请求URL
        proxy: 可选的代理服务器地址,用于网络请求
    """
    def __init__(self, procedures_id=2):
        self.projectVideo_id = procedures_id
        self.server_name = 'http://8.129.239.64'
        self.room_form = f"{self.projectVideo_id}" + "_video:{}"
        super().__init__()
        self.redis_server = self.create_connect_redis(True, redis_db=self.redis_database_config['redis_db'], host=self.redis_database_config['redis_host'])
        self.qie_config = self.global_Configuration.get('qie_config')
        self.domainName = self.qie_config.get('domainName')

    # todo 请求获取视频链接
    def requests_get_flv_url(self, room_id, ua=None, ua_version=None, request_session=requests.Session()):
        try:
            t = math.ceil(time.time() * 1000 / 60000 - 1)  # 时间
            result = self.md5_encrypt(f"aWR6ZWly{room_id}RgP7DW01naDYQSV0{t}")
            headers = {
                "accept": "application/json, text/plain, */*",
                "accept-language": "zh-CN,zh;q=0.9,ja;q=0.8",
                "cache-control": "no-cache",
                "pragma": "no-cache",
                "priority": "u=1, i",
                "referer": f"{self.domainName}/{room_id}",
                "sec-ch-ua": f'"Google Chrome";v="{ua_version or self.ua_version}", "Chromium";v="{ua_version or self.ua_version}", "Not.A/Brand";v="24"',
                "sec-ch-ua-mobile": "?0",
                "sec-ch-ua-platform": "\"Linux\"",
                "sec-fetch-dest": "empty",
                "sec-fetch-mode": "cors",
                "sec-fetch-site": "same-origin",
                "token;": "",
                "user-agent": ua or self.ua}
            url = f"{self.domainName}/swf_api/web_room/{room_id}"
            params = {"cdn": "ws", "nofan": "yes", "_t": t, "sign": result}
            response = self.send_requests(request_session, url, headers=headers, params=params, data_type=1, impersonate=False)
            data = response.get('data')
            rtmp_live = data.get('rtmp_live')
            rtmp_url = data.get('rtmp_url')
            flv_url = f"{rtmp_url}/{rtmp_live}"
            if rtmp_live:
                return flv_url
        except Exception as err:
            logger_handler.error(f"requests_get_flv_url >>>>>>获取flv_url失败:{err}")
            print(f"requests_get_flv_url >>>>>>获取flv_url失败:{err}")

    def get_flv_url_configuration(self, room_id):
        ua, ua_version = self.update_ua()
        flv_headers = {
            "accept": "*/*", "accept-language": "zh-CN,zh;q=0.9,ja;q=0.8", "cache-control": "no-cache",
            "origin": self.domainName, "pragma": "no-cache", "priority": "u=1, i",
            "referer": f"{self.domainName}/{room_id}", "sec-ch-ua": f'"Google Chrome";v="{ua_version or self.ua_version}", "Chromium";v="{ua_version or self.ua_version}", "Not.A/Brand";v="24"', "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "\"Linux\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "cross-site", "user-agent": ua or self.ua}
        request_session = requests.Session()
        proxy = self.getProxy(tunnel_mode=self.global_Configuration.get('tunnel_mode'), type_=False, proxy_list=self.proxies_list, logger=logger_handler)
        if proxy:
            request_session.proxies = {'http': proxy, 'https': proxy}
        flv_url = self.requests_get_flv_url(room_id, ua=ua, ua_version=ua_version, request_session=request_session)
        request_session.close()
        return flv_url, flv_headers

    # todo 获取视频链接进行推流
    def flv_pushTheFlow_rtmp(self, room_key, quality=2000):
        video_key = room_key.replace("room", "video")
        try:
            video_data = self.redis_server.hgetall(room_key) or {}
            room_id = video_data.get('videoId')
            server_ip = video_data.get("origin") or self.server_name
            rtmp_url = f"rtmp://{self.server_name.split('//')[-1]}:1935/live/{room_id}"
            for _ in range(5):
                flv_url, flv_headers = self.get_flv_url_configuration(room_id)
                """主函数"""
                if flv_url:

                    cmd_list = self.build_ffmpeg_cmd(rtmp_url, flv_url, flv_headers)  #  配置参数
                    video_dict = {"rtcUrl": f"{server_ip}:1985/rtc/v1/whep/?app=live&stream={room_id}", 'flvUrl': f"{server_ip}/live/{room_id}.flv",
                                  'm3u8Url': f"{server_ip}/live/{room_id}.m3u8", "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "pixels_list": ''}
                    if self.execution_cmd(self.redis_server, video_key, cmd_list, video_dict, logger_handler=logger_handler):
                        self.redis_server.delete(video_key)
                        break
                else:
                    self.video_state.update({video_key: "获取 flv_url 失败"})
                    self.send_email(f"{self.projectVideo_id}--flv_pushTheFlow_rtmp>>>>>>>>>>{str(room_id)}", logger_handler=logger_handler)
            if self.redis_server.exists(room_key):
                self.redis_server.delete(room_key)
        except Exception as err:
            logger_handler.exception(f"flv_pushTheFlow_rtmp>>>{video_key}>>>>>>>>>{err}")
            self.send_email(f"{self.projectVideo_id}--flv_pushTheFlow_rtmp>>>>{video_key}>>>>>>{str(err)}", logger_handler=logger_handler)
            self.video_state.update({video_key: f"flv_pushTheFlow_rtmp>>>{video_key}>>>>>>>>>{str(err)}"})
            self.redis_server.delete(video_key)
            if self.redis_server.exists(room_key):
                self.redis_server.delete(room_key)
        if self.thread_statistics.get(room_key):
            del self.thread_statistics[room_key]
        if self.video_state.get(video_key):
            del self.video_state[video_key]

    # 获取房间id进行推流
    def allocation_pushTheFlow(self, quality=500):
        while True:
            try:
                if not self.check_redis_connection(self.redis_server):  # 判断redis是否断开进行重连接
                    self.redis_server = self.create_connect_redis(True, redis_db=self.redis_database_config['redis_db'], host=self.redis_database_config['redis_host'])
                self.global_Configuration = self.redis_server.get_json("global_Configuration")
                self.qie_config = self.global_Configuration['qie_config']
                self.server_name = self.qie_config['server_name']
                redis_server_keys = self.redis_server.keys(f'{self.projectVideo_id}_room:*')
                for room_key in redis_server_keys:
                    if not self.redis_server.exists(room_key.replace("room", "video")):
                        target = threading.Thread(target=self.flv_pushTheFlow_rtmp, args=(room_key, quality), daemon=True)
                        target.start()
                        self.thread_statistics.update({room_key: target})
                        # target.join()

                self.redis_server.set_json(f"{self.projectVideo_id}:PaserGetQieVideo", {"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "thread_num": len(self.thread_statistics), "thread": list(self.thread_statistics.keys())})
                self.redis_server.set_json(f"{self.projectVideo_id}:GetHuYaVideo_state", {"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "data_num": len(self.video_state), 'data': self.video_state})
                if random.randint(1, 20) in [2, 4, 6, 8]:
                    print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "_______________", redis_server_keys)
            except Exception as err:
                logger_handler.exception(f"allocation_pushTheFlow>>>>>>>>>>{str(err)}")
                self.send_email(f"{self.projectVideo_id}--allocation_pushTheFlow>>>>>>>>>>{str(err)}", logger_handler=logger_handler)
            time.sleep(2)

    # todo 获取视频列表
    def run_main(self):
        self.print_state = True
        self.allocation_pushTheFlow()
        # self.flv_pushTheFlow_rtmp(10207092, "0")


if __name__ == '__main__':
    PaserGetQieVideo().run_main()















































代码文件

ubuntu_install_srs

下载 · 4.7 KB
ubuntu_install_srs.sh







sudo apt update
sudo apt install -y git build-essential pkg-config libssl-dev zlib1g-dev
apt update
apt install -y \
  git \
  unzip \
  automake \
  autoconf \
  libtool \
  build-essential \
  pkg-config \
  cmake \
  libssl-dev \
  zlib1g-dev \
  libpcre3 \
  libpcre3-dev \
  nasm \
  yasm

sudo mkdir -p /usr/local/src
cd /usr/local/src
sudo git clone https://gitee.com/ossrs/srs.git
cd srs
git checkout 7.0release
sudo git tag | grep "v7\|7.0"
sudo git fetch --all
sudo git branch -r | grep -E "7|develop"
sudo git checkout origin/develop
sudo git checkout origin/feature/7.0 2>/dev/null || echo "使用 develop 分支"
sudo git describe --tags --always
cd srs/trunk
sudo ./configure
sudo make -j"$(nproc)"
# 配置定义好路径, 4 “安装”到 /usr/local/srs(便于运维)
sudo mkdir -p /usr/local/srs/{bin,conf,log,run}
sudo install -m 755 ./objs/srs /usr/local/srs/bin/srs

# 设置在srs配置
sudo tee /usr/local/srs/conf/live.conf > /dev/null <<'CONF'
# ============ Global ============
listen              1935;
max_connections     10000;
daemon              off;

pid                 /usr/local/srs/run/srs.pid;
srs_log_tank        file;
srs_log_file        /usr/local/srs/log/srs.log;

# HTTP API: used by WebRTC WHIP/WHEP and other APIs.
http_api {
    enabled         on;
    listen          1985;
    crossdomain     on;
}

# HTTP server: serve HTTP-FLV and HLS files.
http_server {
    enabled         on;
    listen          8080;
    dir             /dev/shm/srs/html;
}

# WebRTC UDP port and candidate IP (public IP).
rtc_server {
    enabled         on;
    listen          8000;
    candidate       $CANDIDATE;
}

# ============ Default VHost ============
vhost __defaultVhost__ {
    enabled         on;

    # Low latency knobs (RTMP/HTTP-FLV)
    tcp_nodelay     on;
    min_latency     on;

    play {
        # For minimum latency, disable GOP cache.
        gop_cache       off;
        queue_length    10;
        mw_latency      100;
    }

    publish {
        # Disable merged-read for lower latency.
        mr              off;
    }

    # HTTP-FLV: remux RTMP to FLV over HTTP.
    http_remux {
        enabled     on;

        # Mount as /live/STREAM.flv (no vhost prefix)
        # This mount style is described in full.conf comments.
        mount       /[app]/[stream].flv;

        # When stream not ready, keep waiting instead of 404.
        hstrs       on;
    }

    # HLS: write segments to RAM (/dev/shm).
    # Low-latency HLS: smaller fragment/window but cannot be <~5s in practice.
    hls {
        enabled         on;
        hls_path        /dev/shm/srs/html;

        # Recommended low-latency baseline (about ~6-8s typical):
        hls_fragment    2;
        hls_window      10;

        hls_cleanup     on;
        hls_wait_keyframe on;
    }

    # WebRTC: convert RTMP -> RTC for ultra-low latency playback.
    rtc {
        enabled         on;
        rtmp_to_rtc     on;
        rtc_to_rtmp     off;

        # RTMP may contain B-frames; WebRTC generally doesn't like it.
        keep_bframe     off;
        bframe          discard;
    }
}
CONF

# 1 系统参数(文件句柄数,防止多人播放时崩)
echo '* soft nofile 100000' | sudo tee -a /etc/security/limits.conf
echo '* hard nofile 100000' | sudo tee -a /etc/security/limits.conf



# 建 service

sudo tee /etc/systemd/system/srs.service > /dev/null <<'SERVICE'
[Unit]
Description=SRS Media Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
Environment=CANDIDATE=39.108.169.242
ExecStart=/usr/local/srs/bin/srs -c /usr/local/srs/conf/live.conf
Restart=always
RestartSec=2
LimitNOFILE=100000

# 如果你未来让 SRS 直接监听 80 端口,可考虑加 CAP_NET_BIND_SERVICE;
# 现在我们用 Nginx 代理到 80/443,所以不需要。

[Install]
WantedBy=multi-user.target
SERVICE

# 启动并设置开机自启
sudo systemctl daemon-reload
sudo systemctl enable --now srs
sudo systemctl status srs --no-pager
tail -n 50 -f /usr/local/srs/log/srs.log




# 安装反向代理nginx
sudo apt install -y nginx
# 配置在反向代理
sudo tee /etc/nginx/sites-available/srs.conf > /dev/null <<'NGINX'
server {
    listen 80;
    server_name _;

    # FLV/HLS/静态页:转到 SRS HTTP 8080
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header Connection "";
        proxy_read_timeout 3600;
    }

    # WebRTC WHEP/WHIP:转到 SRS API 1985
    location /rtc/ {
        proxy_pass http://127.0.0.1:1985/rtc/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_read_timeout 60;
    }
}
NGINX

sudo ln -sf /etc/nginx/sites-available/srs.conf /etc/nginx/sites-enabled/srs.conf
sudo nginx -t && sudo systemctl restart nginx









DISCUSSION

评论交流

还没有公开评论。

评论需要管理员审核后显示。