【python小脚本】摄像头rtsp流转hls m3u8 格式web端播放

写在前面


  • 工作需要,简单整理
  • 实际上这种方式延迟太高了,后来前端直接接的海康的的插件
  • 博文内容为 摄像头 rtsp 实时流转 hls m3u8 的一个 Python 脚本
  • 理解不足小伙伴帮忙指正 😃,生活加油

99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式


摄像头 rtsp 实时流转 hls m3u8 格式 web 端播放

方案介绍:

  • 在服务器上安装并配置 FFmpeg,从 RTSP 摄像头获取实时视频流
  • 使用 FFmpeg并将其转码为 HLS 格式,生成 m3u8 播放列表和 TS 分段文件。
  • 将生成的 HLS 文件托管到 Nginx 服务器的 Web 根目录下,并在 Nginx 配置文件中添加相应的配置,以正确处理 HLS 文件的 MIME 类型和跨域访问等。
  • 在 Web 页面中使用 HTML5 的<video>标签或 HLS.js 库来播放 Nginx 托管的 HLS 视频流。

这里使用的 Nginx 是加载了 rtmp 模块的nginx https://github.com/dreammaker97/nginx-rtmp-win32-dev

rtsp 常见的两个转码方式:

rtsp 转 rtmp

ffmpeg rtsp 2 rtmp

bash 复制代码
ffmpeg.exe -i rtsp://admin:hik12345@10.112.205.103:554/Streaming/Channels/101?transportmode=multicast -acodec aac -strict experimental -ar 44100 -ac 2 -b:a 96k -r 25 -b:v 500k   -c:v libx264 -c:a copy -f flv rtmp://127.0.0.1:1935/live/demo

ffmpeg rtsp 2 hls

rtsp 转 hls

bash 复制代码
ffmpeg -f rtsp -rtsp_transport tcp -i rtsp://admin:hik12345@10.112.205.103:554/Streaming/Channels/101?transportmode=multicast -acodec aac -strict experimental -ar 44100 -ac 2 -b:a 96k -r 25 -b:v 500k -s 640*480  -c:v libx264 -c:a copy -cpu-used 0  -threads 1  -f hls -hls_time 2.0 -hls_list_size 3 -hls_wrap 50 X:\nginx-rtmp-win32-dev\nginx-rtmp-win32-dev\html\hls\test777.m3u8

名词解释:

RTSP 协议: RTSP (Real-Time Streaming Protocol) 是一种用于实时音视频流传输的网络协议,通常用于监控摄像头等设备的实时视频流传输。

HLS 格式: HLS (HTTP Live Streaming) 是苹果公司开发的自适应比特率流式传输协议,可以将视频流转码为 HTTP 可访问的 TS 分段文件和 m3u8 播放列表。HLS 具有良好的跨平台和兼容性。

FFmpeg : FFmpeg 是一个强大的多媒体框架,可以用于音视频的编码、解码、转码等操作。它可以将 RTSP 流转码为 HLS 格式。

Nginx: Nginx 是一款高性能的 Web 服务器,也可作为反向代理服务器使用。它可以托管 HLS 格式的 m3u8 播放列表和 TS 分段文件,为 Web 端提供 HLS 流的访问。

HLS.js: HLS.js 是一款 JavaScript 库,可以在不支持 HLS 原生播放的浏览器上实现 HLS 流的播放。

编码

通过 fastapi 启了一个Web服务,前端获取某个摄像头的流的时候,会启动一个 ffmpeg 子进程来处理流,同时会给前端返回一个 Nginx 推流的 地址

逻辑比较简单,涉及到进程处理,项目启动会自动启动 nginx,当取流时会自动启动 ffmpegnginx 和 ffmpge 都为 当前 Python 服务的子进程,当web 服务死掉,对应子进程全部死掉。

requirements.txt

APScheduler==3.10.4
fastapi==0.111.1
ping3==4.0.8
pyinstaller==6.9.0
pytest==8.3.1
traitlets==5.14.3
uvicorn==0.30.3  

配置文件

yaml 复制代码
# windows 环境配置文件,目录需要修改为 `/` 分割符
ngxin:
  # 启动的推流服务IP,取流的时候使用的IP地址
  nginx_ip : 127.0.0.1 
  # 启动 ng 端口,取流时使用的端口
  nginx_port: 8080
  # 启动的推流服务前缀 
  nginx_fix : /hls/
  # nginx 程序路径,这里不加 `nginx.exe` 实际执行需要跳转到这个目录
  nginx_path: "X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/"
  # nginx 配置文件位置
  nginx_config_path: "X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/conf/nginx.conf"

fastapi:
  # 服务端口
  port: 8991
  # 流存放nginx目录
  hls_dir: "X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/html/hls/"
  # ffmpeg 执行路径
  ffmpeg_dir:  'W:/ffmpeg-20200831-4a11a6f-win64-static/bin/ffmpeg.exe'
  # 最大取流时间
  max_stream_threads : 60
  # 扫描时间
  max_scan_time : 3*60
  # 最大转码数
  max_code_ff_size : 6
  # ffmpeg 转化执行的路径 
  comm: "{ffmpeg_dir} -f rtsp -rtsp_transport tcp -i rtsp://admin:hik12345@{ip}:554/Streaming/Channels/101?transportmode=multicast -acodec aac -strict experimental -ar 44100 -ac 2 -b:a 96k -r 25 -b:v 300k -s {width}*{height} -live 1  -c:v libx264 -c:a copy -cpu-used 0  -threads 1  -f hls -hls_time 0.5 -hls_list_size 1 -hls_wrap 100 {hls_dir}{ip}-{uuid_v}.m3u8"
  

nginx 的配置

config 复制代码
worker_processes  1;

error_log  logs/error.log info;

events {
    worker_connections  1024;
}

http {
    server {
        listen      8080;
		root        html;
        location / {
            root html;
        }
		
        location /stat {
            add_header Access-Control-Allow-Origin *;
            rtmp_stat all;
            rtmp_stat_stylesheet stat.xsl;
        }

        location /stat.xsl {
            root html;
        }
		
        location /hls {
                types {
                   application/vnd.apple.mpegusr m3u8;
                   video/mp2t ts;
                }
                root html;
                # 设置允许跨域的域,* 表示允许任何域,也可以设置特定的域
                add_header 'Access-Control-Allow-Origin' '*';
 
                # 允许的方法
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        
                # 允许的头信息字段
                add_header 'Access-Control-Allow-Headers' 'User-Agent,Keep-Alive,Content-Type';
        
                # 缓存时间
                add_header 'Access-Control-Max-Age' 1728000;
        
                			if ($request_method = 'OPTIONS') {
				add_header 'Access-Control-Allow-Origin' '*';
				add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
				#
				# Custom headers and headers various browsers *should* be OK with but aren't
				#
				add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization';
				#
				# Tell client that this pre-flight info is valid for 20 days
				#
				add_header 'Access-Control-Max-Age' 1728000;
				add_header 'Content-Type' 'text/plain; charset=utf-8';
				add_header 'Content-Length' 0;
				return 200;
			}
			if ($request_method = 'POST') {
				add_header 'Access-Control-Allow-Origin' '*';
				add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
				add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization';
				add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
			}
			if ($request_method = 'GET') {
				add_header 'Access-Control-Allow-Origin' '*';
				add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
				add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization';
				add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
			}
        }
    }
}

脚本:

python 复制代码
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File    :   main.py
@Time    :   2024/07/24 17:20:21
@Author  :   Li Ruilong
@Version :   1.0
@Contact :   liruilonger@gmail.com
@Desc    :   rtmp 转码 到 hls 
"""

# here put the import lib

from apscheduler.schedulers.asyncio import AsyncIOScheduler
import os
import signal
from fastapi import FastAPI, Depends, HTTPException, status, File, UploadFile, Request, Query
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from ping3 import ping, verbose_ping

from datetime import datetime, timezone

from jinja2 import Environment, FileSystemLoader
import uvicorn

import re
import logging
import asyncio
import uuid
import subprocess
import sqlite3
import psutil
import yaml_util
import threading
import datetime
from fastapi.responses import HTMLResponse


env = Environment(loader=FileSystemLoader("templates"))


app = FastAPI()

# 创建定时器
scheduler = AsyncIOScheduler()


# 取流添加锁处理
lock = threading.Lock()


config = yaml_util.get_yaml_config()
nginx = config["ngxin"]
fastapi = config["fastapi"]

locad_id = nginx['nginx_ip']
locad_port = nginx['nginx_port']
locad_fix = nginx['nginx_fix']
nginx_path = nginx['nginx_path']
nginx_config_path = nginx['nginx_config_path']

port = fastapi['port']
hls_dir = fastapi['hls_dir']
ffmpeg_dir = fastapi['ffmpeg_dir']


# 最大取流时间
max_stream_threads = fastapi['max_stream_threads']
# 扫描时间
max_scan_time = fastapi['max_scan_time']
# 最大转码数
max_code_ff_size = fastapi['max_code_ff_size']

comm = fastapi['comm']

# 添加 CORS 中间件 跨域
app.add_middleware(
    CORSMiddleware,
    # 允许跨域的源列表,例如 ["http://www.example.org"] 等等,["*"] 表示允许任何源
    allow_origins=["*"],
    # 跨域请求是否支持 cookie,默认是 False,如果为 True,allow_origins 必须为具体的源,不可以是 ["*"]
    allow_credentials=False,
    # 允许跨域请求的 HTTP 方法列表,默认是 ["GET"]
    allow_methods=["*"],
    # 允许跨域请求的 HTTP 请求头列表,默认是 [],可以使用 ["*"] 表示允许所有的请求头
    # 当然 Accept、Accept-Language、Content-Language 以及 Content-Type 总之被允许的
    allow_headers=["*"],
    # 可以被浏览器访问的响应头, 默认是 [],一般很少指定
    # expose_headers=["*"]
    # 设定浏览器缓存 CORS 响应的最长时间,单位是秒。默认为 600,一般也很少指定
    # max_age=1000
)

chanle = {}


@app.get("/")
async def get_index():
    """
    @Time    :   2024/07/26 14:30:36
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   欢迎页
    """
    return {"status": 200, "message": "Holler  Camera "}


@app.get("/sc_view/get_video_stream")
async def get_video_stream(
    ip: str = Query("192.168.2.25", description="IP地址"),  # 设置默认值为 1
    width: int = Query(320, description=" 流宽度"),  # 设置默认值为 10
    height: int = Query(170, description=" 流高度"),  # 设置默认值为 'name'
):
    """
    @Time    :   2024/07/23 11:04:31
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :    ffmag 解码推流
    """

    if width is None or ip is None or height is None:
        raise HTTPException(status_code=400, detail="参数不能为空")
    import time
    # 获取前端传递的参数
    uuid_v = str(uuid.uuid4())
    if validate_ip_address(ip) is False:
        return {"message": "no validate_ip_address", "code": 600}

    if ping_test(ip) is False:
        return {"message": "ping no pong", "code": 600}
    with lock:
        # 流是否在采集判断
        # dictc = get_process_by_IP("ffmpeg.exe", ip)

        # if len(dictc) != 0:
        #    return dictc[0]

        if ip in chanle:
            return chanle[ip]

        if len(chanle) >= max_code_ff_size:
            return {"status": 400, "message": f"超过最大取流数:{max_code_ff_size}"}

        hls_dir = fastapi['hls_dir']
        ffmpeg_dir = fastapi["ffmpeg_dir"]
        print(vars())
        command = comm.format_map(vars())
        try:
            print(command.strip())
            process = subprocess.Popen(
                command,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
            )
            if process.pid:
                t_d = {
                    "pid": process.pid,
                    "v_url": f'http://{locad_id}:{locad_port}{locad_fix}{ip}-{uuid_v}.m3u8',
                    "ip": ip
                }
                print(t_d)
                print("==============================摄像头数据更新完成...,重新确认子进程是否运行")
                pss = get_process_by_name("ffmpeg.exe", process.pid)
                print("创建的进程为:", pss)
                if len(pss) > 0:
                    chanle[ip] = t_d
                    print(f"返回取流路径为:{t_d}")
                    return t_d
                else:
                    return {"status": 400, "message": "IP 取流失败!,请重新尝试"}
        except subprocess.CalledProcessError as e:
            return {"error": f"Error running ffmpeg: {e}"}


@app.get("/sc_view/stop_video_stream")
async def stop_video_stream(pid: int = Query(2000, description="进程ID")):
    """
    @Time    :   2024/07/24 14:10:43
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   结束推流
    """

    if pid is None:
        raise HTTPException(status_code=400, detail="参数不能为空")

    pss = get_process_by_name("ffmpeg.exe", pid)
    print(pss)
    if len(pss) == 0:
        print("未获取到进程信息", pid)
        return {
            "status": 200,
            "message": "未获取到进程信息"
        }
    print("获取到进程信息:", pss)
    try:
        # 发送 SIGTERM 信号以关闭进程
        os.kill(int(pid), signal.SIGTERM)
        chanle.pop(pid)
        print(f"Process {pid} has been terminated.{str(pss)}")
        return {"status": 200, "message": "关闭成功!"}
    except OSError as e:
        # 调用 kill 命令杀掉
        pss[0].kill()
        print(f"Error terminating process {pid}: {e}")
        return {"status": 200, "message": "关闭成功!"}


@app.get("/sc_view/all_stop_video_stream")
async def all_stop_video_stream():
    """
    @Time    :   2024/07/24 14:10:43
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   批量结束推流
    """
    pss = get_process_by_name("ffmpeg.exe")
    print(pss)
    if len(pss) == 0:
        return {
            "status": 200,
            "message": "转码全部结束"
        }
    print("获取到进程信息:", pss)
    process_list = []
    for p in pss:
        process_list.append({
            "pid": p.info['pid'],
            "name":  p.info['name'],
            "status": p.status(),
            "started": datetime.datetime.fromtimestamp(p.info['create_time']),
            "memory_info": str(p.memory_info().rss / (1024 * 1024)) + " MB",
            "cpu_percent": str(p.cpu_percent()) + " %",
            "cmdline": p.cmdline()
        })
        try:
            # 发送 SIGTERM 信号以关闭进程
            os.kill(int(p.info['pid']), signal.SIGTERM)
            #chanle.pop(p.info['pid'])
            ips =  [ k for k,v in chanle.items() if v.pid == p.info['pid']  ]
            if len(ips) >0:
               chanle.pop(ips[0]) 
            print(f"Process {p.info['pid']} has been terminated.{str(pss)}")
        except OSError as e:
            # 调用 kill 命令杀掉
            pss[0].kill()
            print(f"Error terminating process {p.info['pid']}: {e}")
    return {"status": 200, "message": "关闭成功!", "close_list": process_list}


@app.get("/sc_view/get_video_stream_process_list")
async def get_video_stream_process_list():
    """
    @Time    :   2024/07/24 15:46:38
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   返回当前在采集的流处理进程信息
    """

    pss = get_process_by_name("ffmpeg.exe")
    process_list = []
    for p in pss:
        ip_file = str(p.info['cmdline'][-1]).split("/")[-1]
        process_list.append({
            "pid": p.info['pid'],
            "name":  p.info['name'],
            "status": p.status(),
            "started": datetime.datetime.fromtimestamp(p.info['create_time']),
            "memory_info": str(p.memory_info().rss / (1024 * 1024)) + " MB",
            "cpu_percent": str(p.cpu_percent()) + " %",
            "cmdline": p.cmdline(),
            "v_url":  f'http://{locad_id}:{locad_port}{locad_fix}{ip_file}',
        })
    return {"message": "当前在采集的流信息", "process_list": process_list}


@app.get("/sc_view/get_video_stream_process_live")
async def get_video_stream_process_live(pid: int = Query(2000, description="进程ID")):
    """
    @Time    :   2024/07/24 15:46:38
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   返回当前在采集的流处理进程是否存活
    """
    if pid is None:
        raise HTTPException(status_code=400, detail="参数不能为空")

    pss = get_process_by_name("ffmpeg.exe", pid)
    for p in pss:
        return {"is_running": p.is_running()}

    return {"is_running": False}


@app.get("/sc_view/get_video_player", response_class=HTMLResponse)
async def get_video_player(request: Request):
    """
    @Time    :   2024/07/24 15:46:38
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   返回当前在采集的所有流处理页面
    """

    # pss = get_process_by_IP("ffmpeg.exe")
    # if len(pss) == 0:
    if len(chanle) == 0:
        template = env.get_template("empty_page.html")
        return template.render()
    m3u8_urls = [value['v_url'] for _, value in chanle.items()]
    template = env.get_template("video_player.html")
    return template.render(m3u8_urls=m3u8_urls, request=request)


@scheduler.scheduled_job('interval', seconds=60*60)
async def scan_video_stream_list():
    """
    @Time    :   2024/07/24 16:29:49
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   扫描取流列表,超过最大时间自动结束
    """
    pss = get_process_by_name("ffmpeg.exe")
    return pss


@app.on_event("startup")
async def startup_event():
    scheduler.start()
    # 重启 nginx
    restart_nginx()


@app.on_event("shutdown")
async def shutdown_event():
    scheduler.shutdown()


# 启动 Nginx
def start_nginx():
    """
    @Time    :   2024/07/24 21:13:25
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   启动 nginx
    """
    try:
        os.chdir(nginx_path)
        print("当前执行路径:" + str(nginx_path + "nginx.exe" + " -c " + nginx_config_path))
        subprocess.Popen([nginx_path + "nginx.exe", "-c", nginx_config_path], stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL)
        print("\n===================  Nginx has been started successfully.\n")
    except subprocess.CalledProcessError as e:
        print(f"Failed to start Nginx: {e}")
    finally:
        os.chdir(os.path.dirname(__file__))  # 切换回用户主目录

# 停止 Nginx


def stop_nginx():
    """
    @Time    :   2024/07/24 21:13:41
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   关闭 nginx
    """
    try:
        os.chdir(nginx_path)
        print("当前执行路径:" + str(nginx_path + "nginx.exe" + " -s " + "stop"))
        subprocess.Popen([nginx_path + "nginx.exe", "-s", "stop"], stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL)
        print("\n============  Nginx has been stopped successfully.\n")
    except subprocess.CalledProcessError as e:
        print(f"Failed to stop Nginx: {e}")
    finally:
        os.chdir(os.path.dirname(__file__))  # 切换回用户主目录

# 重启 Nginx


def restart_nginx():
    ns = get_process_by_name("nginx.exe")
    if len(ns) > 0:
        stop_nginx()
    start_nginx()


def get_process_by_name(process_name, pid=None):
    """
    @Time    :   2024/07/24 14:21:31
    @Author  :   liruilonger@gmail.com
    @Version :   1.1
    @Desc    :   获取指定进程名和进程 ID 的进程列表

    Args:
        process_name (str): 进程名称
        pid (int, optional): 进程 ID,默认为 None 表示不筛选 ID

    Returns:
        list: 包含指定进程名和进程 ID 的进程对象的列表
    """

    processes = []
    attrs = ['pid', 'memory_percent', 'name', 'cmdline', 'cpu_times',
             'create_time', 'memory_info', 'status', 'nice', 'username']
    for proc in psutil.process_iter(attrs):
        # print(proc.info['name'])
        try:
            if proc.info['name'] == process_name:
                if pid is None or proc.info['pid'] == pid:
                    processes.append(proc)
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass
    print("Process==================end")
    return processes


def get_process_by_IP(process_name, ip=None):
    """
    @Time    :   2024/07/24 14:21:31
    @Author  :   liruilonger@gmail.com
    @Version :   1.1
    @Desc    :   获取指定进程名和 IP 的进程列表

    Args:
        process_name (str): 进程名称
        pid (int, optional): IP,默认为 None 表示不筛选 IP

    Returns:
        list: 包含指定进程名和进程 IP 的进程对象的列表
    """
    attrs = ['pid', 'memory_percent', 'name', 'cmdline', 'cpu_times',
             'create_time', 'memory_info', 'status', 'nice', 'username']
    press = []
    for proc in psutil.process_iter(attrs):
        try:
            if proc.info['name'] == process_name:

                if ip is None or any(ip in s for s in proc.info['cmdline']):
                    ip_file = str(proc.info['cmdline'][-1]).split("/")[-1]
                    press.append({
                        "pid": proc.info['pid'],
                        "v_url":  f'http://{locad_id}:{locad_port}{locad_fix}{ip_file}',
                        "ip": ip
                    })
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass
    return press


def ping_test(ip_address, timeout=1, count=4):
    """
    @Time    :   2024/07/24 14:08:27
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   Ping 测试
    """

    boo = []
    for i in range(count):
        delay = ping(ip_address, timeout)
        if delay is not None:
            print(f"{ip_address} 在 {delay:.2f} 毫秒内响应")
            boo.append(True)
        else:
            print(f"{ip_address} 无响应")
            boo.append(False)
    return all(boo)


def validate_ip_address(ip_address):
    """
    @Time    :   2024/07/24 09:49:51
    @Author  :   liruilonger@gmail.com
    @Version :   1.0
    @Desc    :   IP  地址校验
    """
    import re
    """
    验证 IP 地址的合法性
    """
    # 定义 IPv4 地址的正则表达式
    ipv4_pattern = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$'

    # 检查 IP 地址是否匹配正则表达式
    match = re.match(ipv4_pattern, ip_address)
    if not match:
        return False

    # 验证每个字段是否在合法范围内
    for i in range(1, 5):
        if int(match.group(i)) < 0 or int(match.group(i)) > 255:
            return False

    return True


if __name__ == "__main__":

    uvicorn.run(app, host="0.0.0.0")

#  uvicorn main:app --reload

解析配置文件脚本

python 复制代码
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
"""
@File    :   yaml_util.py
@Time    :   2022/03/22 14:10:46
@Author  :   Li Ruilong
@Version :   1.0
@Contact :   1224965096@qq.com
@Desc    :   加载配置文件

pip install pyyaml 
"""

# here put the import lib

import os
import time
import yaml
import logging
import json

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')

class Yaml:
    _config = None

    def __new__(cls, *args, **kw):
        # hasattr函数用于判断对象是否包含对应的属性。
        if not hasattr(cls, '_instance'):
            cls._instance = object.__new__(cls)
        return cls._instance

    def __init__(self, file_name="config.yaml"):
        config_temp = None
        try:
            # 获取当前脚本所在文件夹路径
            cur_path = os.path.dirname(os.path.realpath(__file__))
            # 获取yaml文件路径
            yaml_path = os.path.join(cur_path, file_name)

            f = open(yaml_path, 'r', encoding='utf-8')
            config_temp = f.read()
        except Exception as e:
            logging.info("配置文件加载失败", e)
        finally:
            f.close()
        self._config = yaml.safe_load(config_temp)  # 用load方法转化


    def __str__(self):
        return json.dumps(self._config,indent=4)

    def __del__(self):
        self._config = None
        self = None


    @staticmethod
    def get_config(file_name="config.yaml"):
        config = Yaml(file_name)._config
        logging.info("加载配置数据:"+str(config))
        return config

    @staticmethod
    def refresh_config(cls, file_name="config.yaml"):
        del cls
        return Yaml(file_name)._config

def set_config(contain, file_name="config_.yaml"):
    # 配置字典由内存导入静态文件
    cur_path = os.path.dirname(os.path.realpath(__file__))
    yaml_path = os.path.join(cur_path, file_name)
    with  open(yaml_path, 'w', encoding='utf-8') as f:
        yaml.dump(contain, f)

def get_yaml_config(file_name="config.yaml"):
    # 配置文件读入内存为配置字典
    config =  Yaml.get_config(file_name) 
    return config


def refresh_yaml_config(cls, file_name="config.yaml"):
    # 配置文件的动态加载读入内存为字典
    return Yaml.refresh_config(cls,file_name)


if __name__ == '__main__':
    my_yaml_1 = Yaml()
    my_yaml_2 = Yaml()
    #id关键字可用来查看对象在内存中的存放位置
    print(id(my_yaml_1) == id(my_yaml_2))
    time.sleep(10)
    # 修改配置文件后从新加载配置字典会刷新
    refresh_yaml_config(my_yaml_1)

测试的模版,templates 目录下

jijie 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>M3U8 Video Player</title>
    <link href="https://vjs.zencdn.net/7.8.4/video-js.css" rel="stylesheet" />
    <script src="https://vjs.zencdn.net/7.8.4/video.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/videojs-contrib-hls/dist/videojs-contrib-hls.min.js"></script>
    <style>
        .video-container {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
        }
        .video-container > div {
            margin: 10px;
        }
    
    </style>
</head>
<body>
    <div class="video-container">
        {% for url in m3u8_urls %}
        <div class="animated-div" >
            <video id="video-{{ loop.index }}" class="video-js vjs-default-skin" controls>
                <source src="{{ url }}" type="application/x-mpegURL">
            </video>
            <script>
                var player{{ loop.index }} = videojs('video-{{ loop.index }}');
                player{{ loop.index }}.play();
            </script>
        </div>
        {% endfor %}
    </div>
</body>
</html>

打包

bash 复制代码
pyinstaller --add-data "config.yaml;."  --add-data "templates/*;templates"   main.py   

exe 路径

bash 复制代码
rtsp2hls2M3U8\dist\main

配置文件路径

bash 复制代码
rtsp2hls2M3U8\dist\main\_internal

部署测试

bash 复制代码
2024-08-13 15:57:03,404 - win32.py[line:58] - DEBUG: Looking up time zone info from registry
2024-08-13 15:57:03,410 - yaml_util.py[line:62] - INFO: 加载配置数据:{'ngxin': {'nginx_ip': '127.0.0.1', 'nginx_port': 8080, 'nginx_fix': '/hls/', 'nginx_path': 'X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/', 'nginx_config_path': 'X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/conf/nginx.conf'}, 'fastapi': {'port': 8991, 'hls_dir': 'X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/html/hls/', 'ffmpeg_dir': 'W:/ffmpeg-20200831-4a11a6f-win64-static//bin/ffmpeg.exe', 'max_stream_threads': 60, 'max_scan_time': '3*60', 'max_code_ff_size': 6, 'comm': '{ffmpeg_dir} -f rtsp -rtsp_transport tcp -i rtsp://admin:hik12345@{ip}:554/Streaming/Channels/101?transportmode=multicast -acodec aac -strict experimental -ar 44100 -ac 2 -b:a 96k -r 25 -b:v 300k -s {width}*{height} -live 1  -c:v libx264 -c:a copy -cpu-used 0  -threads 1  -f hls -hls_time 0.5 -hls_list_size 1 -hls_wrap 100 {hls_dir}{ip}-{uuid_v}.m3u8'}}
2024-08-13 15:57:03,413 - base.py[line:454] - INFO: Adding job tentatively -- it will be properly scheduled when the scheduler starts
2024-08-13 15:57:03,414 - proactor_events.py[line:630] - DEBUG: Using proactor: IocpProactor
INFO:     Started server process [30404]
INFO:     Waiting for application startup.
2024-08-13 15:57:03,441 - base.py[line:895] - INFO: Added job "scan_video_stream_list" to job store "default"
2024-08-13 15:57:03,441 - base.py[line:181] - INFO: Scheduler started
Process==================end
当前执行路径:X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/nginx.exe -c X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/conf/nginx.conf

===================  Nginx has been started successfully.

2024-08-13 15:57:09,256 - base.py[line:954] - DEBUG: Looking for jobs to run
2024-08-13 15:57:09,256 - base.py[line:1034] - DEBUG: Next wakeup is due at 2024-08-13 16:57:03.413311+08:00 (in 3594.156780 seconds)
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

API 文档:http://127.0.0.1:8000/docs#

测试页面

bash 复制代码
{'ip': '192.168.2.25', 'width': 320, 'height': 170, 'time': <module 'time' (built-in)>, 'uuid_v': 'dbeda9ce-01ec-41cd-8315-8145954d1ea0', 'hls_dir': 'X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/html/hls/', 'ffmpeg_dir': 'W:/ffmpeg-20200831-4a11a6f-win64-static//bin/ffmpeg.exe'}
W:/ffmpeg-20200831-4a11a6f-win64-static//bin/ffmpeg.exe -f rtsp -rtsp_transport tcp -i rtsp://admin:hik12345@192.168.2.25:554/Streaming/Channels/101?transportmode=multicast -acodec aac -strict experimental -ar 44100 -ac 2 -b:a 96k -r 25 -b:v 300k -s 320*170 -live 1  -c:v libx264 -c:a copy -cpu-used 0  -threads 1  -f hls -hls_time 0.5 -hls_list_size 1 -hls_wrap 100 X:/nginx-rtmp-win32-dev/nginx-rtmp-win32-dev/html/hls/192.168.2.25-dbeda9ce-01ec-41cd-8315-8145954d1ea0.m3u8
{'pid': 32416, 'v_url': 'http://127.0.0.1:8080/hls/192.168.2.25-dbeda9ce-01ec-41cd-8315-8145954d1ea0.m3u8', 'ip': '192.168.2.25'}
==============================摄像头数据更新完成...,重新确认子进程是否运行
Process==================end
创建的进程为: [psutil.Process(pid=32416, name='ffmpeg.exe', status='running', started='15:59:38')]
返回取流路径为:{'pid': 32416, 'v_url': 'http://127.0.0.1:8080/hls/192.168.2.25-dbeda9ce-01ec-41cd-8315-8145954d1ea0.m3u8', 'ip': '192.168.2.25'}
INFO:     127.0.0.1:64650 - "GET /sc_view/get_video_stream?ip=192.168.2.25&width=320&height=170 HTTP/1.1" 200 OK

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 😃



© 2018-2024 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

相关推荐
学不会•13 分钟前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
Theodore_10221 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
网易独家音乐人Mike Zhou2 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
安静读书2 小时前
Python解析视频FPS(帧率)、分辨率信息
python·opencv·音视频
活宝小娜3 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点3 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow3 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o3 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
----云烟----3 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024063 小时前
SQL SELECT 语句:基础与进阶应用
开发语言