first commit
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
||||
/target
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
.venv/
|
||||
env/
|
||||
bin/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
include/
|
||||
man/
|
||||
venv/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
pip-selfcheck.json
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Rope
|
||||
.ropeproject
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
*.pot
|
||||
|
||||
.DS_Store
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
# Pyenv
|
||||
.python-version
|
||||
@@ -0,0 +1,14 @@
|
||||
[build-system]
|
||||
build-backend = "hatchling.build"
|
||||
requires = ["hatchling"]
|
||||
|
||||
[project]
|
||||
name = "chongming-stream-manager"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/chongming_stream_manager"]
|
||||
@@ -0,0 +1,130 @@
|
||||
import asyncio
|
||||
from typing import Dict, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class StreamManager:
|
||||
def __init__(self,
|
||||
mediamtx_addr,
|
||||
max_streams: int = 16,
|
||||
video_path: str = ""):
|
||||
self.max_streams = max_streams
|
||||
self._processes: Dict[str, asyncio.subprocess.Process] = {}
|
||||
self._tasks: Dict[str, asyncio.Task] = {}
|
||||
self._semaphore = asyncio.Semaphore(max_streams)
|
||||
self._mediamtx_addr = mediamtx_addr
|
||||
self._video_path = video_path
|
||||
|
||||
def _is_local_file(self, url: str) -> bool:
|
||||
"""判断是否为本地文件(绝对路径或相对路径)"""
|
||||
# 如果以 / 开头,或者包含 .mp4/.avi/.mkv 等,认定为文件
|
||||
return url.startswith('/') or url.startswith('./') or \
|
||||
any(url.lower().endswith(ext) for ext in ['.mp4', '.avi', '.mkv', '.mov', '.flv'])
|
||||
|
||||
def _build_ffmpeg_cmd(self, source_url: str, output_url: str, loop: int = -1) -> list:
|
||||
"""根据源类型构建 ffmpeg 命令"""
|
||||
cmd = ["ffmpeg"]
|
||||
|
||||
if self._is_local_file(source_url):
|
||||
# ----- 本地文件模式(支持循环) -----
|
||||
cmd.extend([
|
||||
"-re", # 按原始帧率读取
|
||||
"-stream_loop", str(loop), # -1 为无限循环
|
||||
"-i", source_url
|
||||
])
|
||||
else:
|
||||
# ----- RTSP 网络流模式(支持自动重连) -----
|
||||
cmd.extend([
|
||||
"-rtsp_transport", "tcp",
|
||||
"-stimeout", "3000000", # 3 秒超时
|
||||
"-reconnect", "1",
|
||||
"-reconnect_streamed", "1",
|
||||
"-reconnect_delay_max", "5",
|
||||
"-i", source_url
|
||||
])
|
||||
|
||||
# 公共输出部分(流复制,推送到 MediaMTX)
|
||||
cmd.extend([
|
||||
"-c", "copy",
|
||||
"-f", "rtsp",
|
||||
output_url
|
||||
])
|
||||
return cmd
|
||||
|
||||
async def start_stream(self, stream_id: str, source_url: str, duration: int = 0, loop: int = -1):
|
||||
if len(self._processes) >= self.max_streams:
|
||||
logger.warning(f"已达最大流上限 {self.max_streams},拒绝新流 {stream_id}")
|
||||
return False
|
||||
|
||||
if stream_id in self._processes:
|
||||
logger.info(f"流 {stream_id} 已存在")
|
||||
return True
|
||||
|
||||
async with self._semaphore:
|
||||
output_url = f"{self._mediamtx_addr}{stream_id}"
|
||||
cmd = self._build_ffmpeg_cmd(source_url, output_url, loop)
|
||||
|
||||
logging.info(f"[Stream {stream_id}] 启动命令: {' '.join(cmd)}")
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
self._processes[stream_id] = process
|
||||
|
||||
task = asyncio.create_task(self._monitor(stream_id, process, duration))
|
||||
self._tasks[stream_id] = task
|
||||
return True
|
||||
|
||||
async def _monitor(self, stream_id: str, process: asyncio.subprocess.Process, duration: int):
|
||||
"""监控单个 ffmpeg 进程的生命周期"""
|
||||
try:
|
||||
if duration > 0:
|
||||
await asyncio.wait_for(process.wait(), timeout=duration)
|
||||
else:
|
||||
await process.wait()
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.info(f"[Stream {stream_id}] 推流超时 ({duration}s), 自动停止")
|
||||
process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=3)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
await process.wait()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Stream {stream_id}] 监控异常: {e}")
|
||||
|
||||
finally:
|
||||
await self._cleanup(stream_id)
|
||||
|
||||
async def stop_stream(self, stream_id: str):
|
||||
"""主动停止一路流"""
|
||||
process = self._processes.get(stream_id)
|
||||
if process:
|
||||
logger.info(f"[Stream {stream_id}] 收到停止指令")
|
||||
process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
await process.wait()
|
||||
await self._cleanup(stream_id)
|
||||
|
||||
async def _cleanup(self, stream_id: str):
|
||||
if stream_id in self._processes:
|
||||
del self._processes[stream_id]
|
||||
if stream_id in self._tasks:
|
||||
task = self._tasks.pop(stream_id)
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
logger.info(f"[Stream {stream_id}] 资源已释放")
|
||||
|
||||
async def stop_all(self):
|
||||
"""停止所有流 (用于应用关闭时) """
|
||||
for sid in list(self._processes.keys()):
|
||||
await self.stop_stream(sid)
|
||||
@@ -0,0 +1,15 @@
|
||||
import asyncio
|
||||
from chongming_stream_manager import StreamManager
|
||||
|
||||
stream_manager = StreamManager("rtsp://172.21.110.223:8554/")
|
||||
|
||||
async def main():
|
||||
await stream_manager.start_stream(
|
||||
"test",
|
||||
"/tmp/bm-app/Upload/Video/VIDEO_02303069-B52F-47BA-A4F3-7DB4C66E087D.mp4"
|
||||
)
|
||||
await asyncio.sleep(120)
|
||||
await stream_manager.stop_all()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user