| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- 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
|