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

效果图
image

代码:

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())
posted @ 2025-10-15 15:57  嘚惹  阅读(16)  评论(0)    收藏  举报