kodi_thread.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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. def _get_volume(self) -> int:
  165. try:
  166. if self.manager:
  167. return self.manager.get_volume()
  168. return -1
  169. except Exception as e:
  170. logger.error(f"获取音量异常: {e}")
  171. return -1
  172. # 全局单例
  173. _kodi_thread = KodiPlayThreadSingleton()
  174. def start_kodi_play(video_id: int, volume: int = -1) -> bool:
  175. """开始(或切换到)播放指定视频ID(可被新任务打断)。"""
  176. with _kodi_thread._lock_data:
  177. _kodi_thread._incoming_task_id = video_id
  178. _kodi_thread._incoming_task_volume = volume
  179. # 如果线程挂了,尝试重启
  180. if _kodi_thread.thread is None or not _kodi_thread.thread.is_alive():
  181. _kodi_thread._start_worker_thread()
  182. return True
  183. def stop_kodi_play() -> bool:
  184. """停止播放线程与当前播放。"""
  185. return _kodi_thread.stop()
  186. def is_kodi_thread_running() -> bool:
  187. """线程是否在运行。"""
  188. return _kodi_thread.is_running
  189. def play_image(image_url: str, client_index: int) -> bool:
  190. """在指定的 Kodi 客户端上播放图片(通过URL)。
  191. Args:
  192. image_url: 图片的URL地址
  193. client_index: Kodi客户端索引(从0开始)
  194. Returns:
  195. bool: 是否成功启动播放
  196. """
  197. _kodi_thread._initialize_manager()
  198. if _kodi_thread.manager is None:
  199. logger.error("KodiClientManager 初始化失败")
  200. return False
  201. if client_index < 0 or client_index >= len(_kodi_thread.manager.kodi_clients):
  202. logger.error(f"无效的客户端索引: {client_index},有效范围: 0-{len(_kodi_thread.manager.kodi_clients)-1}")
  203. return False
  204. return _kodi_thread._play_image_by_url(image_url, client_index)
  205. def play_rtsp(rtsp_url: str, client_index: int, volume: int = 0) -> bool:
  206. """在指定的 Kodi 客户端上播放 RTSP 视频流。
  207. Args:
  208. rtsp_url: RTSP视频流的URL地址
  209. client_index: Kodi客户端索引(从0开始)
  210. volume: 播放音量(0-100),默认为0
  211. Returns:
  212. bool: 是否成功启动播放
  213. """
  214. _kodi_thread._initialize_manager()
  215. if _kodi_thread.manager is None:
  216. logger.error("KodiClientManager 初始化失败")
  217. return False
  218. if client_index < 0 or client_index >= len(_kodi_thread.manager.kodi_clients):
  219. logger.error(f"无效的客户端索引: {client_index},有效范围: 0-{len(_kodi_thread.manager.kodi_clients)-1}")
  220. return False
  221. # 确保音量在有效范围内
  222. if volume < 0:
  223. volume = 0
  224. elif volume > 100:
  225. volume = 100
  226. return _kodi_thread._play_rtsp_video_by_url(rtsp_url, client_index, volume)
  227. def revoke_individual_state() -> bool:
  228. """撤销所有客户端的独立状态。
  229. Returns:
  230. bool: 是否成功撤销
  231. """
  232. _kodi_thread._initialize_manager()
  233. if _kodi_thread.manager is None:
  234. logger.error("KodiClientManager 初始化失败")
  235. return False
  236. return _kodi_thread._revoke_individual_state()
  237. def start_all_kodi_apps() -> bool:
  238. """启动所有kodi应用程序。
  239. Returns:
  240. bool: 是否成功启动
  241. """
  242. _kodi_thread._initialize_manager()
  243. if _kodi_thread.manager is None:
  244. logger.error("KodiClientManager 初始化失败")
  245. return False
  246. return _kodi_thread._start_all_kodi_apps()
  247. def set_volume(volume: int) -> bool:
  248. """设置同步播放音量
  249. Args:
  250. volume: 音量值 (0-100)
  251. Returns:
  252. bool: 是否成功设置
  253. """
  254. _kodi_thread._initialize_manager()
  255. if _kodi_thread.manager is None:
  256. logger.error("KodiClientManager 初始化失败")
  257. return False
  258. # 确保音量在有效范围内
  259. if volume < 0:
  260. volume = 0
  261. elif volume > 100:
  262. volume = 100
  263. return _kodi_thread._set_volume(volume)
  264. def get_volume() -> int:
  265. """获取当前全局音量
  266. Returns:
  267. int: 当前音量值
  268. """
  269. _kodi_thread._initialize_manager()
  270. if _kodi_thread.manager is None:
  271. logger.error("KodiClientManager 初始化失败")
  272. return -1
  273. return _kodi_thread._get_volume()
  274. def get_kodi_clients() -> List[Dict[str, Any]]:
  275. """获取所有 Kodi 客户端列表
  276. Returns:
  277. list: 包含客户端信息的字典列表
  278. """
  279. _kodi_thread._initialize_manager()
  280. if _kodi_thread.manager is None:
  281. return []
  282. clients_list = []
  283. for index, client in enumerate(_kodi_thread.manager.kodi_clients):
  284. # 假设 ID 对应 0, 1, 2...
  285. # 用户需求:左边计数起算一号电视 (ID: 0 -> 1号电视)
  286. display_id = index + 1
  287. name = f"{display_id}号电视 (ID: {client.id}, IP: {client.host})"
  288. clients_list.append({
  289. "index": index,
  290. "id": client.id,
  291. "ip": client.host,
  292. "name": name
  293. })
  294. return clients_list
  295. def get_video_list() -> List[Dict[str, Any]]:
  296. """获取所有视频列表
  297. Returns:
  298. list: 包含视频信息的字典列表
  299. """
  300. _kodi_thread._initialize_manager()
  301. if _kodi_thread.manager is None:
  302. return []
  303. videos_list = []
  304. for video in _kodi_thread.manager.video_infos:
  305. videos_list.append({
  306. "id": video.id,
  307. "name": video.name,
  308. "description": video.description,
  309. "duration": video.video_duration
  310. })
  311. return videos_list