写在前面
- 工作需要,简单整理
- 实际上这种方式延迟太高了,后来前端直接接的海康的的插件
- 博文内容为
摄像头 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
,当取流时会自动启动 ffmpeg
,nginx 和 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)