import threading import time from typing import Optional, List, Dict, Any from hardware.kodi_module import KodiClientManager, VideoInfo from utils.logger_config import logger class KodiPlayThreadSingleton: """ 极简播放线程(仅任务): - 仅在收到新任务时立即播放对应视频一次; - 按配置的时长等待,期间如收到新的任务则打断并切换; - 播放结束后不做任何默认播放,进入空闲,继续等待下一任务。 """ _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super(KodiPlayThreadSingleton, cls).__new__(cls) return cls._instance def __init__(self): if hasattr(self, "_initialized"): return self._initialized = True self.manager: Optional[KodiClientManager] = None self.thread: Optional[threading.Thread] = None self.is_running: bool = False self._lock_data = threading.Lock() self._should_stop: bool = False self._incoming_task_id: Optional[int] = None self._incoming_task_volume: Optional[int] = None self._start_worker_thread() def _start_worker_thread(self): if self.thread is None or not self.thread.is_alive(): self.is_running = True self.thread = threading.Thread(target=self._worker_loop, daemon=True) self.thread.start() logger.info("KODI任务播放线程已启动") def _initialize_manager(self): if self.manager is None: self.manager = KodiClientManager() logger.info("KodiClientManager 初始化成功") def _lookup_video_info(self, video_id: int) -> Optional[VideoInfo]: try: if self.manager is None: return None for v in self.manager.video_infos: if v.id == video_id: return v return None except Exception as e: logger.error(f"查找视频信息失败: {e}") return None def _play_sync_by_video_id(self, video_id: int, loop: bool = False, volume: int = -1) -> bool: video_info = self._lookup_video_info(video_id) if video_info is None: logger.error(f"无效的视频ID: {video_id}") return False try: if volume != -1: self.manager.set_volume(volume) self.manager.sync_play_video(self.manager.kodi_clients, video_info.video_path, loop=loop) logger.info(f"开始同步播放 视频ID={video_id} 路径={video_info.video_path} 音量={volume}") return True except Exception as e: logger.error(f"同步播放异常: {e}") return False def _play_image_by_url(self, image_url: str,client_index: int) -> bool: try: self.manager.play_url_image_on_client(self.manager.kodi_clients[client_index], image_url) logger.info(f"开始同步播放 图片URL={image_url} 客户端索引={client_index}") return True except Exception as e: logger.error(f"同步播放异常: {e}") return False def _play_rtsp_video_by_url(self, rtsp_url: str, client_index: int, volume: int = 0) -> bool: try: self.manager.play_rtsp_video_on_client(self.manager.kodi_clients[client_index], rtsp_url, volume=volume) logger.info(f"开始同步播放 视频URL={rtsp_url} 客户端索引={client_index} 音量={volume}") return True except Exception as e: logger.error(f"同步播放异常: {e}") return False def _worker_loop(self): logger.info("KODI任务播放线程运行中(仅任务,不播放默认)") self._initialize_manager() while self.is_running and not self._should_stop: try: # 等待直到有新任务 task_id: Optional[int] = None task_volume: int = -1 while not self._should_stop and task_id is None: with self._lock_data: if self._incoming_task_id is not None: task_id = self._incoming_task_id task_volume = self._incoming_task_volume self._incoming_task_id = None self._incoming_task_volume = None if task_id is None: time.sleep(0.05) if self._should_stop: break logger.info(f"[主循环] 接到任务,开始播放 视频ID={task_id}") if not self._play_sync_by_video_id(task_id, volume=task_volume): logger.warning(f"任务 视频ID={task_id} 播放启动失败,跳过") continue # 等待任务视频时长,期间若有新任务,则立刻打断并切换到处理新任务 video_info = self._lookup_video_info(task_id) expected = int(video_info.video_duration) if (video_info and isinstance(video_info.video_duration, int)) else 5 interrupted = self._sleep_with_interrupt(expected) if interrupted: logger.info(f"[主循环] 任务ID={task_id} 被新任务/stop打断,立即处理下一任务") continue logger.info(f"[主循环] 任务ID={task_id} 播放完毕,线程进入空闲等待下一任务") time.sleep(0.05) except Exception as e: logger.error(f"线程异常: {e}") time.sleep(0.5) logger.info("KODI任务播放线程结束") def _sleep_with_interrupt(self, seconds: int) -> bool: """ 等待 seconds 秒,如果收到新任务或停止信号,则提前返回 True。 否则等待结束返回 False。 """ end_time = time.time() + seconds while time.time() < end_time: if self._should_stop: return True with self._lock_data: if self._incoming_task_id is not None: return True time.sleep(0.1) return False def stop(self) -> bool: self._should_stop = True self.is_running = False return True # 撤销所有客户端的独立状态 def _revoke_individual_state(self): try: self.manager.revoke_individual_state() logger.info("撤销所有客户端的独立状态") return True except Exception as e: logger.error(f"撤销所有客户端的独立状态异常: {e}") return False # 启动所有kodi应用程序 def _start_all_kodi_apps(self): try: self.manager.start_all_kodi_apps() logger.info("启动所有kodi应用程序") return True except Exception as e: logger.error(f"启动所有kodi应用程序异常: {e}") return False def _set_volume(self, volume: int) -> bool: try: self.manager.set_volume(volume) logger.info(f"设置同步播放音量: {volume}") return True except Exception as e: logger.error(f"设置音量异常: {e}") return False # 全局单例 _kodi_thread = KodiPlayThreadSingleton() def start_kodi_play(video_id: int, volume: int = -1) -> bool: """开始(或切换到)播放指定视频ID(可被新任务打断)。""" with _kodi_thread._lock_data: _kodi_thread._incoming_task_id = video_id _kodi_thread._incoming_task_volume = volume # 如果线程挂了,尝试重启 if _kodi_thread.thread is None or not _kodi_thread.thread.is_alive(): _kodi_thread._start_worker_thread() return True def stop_kodi_play() -> bool: """停止播放线程与当前播放。""" return _kodi_thread.stop() def is_kodi_thread_running() -> bool: """线程是否在运行。""" return _kodi_thread.is_running def play_image(image_url: str, client_index: int) -> bool: """在指定的 Kodi 客户端上播放图片(通过URL)。 Args: image_url: 图片的URL地址 client_index: Kodi客户端索引(从0开始) Returns: bool: 是否成功启动播放 """ _kodi_thread._initialize_manager() if _kodi_thread.manager is None: logger.error("KodiClientManager 初始化失败") return False if client_index < 0 or client_index >= len(_kodi_thread.manager.kodi_clients): logger.error(f"无效的客户端索引: {client_index},有效范围: 0-{len(_kodi_thread.manager.kodi_clients)-1}") return False return _kodi_thread._play_image_by_url(image_url, client_index) def play_rtsp(rtsp_url: str, client_index: int, volume: int = 0) -> bool: """在指定的 Kodi 客户端上播放 RTSP 视频流。 Args: rtsp_url: RTSP视频流的URL地址 client_index: Kodi客户端索引(从0开始) volume: 播放音量(0-100),默认为0 Returns: bool: 是否成功启动播放 """ _kodi_thread._initialize_manager() if _kodi_thread.manager is None: logger.error("KodiClientManager 初始化失败") return False if client_index < 0 or client_index >= len(_kodi_thread.manager.kodi_clients): logger.error(f"无效的客户端索引: {client_index},有效范围: 0-{len(_kodi_thread.manager.kodi_clients)-1}") return False # 确保音量在有效范围内 if volume < 0: volume = 0 elif volume > 100: volume = 100 return _kodi_thread._play_rtsp_video_by_url(rtsp_url, client_index, volume) def revoke_individual_state() -> bool: """撤销所有客户端的独立状态。 Returns: bool: 是否成功撤销 """ _kodi_thread._initialize_manager() if _kodi_thread.manager is None: logger.error("KodiClientManager 初始化失败") return False return _kodi_thread._revoke_individual_state() def start_all_kodi_apps() -> bool: """启动所有kodi应用程序。 Returns: bool: 是否成功启动 """ _kodi_thread._initialize_manager() if _kodi_thread.manager is None: logger.error("KodiClientManager 初始化失败") return False return _kodi_thread._start_all_kodi_apps() def set_volume(volume: int) -> bool: """设置同步播放音量 Args: volume: 音量值 (0-100) Returns: bool: 是否成功设置 """ _kodi_thread._initialize_manager() if _kodi_thread.manager is None: logger.error("KodiClientManager 初始化失败") return False # 确保音量在有效范围内 if volume < 0: volume = 0 elif volume > 100: volume = 100 return _kodi_thread._set_volume(volume) def get_kodi_clients() -> List[Dict[str, Any]]: """获取所有 Kodi 客户端列表 Returns: list: 包含客户端信息的字典列表 """ _kodi_thread._initialize_manager() if _kodi_thread.manager is None: return [] clients_list = [] for index, client in enumerate(_kodi_thread.manager.kodi_clients): # 假设 ID 对应 0, 1, 2... # 用户需求:左边计数起算一号电视 (ID: 0 -> 1号电视) display_id = index + 1 name = f"{display_id}号电视 (ID: {client.id}, IP: {client.host})" clients_list.append({ "index": index, "id": client.id, "ip": client.host, "name": name }) return clients_list def get_video_list() -> List[Dict[str, Any]]: """获取所有视频列表 Returns: list: 包含视频信息的字典列表 """ _kodi_thread._initialize_manager() if _kodi_thread.manager is None: return [] videos_list = [] for video in _kodi_thread.manager.video_infos: videos_list.append({ "id": video.id, "name": video.name, "description": video.description, "duration": video.video_duration }) return videos_list