kodi_thread.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import threading
  2. import time
  3. from typing import Optional
  4. from hardware.kodi_module import KodiClientManager, VideoInfo
  5. from utils.logger_config import logger
  6. class KodiPlayThreadSingleton:
  7. """
  8. 极简播放线程(仅任务):
  9. - 仅在收到新任务时立即播放对应视频一次;
  10. - 按配置的时长等待,期间如收到新的任务则打断并切换;
  11. - 播放结束后不做任何默认播放,进入空闲,继续等待下一任务。
  12. """
  13. _instance = None
  14. _lock = threading.Lock()
  15. def __new__(cls):
  16. if cls._instance is None:
  17. with cls._lock:
  18. if cls._instance is None:
  19. cls._instance = super(KodiPlayThreadSingleton, cls).__new__(cls)
  20. return cls._instance
  21. def __init__(self):
  22. if hasattr(self, "_initialized"):
  23. return
  24. self._initialized = True
  25. self.manager: Optional[KodiClientManager] = None
  26. self.thread: Optional[threading.Thread] = None
  27. self.is_running: bool = False
  28. self._lock_data = threading.Lock()
  29. self._should_stop: bool = False
  30. self._incoming_task_id: Optional[int] = None
  31. self._start_worker_thread()
  32. def _start_worker_thread(self):
  33. if self.thread is None or not self.thread.is_alive():
  34. self.is_running = True
  35. self.thread = threading.Thread(target=self._worker_loop, daemon=True)
  36. self.thread.start()
  37. logger.info("KODI任务播放线程已启动")
  38. def _initialize_manager(self):
  39. if self.manager is None:
  40. self.manager = KodiClientManager()
  41. logger.info("KodiClientManager 初始化成功")
  42. def _lookup_video_info(self, video_id: int) -> Optional[VideoInfo]:
  43. try:
  44. if self.manager is None:
  45. return None
  46. for v in self.manager.video_infos:
  47. if v.id == video_id:
  48. return v
  49. return None
  50. except Exception as e:
  51. logger.error(f"查找视频信息失败: {e}")
  52. return None
  53. def _play_sync_by_video_id(self, video_id: int,loop: bool = False) -> bool:
  54. video_info = self._lookup_video_info(video_id)
  55. if video_info is None:
  56. logger.error(f"无效的视频ID: {video_id}")
  57. return False
  58. try:
  59. self.manager.sync_play_video(self.manager.kodi_clients, video_info.video_path, loop=loop)
  60. logger.info(f"开始同步播放 视频ID={video_id} 路径={video_info.video_path}")
  61. return True
  62. except Exception as e:
  63. logger.error(f"同步播放异常: {e}")
  64. return False
  65. def _play_image_by_url(self, image_url: str,client_index: int) -> bool:
  66. try:
  67. self.manager.play_url_image_on_client(self.manager.kodi_clients[client_index], image_url)
  68. logger.info(f"开始同步播放 图片URL={image_url} 客户端索引={client_index}")
  69. return True
  70. except Exception as e:
  71. logger.error(f"同步播放异常: {e}")
  72. return False
  73. def _play_rtsp_video_by_url(self, rtsp_url: str, client_index: int, volume: int = 0) -> bool:
  74. try:
  75. self.manager.play_rtsp_video_on_client(self.manager.kodi_clients[client_index], rtsp_url, volume=volume)
  76. logger.info(f"开始同步播放 视频URL={rtsp_url} 客户端索引={client_index} 音量={volume}")
  77. return True
  78. except Exception as e:
  79. logger.error(f"同步播放异常: {e}")
  80. return False
  81. def _worker_loop(self):
  82. logger.info("KODI任务播放线程运行中(仅任务,不播放默认)")
  83. self._initialize_manager()
  84. while self.is_running and not self._should_stop:
  85. try:
  86. # 等待直到有新任务
  87. task_id: Optional[int] = None
  88. while not self._should_stop and task_id is None:
  89. with self._lock_data:
  90. if self._incoming_task_id is not None:
  91. task_id = self._incoming_task_id
  92. self._incoming_task_id = None
  93. if task_id is None:
  94. time.sleep(0.05)
  95. if self._should_stop:
  96. break
  97. logger.info(f"[主循环] 接到任务,开始播放 视频ID={task_id}")
  98. if not self._play_sync_by_video_id(task_id):
  99. logger.warning(f"任务 视频ID={task_id} 播放启动失败,跳过")
  100. continue
  101. # 等待任务视频时长,期间若有新任务,则立刻打断并切换到处理新任务
  102. video_info = self._lookup_video_info(task_id)
  103. expected = int(video_info.video_duration) if (video_info and isinstance(video_info.video_duration, int)) else 5
  104. interrupted = self._sleep_with_interrupt(expected)
  105. if interrupted:
  106. logger.info(f"[主循环] 任务ID={task_id} 被新任务/stop打断,立即处理下一任务")
  107. continue
  108. logger.info(f"[主循环] 任务ID={task_id} 播放完毕,线程进入空闲等待下一任务")
  109. time.sleep(0.05)
  110. except Exception as e:
  111. logger.error(f"线程异常: {e}")
  112. time.sleep(0.5)
  113. logger.info("KODI任务播放线程结束")
  114. # 撤销所有客户端的独立状态
  115. def _revoke_individual_state(self):
  116. try:
  117. self.manager.revoke_individual_state()
  118. logger.info("撤销所有客户端的独立状态")
  119. return True
  120. except Exception as e:
  121. logger.error(f"撤销所有客户端的独立状态异常: {e}")
  122. return False
  123. # 启动所有kodi应用程序
  124. def _start_all_kodi_apps(self):
  125. try:
  126. self.manager.start_all_kodi_apps()
  127. logger.info("启动所有kodi应用程序")
  128. return True
  129. except Exception as e:
  130. logger.error(f"启动所有kodi应用程序异常: {e}")
  131. return False
  132. # 全局单例
  133. _kodi_thread = KodiPlayThreadSingleton()
  134. def start_kodi_play(video_id: int) -> bool:
  135. """开始(或切换到)播放指定视频ID(可被新任务打断)。"""
  136. return _kodi_thread._play_sync_by_video_id(video_id)
  137. def stop_kodi_play() -> bool:
  138. """停止播放线程与当前播放。"""
  139. return _kodi_thread.stop()
  140. def is_kodi_thread_running() -> bool:
  141. """线程是否在运行。"""
  142. return _kodi_thread.is_running
  143. def play_image(image_url: str, client_index: int) -> bool:
  144. """在指定的 Kodi 客户端上播放图片(通过URL)。
  145. Args:
  146. image_url: 图片的URL地址
  147. client_index: Kodi客户端索引(从0开始)
  148. Returns:
  149. bool: 是否成功启动播放
  150. """
  151. _kodi_thread._initialize_manager()
  152. if _kodi_thread.manager is None:
  153. logger.error("KodiClientManager 初始化失败")
  154. return False
  155. if client_index < 0 or client_index >= len(_kodi_thread.manager.kodi_clients):
  156. logger.error(f"无效的客户端索引: {client_index},有效范围: 0-{len(_kodi_thread.manager.kodi_clients)-1}")
  157. return False
  158. return _kodi_thread._play_image_by_url(image_url, client_index)
  159. def play_rtsp(rtsp_url: str, client_index: int, volume: int = 0) -> bool:
  160. """在指定的 Kodi 客户端上播放 RTSP 视频流。
  161. Args:
  162. rtsp_url: RTSP视频流的URL地址
  163. client_index: Kodi客户端索引(从0开始)
  164. volume: 播放音量(0-100),默认为0
  165. Returns:
  166. bool: 是否成功启动播放
  167. """
  168. _kodi_thread._initialize_manager()
  169. if _kodi_thread.manager is None:
  170. logger.error("KodiClientManager 初始化失败")
  171. return False
  172. if client_index < 0 or client_index >= len(_kodi_thread.manager.kodi_clients):
  173. logger.error(f"无效的客户端索引: {client_index},有效范围: 0-{len(_kodi_thread.manager.kodi_clients)-1}")
  174. return False
  175. # 确保音量在有效范围内
  176. if volume < 0:
  177. volume = 0
  178. elif volume > 100:
  179. volume = 100
  180. return _kodi_thread._play_rtsp_video_by_url(rtsp_url, client_index, volume)
  181. def revoke_individual_state() -> bool:
  182. """撤销所有客户端的独立状态。
  183. Returns:
  184. bool: 是否成功撤销
  185. """
  186. _kodi_thread._initialize_manager()
  187. if _kodi_thread.manager is None:
  188. logger.error("KodiClientManager 初始化失败")
  189. return False
  190. return _kodi_thread._revoke_individual_state()
  191. def start_all_kodi_apps() -> bool:
  192. """启动所有kodi应用程序。
  193. Returns:
  194. bool: 是否成功启动
  195. """
  196. _kodi_thread._initialize_manager()
  197. if _kodi_thread.manager is None:
  198. logger.error("KodiClientManager 初始化失败")
  199. return False
  200. return _kodi_thread._start_all_kodi_apps()