kodi_thread.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import threading
  2. import time
  3. from typing import Optional, List, Dict, Any
  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._incoming_task_volume: Optional[int] = None
  32. self._start_worker_thread()
  33. def _start_worker_thread(self):
  34. if self.thread is None or not self.thread.is_alive():
  35. self.is_running = True
  36. self.thread = threading.Thread(target=self._worker_loop, daemon=True)
  37. self.thread.start()
  38. logger.info("KODI任务播放线程已启动")
  39. def _initialize_manager(self):
  40. if self.manager is None:
  41. self.manager = KodiClientManager()
  42. logger.info("KodiClientManager 初始化成功")
  43. def _lookup_video_info(self, video_id: int) -> Optional[VideoInfo]:
  44. try:
  45. if self.manager is None:
  46. return None
  47. for v in self.manager.video_infos:
  48. if v.id == video_id:
  49. return v
  50. return None
  51. except Exception as e:
  52. logger.error(f"查找视频信息失败: {e}")
  53. return None
  54. def _play_sync_by_video_id(self, video_id: int, loop: bool = False, volume: int = -1) -> bool:
  55. video_info = self._lookup_video_info(video_id)
  56. if video_info is None:
  57. logger.error(f"无效的视频ID: {video_id}")
  58. return False
  59. try:
  60. if volume != -1:
  61. self.manager.set_volume(volume)
  62. self.manager.sync_play_video(self.manager.kodi_clients, video_info.video_path, loop=loop)
  63. logger.info(f"开始同步播放 视频ID={video_id} 路径={video_info.video_path} 音量={volume}")
  64. return True
  65. except Exception as e:
  66. logger.error(f"同步播放异常: {e}")
  67. return False
  68. def _play_image_by_url(self, image_url: str,client_index: int) -> bool:
  69. try:
  70. self.manager.play_url_image_on_client(self.manager.kodi_clients[client_index], image_url)
  71. logger.info(f"开始同步播放 图片URL={image_url} 客户端索引={client_index}")
  72. return True
  73. except Exception as e:
  74. logger.error(f"同步播放异常: {e}")
  75. return False
  76. def _play_rtsp_video_by_url(self, rtsp_url: str, client_index: int, volume: int = 0) -> bool:
  77. try:
  78. self.manager.play_rtsp_video_on_client(self.manager.kodi_clients[client_index], rtsp_url, volume=volume)
  79. logger.info(f"开始同步播放 视频URL={rtsp_url} 客户端索引={client_index} 音量={volume}")
  80. return True
  81. except Exception as e:
  82. logger.error(f"同步播放异常: {e}")
  83. return False
  84. def _worker_loop(self):
  85. logger.info("KODI任务播放线程运行中(仅任务,不播放默认)")
  86. self._initialize_manager()
  87. while self.is_running and not self._should_stop:
  88. try:
  89. # 等待直到有新任务
  90. task_id: Optional[int] = None
  91. task_volume: int = -1
  92. while not self._should_stop and task_id is None:
  93. with self._lock_data:
  94. if self._incoming_task_id is not None:
  95. task_id = self._incoming_task_id
  96. task_volume = self._incoming_task_volume
  97. self._incoming_task_id = None
  98. self._incoming_task_volume = None
  99. if task_id is None:
  100. time.sleep(0.05)
  101. if self._should_stop:
  102. break
  103. logger.info(f"[主循环] 接到任务,开始播放 视频ID={task_id}")
  104. if not self._play_sync_by_video_id(task_id, volume=task_volume):
  105. logger.warning(f"任务 视频ID={task_id} 播放启动失败,跳过")
  106. continue
  107. # 等待任务视频时长,期间若有新任务,则立刻打断并切换到处理新任务
  108. video_info = self._lookup_video_info(task_id)
  109. expected = int(video_info.video_duration) if (video_info and isinstance(video_info.video_duration, int)) else 5
  110. interrupted = self._sleep_with_interrupt(expected)
  111. if interrupted:
  112. logger.info(f"[主循环] 任务ID={task_id} 被新任务/stop打断,立即处理下一任务")
  113. continue
  114. logger.info(f"[主循环] 任务ID={task_id} 播放完毕,线程进入空闲等待下一任务")
  115. time.sleep(0.05)
  116. except Exception as e:
  117. logger.error(f"线程异常: {e}")
  118. time.sleep(0.5)
  119. logger.info("KODI任务播放线程结束")
  120. def _sleep_with_interrupt(self, seconds: int) -> bool:
  121. """
  122. 等待 seconds 秒,如果收到新任务或停止信号,则提前返回 True。
  123. 否则等待结束返回 False。
  124. """
  125. end_time = time.time() + seconds
  126. while time.time() < end_time:
  127. if self._should_stop:
  128. return True
  129. with self._lock_data:
  130. if self._incoming_task_id is not None:
  131. return True
  132. time.sleep(0.1)
  133. return False
  134. def stop(self) -> bool:
  135. self._should_stop = True
  136. self.is_running = False
  137. return True
  138. # 撤销所有客户端的独立状态
  139. def _revoke_individual_state(self):
  140. try:
  141. self.manager.revoke_individual_state()
  142. logger.info("撤销所有客户端的独立状态")
  143. return True
  144. except Exception as e:
  145. logger.error(f"撤销所有客户端的独立状态异常: {e}")
  146. return False
  147. # 启动所有kodi应用程序
  148. def _start_all_kodi_apps(self):
  149. try:
  150. self.manager.start_all_kodi_apps()
  151. logger.info("启动所有kodi应用程序")
  152. return True
  153. except Exception as e:
  154. logger.error(f"启动所有kodi应用程序异常: {e}")
  155. return False
  156. def _set_volume(self, volume: int) -> bool:
  157. try:
  158. self.manager.set_volume(volume)
  159. logger.info(f"设置同步播放音量: {volume}")
  160. return True
  161. except Exception as e:
  162. logger.error(f"设置音量异常: {e}")
  163. return False
  164. # 全局单例
  165. _kodi_thread = KodiPlayThreadSingleton()
  166. def start_kodi_play(video_id: int, volume: int = -1) -> bool:
  167. """开始(或切换到)播放指定视频ID(可被新任务打断)。"""
  168. with _kodi_thread._lock_data:
  169. _kodi_thread._incoming_task_id = video_id
  170. _kodi_thread._incoming_task_volume = volume
  171. # 如果线程挂了,尝试重启
  172. if _kodi_thread.thread is None or not _kodi_thread.thread.is_alive():
  173. _kodi_thread._start_worker_thread()
  174. return True
  175. def stop_kodi_play() -> bool:
  176. """停止播放线程与当前播放。"""
  177. return _kodi_thread.stop()
  178. def is_kodi_thread_running() -> bool:
  179. """线程是否在运行。"""
  180. return _kodi_thread.is_running
  181. def play_image(image_url: str, client_index: int) -> bool:
  182. """在指定的 Kodi 客户端上播放图片(通过URL)。
  183. Args:
  184. image_url: 图片的URL地址
  185. client_index: Kodi客户端索引(从0开始)
  186. Returns:
  187. bool: 是否成功启动播放
  188. """
  189. _kodi_thread._initialize_manager()
  190. if _kodi_thread.manager is None:
  191. logger.error("KodiClientManager 初始化失败")
  192. return False
  193. if client_index < 0 or client_index >= len(_kodi_thread.manager.kodi_clients):
  194. logger.error(f"无效的客户端索引: {client_index},有效范围: 0-{len(_kodi_thread.manager.kodi_clients)-1}")
  195. return False
  196. return _kodi_thread._play_image_by_url(image_url, client_index)
  197. def play_rtsp(rtsp_url: str, client_index: int, volume: int = 0) -> bool:
  198. """在指定的 Kodi 客户端上播放 RTSP 视频流。
  199. Args:
  200. rtsp_url: RTSP视频流的URL地址
  201. client_index: Kodi客户端索引(从0开始)
  202. volume: 播放音量(0-100),默认为0
  203. Returns:
  204. bool: 是否成功启动播放
  205. """
  206. _kodi_thread._initialize_manager()
  207. if _kodi_thread.manager is None:
  208. logger.error("KodiClientManager 初始化失败")
  209. return False
  210. if client_index < 0 or client_index >= len(_kodi_thread.manager.kodi_clients):
  211. logger.error(f"无效的客户端索引: {client_index},有效范围: 0-{len(_kodi_thread.manager.kodi_clients)-1}")
  212. return False
  213. # 确保音量在有效范围内
  214. if volume < 0:
  215. volume = 0
  216. elif volume > 100:
  217. volume = 100
  218. return _kodi_thread._play_rtsp_video_by_url(rtsp_url, client_index, volume)
  219. def revoke_individual_state() -> bool:
  220. """撤销所有客户端的独立状态。
  221. Returns:
  222. bool: 是否成功撤销
  223. """
  224. _kodi_thread._initialize_manager()
  225. if _kodi_thread.manager is None:
  226. logger.error("KodiClientManager 初始化失败")
  227. return False
  228. return _kodi_thread._revoke_individual_state()
  229. def start_all_kodi_apps() -> bool:
  230. """启动所有kodi应用程序。
  231. Returns:
  232. bool: 是否成功启动
  233. """
  234. _kodi_thread._initialize_manager()
  235. if _kodi_thread.manager is None:
  236. logger.error("KodiClientManager 初始化失败")
  237. return False
  238. return _kodi_thread._start_all_kodi_apps()
  239. def set_volume(volume: int) -> bool:
  240. """设置同步播放音量
  241. Args:
  242. volume: 音量值 (0-100)
  243. Returns:
  244. bool: 是否成功设置
  245. """
  246. _kodi_thread._initialize_manager()
  247. if _kodi_thread.manager is None:
  248. logger.error("KodiClientManager 初始化失败")
  249. return False
  250. # 确保音量在有效范围内
  251. if volume < 0:
  252. volume = 0
  253. elif volume > 100:
  254. volume = 100
  255. return _kodi_thread._set_volume(volume)
  256. def get_kodi_clients() -> List[Dict[str, Any]]:
  257. """获取所有 Kodi 客户端列表
  258. Returns:
  259. list: 包含客户端信息的字典列表
  260. """
  261. _kodi_thread._initialize_manager()
  262. if _kodi_thread.manager is None:
  263. return []
  264. clients_list = []
  265. for index, client in enumerate(_kodi_thread.manager.kodi_clients):
  266. # 假设 ID 对应 0, 1, 2...
  267. # 用户需求:左边计数起算一号电视 (ID: 0 -> 1号电视)
  268. display_id = index + 1
  269. name = f"{display_id}号电视 (ID: {client.id}, IP: {client.host})"
  270. clients_list.append({
  271. "index": index,
  272. "id": client.id,
  273. "ip": client.host,
  274. "name": name
  275. })
  276. return clients_list
  277. def get_video_list() -> List[Dict[str, Any]]:
  278. """获取所有视频列表
  279. Returns:
  280. list: 包含视频信息的字典列表
  281. """
  282. _kodi_thread._initialize_manager()
  283. if _kodi_thread.manager is None:
  284. return []
  285. videos_list = []
  286. for video in _kodi_thread.manager.video_infos:
  287. videos_list.append({
  288. "id": video.id,
  289. "name": video.name,
  290. "description": video.description,
  291. "duration": video.video_duration
  292. })
  293. return videos_list