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

news2024/11/13 15:17:24

写在前面


  • 工作需要,简单整理
  • 实际上这种方式延迟太高了,后来前端直接接的海康的的插件
  • 博文内容为 摄像头 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

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

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  

配置文件


# 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 的配置

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';
			}
        }
    }
}

脚本:

#!/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

解析配置文件脚本

#!/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 目录下

<!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>

打包

pyinstaller --add-data "config.yaml;."  --add-data "templates/*;templates"   main.py   

exe 路径

rtsp2hls2M3U8\dist\main

配置文件路径

rtsp2hls2M3U8\dist\main\_internal

部署测试

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#

在这里插入图片描述

测试页面

{'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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2038127.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【TiDB】07-tiup工具安装及使用

目录 1、在线安装 1.2、脚本代码如下 2、离线安装 2.1、下载安装包 2.2、安装脚本 3、卸载 4、常用命令 TiDB 提供了丰富的工具&#xff0c;可以帮助你进行部署运维、数据管理&#xff08;例如&#xff0c;数据迁移、备份恢复、数据校验&#xff09;、在 TiKV 上运行 S…

意大利Panel触摸屏维修显示屏EBP419 EMBEDDED

意大利Panel触摸屏维修显示屏维修EBP400系列EMBEDDED PC EBP419 panel维修产品主要包括&#xff1a;操作面板,触摸屏,终端机,触摸面板等。应用于包装、食品饮料、纺织与各种工业自动化领域。 panel触摸屏无论是机械工程、装置或单个应用,HMI几乎都可以简化操作,并且通过HMI,还…

大数据技术——DolphinScheduler的集群部署

目录 第1章 DolphinScheduler简介 1.1 DolphinScheduler概述 1.2 DolphinScheduler核心架构 第2章 DolphinScheduler部署说明 2.1 软硬件环境要求 2.1.1 操作系统版本要求 2.1.2 服务器硬件要求 2.2 部署模式 2.2.1 单机模式 2.2.2 伪集群模式 2.2.3 集群模式 第3章…

语音转文字例会纪要有什么好?一分钟掌握5款语音转文字软件

你是否曾在紧张的会议现场&#xff0c;因笔记跟不上发言者语速而焦虑不已&#xff1f;亦或是在采访结束后&#xff0c;面对一堆录音文件感到头大&#xff0c;不知如何迅速整理成可用的文本&#xff1f; 别急&#xff0c;科技的进步总能给我们惊喜。今天&#xff0c;我要为你带…

Golang多版本环境安装并存

1. 准备 请先安装最新版本的Go&#xff0c;详见 https://go.dev/doc/install go version2. 配置镜像加速 go env -w GO111MODULEon go env -w GOPROXYhttps://goproxy.cn,direct3. 安装Go指定版本 Golang官方提供dl工具来实现多版本的Go环境管理&#xff0c;详见项目&#…

带你速通C语言——常量(3)

常 量 在C语言中&#xff0c;常量是一种在程序执行过程中其值不会改变的量。声明常量的方法有两种主要方式&#xff1a;使用const关键字和预处理指令#define。 1.使用 const关键字 当你使用const关键字声明常量时&#xff0c;你需要在声明时立即初始化它&#xff0c;并且在其生…

【强化学习的数学原理】课程:从零开始到透彻理解-30分钟了解强化学习名词脉络

完全零基础的学习强化学习&#xff0c;希望能入门 此图为强化学习的一个脉络图&#xff0c;主要分为两个板块 基础的工具 基本的概念贝尔曼公式贝尔曼最优公式算法和方法 值迭代和策略迭代蒙特卡洛的方法时序差分的方法Value Function ApproximationPolicy Gradient 方法Actor…

糟糕界面集锦-控件篇07

对制作一个图形界面程序的步骤还有疑问吗&#xff1f;在Unisyns Automate Pro 中这个问题特别明显&#xff1a;开发人员显然忘记了TAB 键的作用是在控件之间遍历这个常识。作者使用了开发工具自动生成的控件次序(Tab Order)。程序的界面如下图&#xff0c;需要说明的是&#xf…

二、达梦数据库(DM8)- Django集成

达梦数据库&#xff08;DM8&#xff09;- Django集成 一、集成介绍二、下载驱动包三、Windows1.安装dmPython2.测试dmPython3.安装django-dmPython5.配置数据库信息6.测试 四、Linux1.x86_642.aarch_642.1 安装gcc编译工具2.2 安装dmPython 五、注意事项1.django-授权SYS.SYSOB…

<数据集>怠岗图片数据集<目标检测>

图片数量&#xff1a;2479张 类别数&#xff1a;3 序号类别名称图片数1工作状态9182使用手机2883打瞌睡1273 图片示例&#xff1a; 工作状态&#xff1a; 使用手机&#xff1a; 打瞌睡&#xff1a;

如何减少 Docker 镜像大小:6 种优化方法

如果您想减少docker镜像的大小&#xff0c;您需要使用构建docker镜像的标准最佳实践。 本博客讨论了您可以快速实施的各种优化技术&#xff0c;以制作最小、最精简的 docker 镜像。我们还将介绍一些用于 Docker 镜像优化的最佳工具。 Docker 作为一种容器引擎&#xff0c;可以…

SpringBoot快速入门(手动创建)

目录 案例&#xff1a;需求 步骤 1 创建Maven项目 2 导入SpringBoot起步依赖 3 定义Controller 4 编写引导类 案例&#xff1a;需求 搭建简单的SpringBoot工程&#xff0c;创建hello的类定义h1的方法&#xff0c;返回Hello SpringBoot! 步骤 1 创建Maven项目 大家&…

【Java】文件IO

文章目录 一、什么是文件二、硬盘1. 机械硬盘2. 固态硬盘 三、文件路径四、文件类型 文件 硬盘 系统管理文件&#xff0c;引入一个额专门的模块&#xff0c;“文件系统”&#xff0c;每个文件都有一个“路径”描述文件所在位置 一、什么是文件 此处谈到的文件&#xff0c;本…

SQL Zoo 8+.NSS Tutorial

以下数据来自SQL Zoo 1.at Edinburgh Napier University&#xff0c;studying (8) Computer Science&#xff0c;Show the the percentage who STRONGLY AGREE.&#xff08;在爱丁堡纳皮尔大学&#xff0c;学习“计算机科学”&#xff0c;显示STRONGLY AGREE的百分比&#xff0…

解决navicat隔一段时间连接(操作,查询)就卡顿问题Navicat老是卡死,必须强制退出

解决方法&#xff1a; 打开编辑--->高级--->设置心跳时间30s&#xff08;输入框设置为30 (建议30-240之间&#xff0c;不要超过240)– 点击确定&#xff01;&#xff09; 问题&#xff1a; 使用 Navicat 连接服务器上的数据库时&#xff0c;如果隔一段时间没有使用&…

餐厅管理系统

目录 一、 系统简介 1.1需求分析 餐厅管理系统是一个基于Javaweb的系统&#xff0c;旨在为音乐餐厅、情侣餐厅和中餐厅提供全面的管理和服务功能。系统主要包括餐厅详情管理、价格管理和加入购物车功能。 1.2 编程环境与工具 二、 系统总体设计 2.1 系统的功能模块图…

[log4cplus]: 快速搭建分布式日志系统

关键词: 日志系统 、日志分类、自动分文件夹、按时间(月/周/日/小时/分)轮替 一、引言 这里我默认看此文的我的朋友们都已经具备一定的基础,所以,我们本篇不打算讲关于log4cplus的基础内容,文中如果涉及到没有吃透的点,需要朋友们动动自己聪明的脑袋和发财的手指,进一…

C++进阶中多态的全部主要内容

今天小编和大家一起学习C中多态的全部主要内容&#xff0c;希望今天大家和小编一起学习之后&#xff0c;会对多态有一个初步的了解和使用&#xff0c;好啦&#xff0c;话不多说&#xff0c;开始学习&#xff01;~~~ 一、多态的概念及满足条件 概念&#xff1a;指的就是不同的对…

NC单链表的排序

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 描述 给定一个节点…

九、OpenCVSharp 中的图像形态学操作

文章目录 简介一、腐蚀1. 腐蚀的原理和数学定义2. 结构元素的形状和大小选择3. 腐蚀操作的代码实现和效果展示二、膨胀1. 膨胀的概念和作用2. 与腐蚀的对比和组合使用(如开运算、闭运算)三、开运算1. 开运算的定义和用途(去除小的明亮区域)2. 开运算在去除噪声和分离物体方…