在浏览器播放多个视频 opencv+Nicegui
效果图

代码:
from nicegui import ui, native
import cv2
import numpy as np
import base64
import time
import threading
import os
class VideoStream:
"""单个视频流管理类"""
def __init__(self, video_source, viewer):
self.video_source = video_source
self.viewer = viewer
self.cap = None
self.is_playing = False
self.target_fps = 10
self.timer = None
# 双缓冲机制
self.current_frame = None
self.next_frame = None
self.frame_ready = False
self.last_frame_time = 0
# 色彩校正参数
self.color_correction_enabled = True
# UI组件
self.image = None
self.start_btn = None
self.stop_btn = None
self.status_label = None
self.color_checkbox = None
# 视频信息
self.is_file = isinstance(video_source, str)
self.video_name = os.path.basename(video_source) if self.is_file else f'摄像头 {video_source}'
# 线程安全锁
self.lock = threading.RLock()
def create_ui(self, parent_container):
"""为单个视频流创建UI组件"""
with parent_container:
with ui.card().classes('w-full h-full flex flex-col'):
header = ui.row().classes('justify-between items-center')
with header:
ui.label(self.video_name).classes('font-semibold')
self.color_checkbox = ui.checkbox('色彩校正', value=True,
on_change=lambda e: setattr(self, 'color_correction_enabled', e.value))
# 视频显示区域
with ui.column().classes('flex-1 justify-center items-center bg-gray-100'):
self.image = ui.interactive_image().classes('max-w-full max-h-[200px]')
# 控制区域
with ui.row().classes('justify-between items-center mt-2'):
control_buttons = ui.row().classes('gap-2')
with control_buttons:
self.start_btn = ui.button('开始', on_click=lambda: self.start())
self.stop_btn = ui.button('停止', on_click=lambda: self.stop())
self.stop_btn.disable()
self.status_label = ui.label('就绪').classes('text-sm')
def start(self):
"""开始播放视频"""
if self.is_playing:
return
with self.lock:
self.cap = cv2.VideoCapture(self.video_source)
# 配置摄像头/视频属性
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
self.cap.set(cv2.CAP_PROP_FPS, self.target_fps)
if not self.cap.isOpened():
if self.is_file:
self.status_label.set_text(f'无法打开视频文件: {self.video_name}')
else:
self.status_label.set_text(f'无法打开摄像头 {self.video_source}')
return
self.is_playing = True
self.start_btn.disable()
self.stop_btn.enable()
self.status_label.set_text('播放中...')
# 使用timer更新视频帧
self.last_frame_time = time.time()
self.timer = ui.timer(interval=1/self.target_fps, callback=self.update_frame)
def stop(self):
"""停止播放视频"""
with self.lock:
self.is_playing = False
if self.timer:
self.timer.cancel()
self.timer = None
if self.cap:
self.cap.release()
self.cap = None
self.start_btn.enable()
self.stop_btn.disable()
self.status_label.set_text('已停止')
# 显示黑色帧
black_frame = np.zeros((480, 640, 3), dtype=np.uint8)
self._update_ui(black_frame)
def update_frame(self):
"""更新视频帧"""
if not self.is_playing or not self.cap or not self.cap.isOpened():
self.stop()
return
# 获取一帧
ret, frame = self.cap.read()
# 对于视频文件,如果播放结束则循环播放
if not ret:
if self.is_file:
# 重新打开视频文件以循环播放
self.cap.release()
self.cap = cv2.VideoCapture(self.video_source)
ret, frame = self.cap.read()
if not ret:
self.status_label.set_text('无法重新加载视频文件')
self.stop()
return
else:
self.status_label.set_text('无法获取视频帧')
self.stop()
return
# 使用双缓冲
with self.lock:
self.next_frame = frame
self.frame_ready = True
# 帧率控制
current_time = time.time()
elapsed = current_time - self.last_frame_time
if elapsed >= 1/self.target_fps:
if self.frame_ready:
with self.lock:
self.current_frame = self.next_frame
self.frame_ready = False
# 更新UI
self._update_ui(self.current_frame)
self.last_frame_time = current_time
def _update_ui(self, frame):
"""更新显示的图像"""
# 调整图像大小
frame = cv2.resize(frame, (640, 480))
# 色彩校正处理
if self.color_correction_enabled:
pass
# 转换为RGB格式
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
else:
# 不进行色彩校正,使用原始BGR格式
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 仍然需要转换为RGB用于显示
# 编码为JPEG
encode_params = [cv2.IMWRITE_JPEG_QUALITY, 90]
_, encoded_image = cv2.imencode('.jpg', frame, encode_params)
# 转换为base64
base64_image = base64.b64encode(encoded_image).decode('utf-8')
# 更新图像
self.image.set_source(f'data:image/jpeg;base64,{base64_image}')
class MultiVideoViewer:
"""多视频文件查看器类"""
def __init__(self, video_sources=[]):
# 如果未提供视频源,使用默认摄像头
self.video_sources = video_sources if video_sources else [0, 1, 2, 3, 4, 5]
self.streams = []
self.create_ui()
def create_ui(self):
"""创建用户界面"""
with ui.header():
ui.label('多视频播放器').classes('text-2xl font-bold')
with ui.row().classes('gap-2 ml-auto'):
ui.button('全部开始', on_click=self.start_all)
ui.button('全部停止', on_click=self.stop_all)
# 主内容区 - 网格布局显示视频
# 根据视频数量动态调整列数
columns = 3
if len(self.video_sources) == 8 or len(self.video_sources) == 9:
columns = 3 if len(self.video_sources) == 9 else 4
self.video_grid = ui.grid(columns=columns).classes('w-full gap-2 p-2')
# 初始化视频流
self.initialize_streams()
def initialize_streams(self):
"""初始化所有视频流"""
# 清空现有流
self.streams.clear()
self.video_grid.clear()
# 创建新的视频流
for source in self.video_sources:
stream = VideoStream(source, self)
stream.create_ui(self.video_grid)
self.streams.append(stream)
def start_all(self):
"""启动所有视频"""
for stream in self.streams:
stream.start()
def stop_all(self):
"""停止所有视频"""
for stream in self.streams:
stream.stop()
# 示例:传入视频文件路径数组
# 请将以下路径替换为您实际的视频文件路径
video_files = [
0, # 视频文件1
'video/2.mp4', # 视频文件2
'video/3.mp4', # 视频文件3
'video/4.mp4', # 视频文件4
'video/5.mp4', # 视频文件5
'video/6.mp4', # 视频文件6
'video/7.mp4', # 视频文件7
'video/8.mp4', # 视频文件8
'video/9.mp4', # 视频文件9
]
# 创建应用
multi_viewer = MultiVideoViewer(video_sources=video_files)
# 运行应用
ui.run(reload=False, port=native.find_open_port())

浙公网安备 33010602011771号