| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- import sys
- import os
- from kodi_util.task_registry import register_task
- # 添加项目根目录到 Python 路径
- current_dir = os.path.dirname(os.path.abspath(__file__))
- project_root = os.path.dirname(os.path.dirname(current_dir))
- sys.path.append(project_root)
- import serial
- import time
- import threading
- import binascii
- from kodi_util.kodi_play.kodi__play_interface import KodiPlayInterface
- from kodi_util.kodi_server import KodiServer
- from kodi_util.kodi_play.thread import SerialMonitorThread, PlaybackMonitorThread, KodiStateMonitorThread, VideoSyncThread
- from kodi_util.loadconfig import ConfigLoader
- from PyQt5.QtCore import QTimer
- import yaml
- @register_task("监听串口")
- class chuankou(KodiPlayInterface):
- """
- Kodi播放任务类,实现了KodiPlayInterface接口。
- 用于接收串口信号,将信号转换成KodiServer中客户端的索引,通过对应的客户端控制播放。
- 支持十六进制(hex)模式的串口通信。
- 支持两种播放模式:
- - 单独播放模式:只有对应索引的客户端播放视频
- - 所有客户端播放模式:所有客户端播放相同的视频
- """
-
- # 播放模式常量
- MODE_SINGLE = 0 # 单独播放模式
- MODE_ALL = 1 # 所有客户端播放模式
-
- def __init__(self, port=None, baudrate=None, timeout=1.0, kodi_server=None, play_mode=None):
- """
- 初始化Kodi播放任务。
-
- Args:
- port: 串口名称,如果为None则从配置文件读取
- baudrate: 波特率,如果为None则从配置文件读取
- timeout: 串口读取超时时间,默认为1秒
- kodi_server: KodiServer实例,如果为None则创建新实例
- play_mode: 播放模式,如果为None则从配置文件读取
- """
- # 从配置文件加载设置
- config = ConfigLoader.load_config('serial')
-
- # 使用配置文件中的值,如果参数提供了值则优先使用参数值
- self.port = port if port is not None else config.get('port', 'COM3')
- self.baudrate = baudrate if baudrate is not None else config.get('baudrate', 9600)
- self.play_mode = play_mode if play_mode is not None else config.get('play_mode', self.MODE_ALL)
- self.timeout = timeout
- self.serial = None
- self.running = False
- self.thread = None
- self.last_client_index = None
- self.signal_callback = None
- self.default_video = ConfigLoader.load_config('default_video')
- self.current_playing_video = None
- self.playback_monitor_thread = None
- self.is_monitoring_playback = False
-
- # 如果没有提供KodiServer实例,则创建一个
- self.kodi_server = kodi_server if kodi_server else KodiServer()
-
- # 创建状态监控线程
- self.state_monitor = KodiStateMonitorThread(self.kodi_server.clients)
- self.state_monitor.start()
-
- # 打开串口
- self._open_serial()
-
- def _open_serial(self):
- """
- 打开串口连接。
-
- Returns:
- bool: 是否成功打开串口
- """
- try:
- self.serial = serial.Serial(
- port=self.port,
- baudrate=self.baudrate,
- timeout=self.timeout,
- bytesize=serial.EIGHTBITS,
- parity=serial.PARITY_NONE,
- stopbits=serial.STOPBITS_ONE
- )
- return True
- except Exception as e:
- print(f"打开串口失败: {str(e)}")
- return False
-
- def _close_serial(self):
- """
- 关闭串口连接。
- """
- if self.serial and self.serial.is_open:
- self.serial.close()
-
- def _read_signal(self):
- """
- 从串口读取十六进制信号。
-
- Returns:
- bytes: 读取到的原始十六进制数据
- """
- if not self.serial or not self.serial.is_open:
- return None
-
- try:
- # 读取串口数据 (以原始字节形式)
- if self.serial.in_waiting > 0:
- data = self.serial.read(self.serial.in_waiting)
- if data:
- # 打印十六进制格式的数据,便于调试
- hex_data = binascii.hexlify(data).decode('utf-8')
- print(f"接收到十六进制数据: 0x{hex_data}")
- return data
- return None
- except Exception as e:
- print(f"读取串口数据失败: {str(e)}")
- return None
-
- def _parse_hex_signal(self, hex_data):
- """
- 解析十六进制信号,将其转换为客户端索引。
-
- Args:
- hex_data: 原始的十六进制数据字节
-
- Returns:
- int: 客户端索引,如果解析失败则返回None
- """
- try:
- # 没有数据,立即返回
- if not hex_data or len(hex_data) == 0:
- return None
-
- # 如果接收到的是单字节命令
- if len(hex_data) == 1:
- # 直接将字节值作为客户端索引
- client_index = int(chr(hex_data[0]))
- print(f"解析十六进制单字节: 0x{hex_data[0]:02X} -> 客户端索引 {client_index}")
- return client_index
-
- # 如果是多字节命令
- elif len(hex_data) >= 2:
- # 根据实际协议解析
- # 示例: 假设第一个字节是命令类型,第二个字节是客户端索引
- cmd_type = hex_data[0]
- client_index_byte = hex_data[1]
-
- # 假设0x01表示客户端索引命令
- if cmd_type == 0x01:
- client_index = int(client_index_byte)
- print(f"解析十六进制多字节: 0x{cmd_type:02X} 0x{client_index_byte:02X} -> 客户端索引 {client_index}")
- return client_index
- # 其他命令类型可以在这里扩展
- else:
- print(f"未知命令类型: 0x{cmd_type:02X}")
- return None
-
- # 如果数据格式无法解析
- else:
- print(f"无法解析的十六进制数据: {binascii.hexlify(hex_data).decode('utf-8')}")
- return None
-
- except Exception as e:
- print(f"解析十六进制信号出错: {str(e)}")
- return None
-
- def _send_hex_response(self, client_index, success=True):
- """
- 发送十六进制响应。
-
- Args:
- client_index: 客户端索引
- success: 是否成功识别
- """
- if not self.serial or not self.serial.is_open:
- return
-
- try:
- # 构造响应包
- # 例如: 0x02(响应命令) + 客户端索引字节 + 状态字节(0x01成功,0x00失败)
- response = bytearray([0x02, client_index, 0x01 if success else 0x00])
-
- # 发送响应
- self.serial.write(response)
-
- # 打印发送的十六进制数据
- hex_response = binascii.hexlify(response).decode('utf-8')
- print(f"发送十六进制响应: 0x{hex_response}")
-
- except Exception as e:
- print(f"发送十六进制响应失败: {str(e)}")
-
- def _handle_serial_data(self, data):
- """
- 处理串口数据
-
- Args:
- data: 串口接收到的数据
- """
- try:
- # 解析十六进制信号为客户端索引
- client_index = self._parse_hex_signal(data)
-
- # 如果解析成功
- if client_index is not None:
- print(f"接收到客户端索引: {client_index}")
-
- # 保存最近的客户端索引
- self.last_client_index = client_index
-
- # 判断客户端索引是否有效
- is_valid = client_index < len(self.kodi_server.clients)
-
- # 发送响应
- self._send_hex_response(client_index, is_valid)
-
- # 如果设置了回调函数,则调用回调函数
- if self.signal_callback:
- self.signal_callback(client_index)
-
- except Exception as e:
- print(f"处理串口数据出错: {str(e)}")
-
- def _task_loop(self):
- """
- 任务循环,持续读取串口信号获取客户端索引。
- """
- if not self._open_serial():
- print("串口打开失败,任务终止")
- return
-
- print(f"开始监听串口 {self.port},等待客户端索引信号...")
-
- while self.running:
- # 读取信号
- hex_data = self._read_signal()
-
- # 如果读取到有效信号
- if hex_data:
- # 解析十六进制信号为客户端索引
- client_index = self._parse_hex_signal(hex_data)
-
- # 如果解析成功
- if client_index is not None:
- print(f"接收到客户端索引: {client_index}")
-
- # 保存最近的客户端索引
- self.last_client_index = client_index
-
- # 判断客户端索引是否有效
- is_valid = client_index < len(self.kodi_server.clients)
-
- # 发送响应
- self._send_hex_response(client_index, is_valid)
-
- # 如果设置了回调函数,则调用回调函数
- if self.signal_callback:
- self.signal_callback(client_index)
-
- # 短暂休眠,避免CPU使用率过高
- time.sleep(0.1)
-
- # 循环结束,关闭串口
- self._close_serial()
- print("监听串口任务结束")
-
- def _monitor_playback(self, client_index=None):
- """
- 监控播放状态
-
- Args:
- client_index: 客户端索引,如果为None则监控所有客户端
- """
- if self.play_mode == self.MODE_SINGLE:
- client = self.kodi_server.clients[client_index]
- monitor = PlaybackMonitorThread(
- client=client,
- client_idx=client_index,
- state_monitor=self.state_monitor,
- start_time=0,
- end_time=float('inf'),
- callback=lambda: setattr(self, 'is_monitoring_playback', False)
- )
- monitor.start()
- else:
- # 监控所有客户端
- for idx, client in enumerate(self.kodi_server.clients):
- monitor = PlaybackMonitorThread(
- client=client,
- client_idx=idx,
- state_monitor=self.state_monitor,
- start_time=0,
- end_time=float('inf'),
- callback=lambda: setattr(self, 'is_monitoring_playback', False)
- )
- monitor.start()
-
- def play(self, video_path, client_index=None, loop=False):
- """
- 实现KodiPlayInterface的play方法,播放指定视频。
- 根据当前播放模式,播放方式会有所不同:
- - 单独播放模式(MODE_SINGLE):只有指定索引的客户端播放视频
- - 所有客户端模式(MODE_ALL):所有客户端播放相同的视频
-
- 视频播放完毕后会自动播放默认视频。
-
- Args:
- video_path: 视频路径
- client_index: 客户端索引,如果为None则使用上一次的索引
- loop: 是否循环播放
-
- Returns:
- bool: 是否成功开始播放
- """
- # 记录当前播放的视频
- self.current_playing_video = video_path
-
- # 停止之前的监控线程
- if self.playback_monitor_thread and self.playback_monitor_thread.is_alive():
- self.is_monitoring_playback = False
- self.playback_monitor_thread.join(timeout=2)
-
- # 如果没有提供客户端索引,使用上一次的索引
- if client_index is None:
- client_index = self.last_client_index
-
- # 如果仍然没有客户端索引,且播放模式为单独播放,则无法播放
- if client_index is None and self.play_mode == self.MODE_SINGLE:
- print("错误:单独播放模式下必须提供客户端索引")
- return False
-
- # 检查客户端索引是否有效
- if self.play_mode == self.MODE_SINGLE and client_index >= len(self.kodi_server.clients):
- print(f"错误:客户端索引 {client_index} 超出范围")
- return False
-
- # 打印播放信息
- if self.play_mode == self.MODE_SINGLE:
- print(f"在客户端 {client_index} 上播放视频: {video_path}")
- else:
- print(f"在所有客户端上播放视频: {video_path}")
-
- # 根据播放模式选择播放方式
- result = False
- if self.play_mode == self.MODE_SINGLE:
- # 单独播放模式
- client = self.kodi_server.clients[client_index]
- result = client.play_video(video_path, loop=loop)
- else:
- # 所有客户端播放模式
- play_result = self.kodi_server.sync_play_video(
- video_path,
- sound_client_index=-1, # 默认所有客户端播放声音
- default_volume=70, # 默认音量70%
- loop=loop
- )
- result = play_result.get("success", False)
- if not result:
- print(f"同步播放失败: {play_result.get('message', '未知错误')}")
- if play_result.get("failed_clients"):
- print(f"失败的客户端: {', '.join(play_result['failed_clients'])}")
-
- # 如果播放成功且不是循环播放,启动监控线程
- if result and not loop:
- # 设置监控标志
- self.is_monitoring_playback = True
-
- # 创建客户端循环播放设置的列表,所有客户端都不循环播放
- # 通过监控主客户端的播放状态来控制整体播放结束
- master_client_index = self.sound_client_index
-
- # 如果sound_client_index为-1,则默认使用第一个客户端作为主客户端
- if master_client_index < 0 and len(self.kodi_server.clients) > 0:
- master_client_index = 0
- print(f"主客户端索引为-1,使用第一个客户端(索引0)作为主客户端")
-
- # 创建客户端循环设置列表 - 所有客户端都不循环播放
- client_loop_settings = []
- for i in range(len(self.kodi_server.clients)):
- client_loop_settings.append(False) # 所有客户端都不循环播放
- print(f"客户端 {i} 设置为不循环播放")
-
- print(f"主客户端索引: {master_client_index},将监控其播放状态来控制整体播放结束")
-
- # 创建并启动监控线程
- self.playback_monitor_thread = threading.Thread(
- target=self._monitor_playback,
- args=(client_index if self.play_mode == self.MODE_SINGLE else None,)
- )
- self.playback_monitor_thread.daemon = True
- self.playback_monitor_thread.start()
-
- return result
-
- def set_signal_callback(self, callback):
- """
- 设置信号回调函数。
-
- Args:
- callback: 回调函数,接收一个参数(客户端索引)
- """
- self.signal_callback = callback
-
- def start_monitoring(self):
- """
- 开始监听串口信号。
-
- Returns:
- bool: 是否成功启动监听
- """
- if self.thread and self.thread.is_alive():
- print("警告:监听任务已经在运行")
- return False
-
- if not self.serial or not self.serial.is_open:
- if not self._open_serial():
- print("错误:无法打开串口")
- return False
-
- self.running = True
- self.thread = SerialMonitorThread(
- self.serial,
- self._handle_serial_data,
- interval=0.1
- )
- self.thread.start()
-
- # 启动时自动播放默认视频(循环播放)
- self._play_default_video(loop=True)
-
- return True
-
- def _play_default_video(self, loop=True):
- """
- 播放默认视频
-
- Args:
- loop: 是否循环播放,默认为True(但实际会被覆盖为False,通过监控重新下发实现循环效果)
-
- Returns:
- bool: 是否成功开始播放
- """
- try:
- print(f"========== 开始加载和播放默认视频(监控重新下发模式) ==========")
-
- # 从配置文件重新加载默认视频配置,确保使用最新配置
- self.default_video = ConfigLoader.load_config('default_video')
- print(f"加载到的default_video配置: {self.default_video}")
-
- # 重新加载声音客户端配置
- config = ConfigLoader.load_config('ButtonListenerTask')
- if 'sound' in config:
- if 'client_index' in config['sound']:
- old_index = self.sound_client_index
- self.sound_client_index = config['sound']['client_index']
- if old_index != self.sound_client_index:
- print(f"声音客户端索引已更新: {old_index} -> {self.sound_client_index}")
- if 'volume' in config['sound']:
- old_volume = self.default_volume
- self.default_volume = config['sound']['volume']
- if old_volume != self.default_volume:
- print(f"默认音量已更新: {old_volume}% -> {self.default_volume}%")
-
- # 获取默认视频路径
- default_video_path = None
-
- if self.default_video:
- if isinstance(self.default_video, str):
- # 向后兼容:如果是字符串,直接使用作为路径
- default_video_path = self.default_video
- print(f"默认视频路径(字符串格式): {default_video_path}")
- elif isinstance(self.default_video, dict):
- # 新格式:包含path和loop字段的字典
- if 'path' in self.default_video:
- default_video_path = self.default_video['path']
- print(f"默认视频路径(字典格式): {default_video_path}")
-
- if not default_video_path:
- print("警告: 没有找到默认视频配置,无法播放默认视频")
- return False
-
- print(f"即将播放默认视频: {default_video_path} (监控重新下发模式,不使用循环播放)")
-
- # 创建客户端循环播放设置的列表,所有客户端都不循环播放
- # 通过监控所有客户端的播放状态,当任何一台结束时重新下发
- master_client_index = self.sound_client_index
-
- # 如果sound_client_index为-1,则默认使用第一个客户端作为主客户端
- if master_client_index < 0 and len(self.kodi_server.clients) > 0:
- master_client_index = 0
- print(f"主客户端索引为-1,使用第一个客户端(索引0)作为主客户端")
-
- # 创建客户端循环设置列表 - 所有客户端都不循环播放
- client_loop_settings = []
- for i in range(len(self.kodi_server.clients)):
- client_loop_settings.append(False) # 所有客户端都不循环播放
- print(f"客户端 {i} 设置为不循环播放默认视频")
-
- print(f"主客户端索引: {master_client_index},将监控所有客户端播放状态,任何一台结束时重新下发")
-
- # 立即下发播放指令,不使用循环播放,通过监控重新下发实现循环效果
- print(f"立即下发默认视频播放指令: {default_video_path} (监控重新下发模式)")
- result = self.kodi_server.sync_play_video(
- default_video_path,
- sound_client_index=self.sound_client_index,
- default_volume=self.default_volume,
- loop=False, # 不使用循环播放,通过监控重新下发实现循环效果
- client_loop_settings=client_loop_settings # 添加客户端循环设置列表
- )
-
- if result.get("success", False):
- print("播放指令下发成功")
-
- # 等待播放开始
- time.sleep(2)
-
- # 启动监控线程,监控所有客户端的播放状态
- print("启动默认视频监控线程,监控所有客户端播放状态")
- self.is_monitoring_playback = True
- self.playback_monitor_thread = threading.Thread(
- target=self._monitor_default_video_playback
- )
- self.playback_monitor_thread.daemon = True
- self.playback_monitor_thread.start()
- print(f"默认视频监控线程已启动: is_alive={self.playback_monitor_thread.is_alive()}")
-
- # 验证是否确实开始播放
- print("验证播放是否成功启动")
- for idx, client in enumerate(self.kodi_server.clients):
- try:
- state = client.get_player_state()
- is_playing = state.get('playing', False)
- if not is_playing:
- print(f"警告:客户端 {idx} 可能未正确播放,状态: {state}")
- except Exception as e:
- print(f"验证客户端 {idx} 播放状态出错: {str(e)}")
-
- else:
- print(f"播放指令下发失败: {result.get('message', '未知错误')}")
- if result.get("failed_clients"):
- print(f"失败的客户端: {', '.join(result['failed_clients'])}")
-
- # 如果同步播放失败,尝试逐个客户端单独播放
- print("第七阶段:失败后尝试逐个播放")
- print("尝试逐个客户端单独播放默认视频")
- for idx, client in enumerate(self.kodi_server.clients):
- try:
- # 设置音量
- if idx == self.sound_client_index or self.sound_client_index == -1:
- client.set_volume(self.default_volume)
- else:
- client.set_volume(0)
-
- # 所有客户端都不循环播放
- print(f"客户端 {idx} 单独播放,不循环播放")
-
- # 播放视频
- result = client.play_video(default_video_path, loop=False)
- print(f"客户端 {idx} 单独播放结果: {result}")
- time.sleep(0.5) # 增加间隔,避免同时发送太多命令
- except Exception as e:
- print(f"客户端 {idx} 单独播放出错: {str(e)}")
-
- # 即使单独播放,也启动监控线程
- print("启动默认视频监控线程(单独播放模式)")
- self.is_monitoring_playback = True
- self.playback_monitor_thread = threading.Thread(
- target=self._monitor_default_video_playback
- )
- self.playback_monitor_thread.daemon = True
- self.playback_monitor_thread.start()
-
- print("=========== 视频播放完毕回调函数执行结束 ===========")
-
- except Exception as e:
- print(f"重新下发播放指令时出错: {str(e)}")
- import traceback
- traceback.print_exc()
-
- def interrupt(self):
- """
- 中断监听任务。
-
- Returns:
- bool: 是否成功中断
- """
- if not self.thread or not self.thread.is_alive():
- print("警告:没有运行中的监听任务")
- return False
-
- self.running = False
- self.thread.join(timeout=2) # 等待线程结束,最多等待2秒
-
- # 停止播放状态监控线程
- if self.playback_monitor_thread and self.playback_monitor_thread.is_alive():
- self.is_monitoring_playback = False
- self.playback_monitor_thread.join(timeout=1)
-
- # 停止状态监控线程
- self.state_monitor.stop()
-
- if self.thread.is_alive():
- print("警告:监听任务未能在超时时间内结束")
- return False
- else:
- print("监听任务已成功中断")
- return True
-
- def get_last_client_index(self):
- """
- 获取最后接收到的客户端索引。
-
- Returns:
- int: 最后的客户端索引,如果没有则返回None
- """
- return self.last_client_index
- @register_task("联动")
- class LinkageTask(KodiPlayInterface):
- """
- 联动任务类,用于实现多个Kodi客户端的接力播放功能。
- 从配置文件中读取视频信息,按照指定的时间段在不同客户端之间接力播放。
- """
-
- def __init__(self, kodi_server=None):
- """
- 初始化联动任务。
-
- Args:
- kodi_server: KodiServer实例,如果为None则创建新实例
- """
- self.kodi_server = kodi_server if kodi_server else KodiServer()
- self.current_video_index = 0
- self.current_segment_index = 0
- self.is_playing = False
- self.playback_thread = None
- self.config = {"videos": []}
-
- # 创建状态监控线程
- self.state_monitor = KodiStateMonitorThread(self.kodi_server.clients)
- self.state_monitor.start()
-
- # 异步加载配置文件
- self._load_config_async()
-
- def _load_config_async(self):
- """异步加载配置文件"""
- try:
- config = ConfigLoader.load_config('relay')
- if not config or "videos" not in config:
- print("配置文件格式错误:缺少videos字段")
- return
- if not config["videos"]:
- print("配置文件为空:没有可播放的视频")
- return
- self.config = config
- except Exception as e:
- print(f"加载配置文件失败: {str(e)}")
-
- def _monitor_playback(self, client, start_time, end_time):
- """
- 监控播放进度,在指定时间点切换客户端。
-
- Args:
- client: Kodi客户端实例
- start_time: 开始时间(秒)
- end_time: 结束时间(秒)
- """
- try:
- client_idx = self.kodi_server.clients.index(client)
- except ValueError:
- print(f"错误:客户端 {client.host} 不在客户端列表中")
- return
-
- while self.is_playing:
- try:
- # 从状态监控线程获取当前播放时间
- current_time = self.state_monitor.get_playback_time(client_idx)
-
- # 如果超过结束时间,切换到下一个片段
- if current_time >= end_time:
- # 使用QTimer延迟执行下一个片段,避免阻塞
- QTimer.singleShot(0, self._play_next_segment)
- break
-
- time.sleep(1) # 每秒检查一次
-
- except Exception as e:
- print(f"监控播放进度时出错: {str(e)}")
- time.sleep(5)
-
- def _play_next_segment(self):
- """播放下一个视频片段"""
- try:
- if not self.config["videos"]:
- print("没有可播放的视频")
- self.is_playing = False
- return
-
- # 获取当前视频信息
- current_video = self.config["videos"][self.current_video_index]
- if "segments" not in current_video or not current_video["segments"]:
- print("当前视频没有有效的片段配置")
- self.is_playing = False
- return
-
- segments = current_video["segments"]
-
- # 如果当前片段是最后一个
- if self.current_segment_index >= len(segments) - 1:
- # 如果是最后一个视频,重新开始
- if self.current_video_index >= len(self.config["videos"]) - 1:
- self.current_video_index = 0
- else:
- self.current_video_index += 1
- self.current_segment_index = 0
- else:
- self.current_segment_index += 1
-
- # 获取新的片段信息
- segment = segments[self.current_segment_index]
- if "client_index" not in segment or "start_time" not in segment or "end_time" not in segment:
- print("片段配置格式错误")
- self.is_playing = False
- return
-
- client_index = segment["client_index"]
- start_time = segment["start_time"]
- end_time = segment["end_time"]
-
- # 检查客户端索引是否有效
- if client_index >= len(self.kodi_server.clients):
- print(f"错误:客户端索引 {client_index} 超出范围")
- self.is_playing = False
- return
-
- # 在指定客户端上播放视频
- client = self.kodi_server.clients[client_index]
-
- # 使用QTimer延迟执行播放,避免阻塞
- QTimer.singleShot(0, lambda: self._play_video_segment(client, current_video["path"], start_time, end_time))
-
- except Exception as e:
- print(f"播放下一个片段时出错: {str(e)}")
- self.is_playing = False
-
- def _play_video_segment(self, client, video_path, start_time, end_time):
- """播放视频片段"""
- try:
- if not client.play_video(video_path, start_time=start_time):
- print(f"错误:在客户端 {client.host} 上播放视频失败")
- self.is_playing = False
- return
-
- # 启动监控线程
- self.playback_thread = threading.Thread(
- target=self._monitor_playback,
- args=(client, start_time, end_time)
- )
- self.playback_thread.daemon = True
- self.playback_thread.start()
-
- except Exception as e:
- print(f"播放视频片段时出错: {str(e)}")
- self.is_playing = False
-
- def play(self, video_path=None, client_index=None, loop=False):
- """
- 开始播放视频。
-
- Args:
- video_path: 视频路径(可选,如果提供则播放指定视频)
- client_index: 客户端索引(可选,如果提供则从指定客户端开始)
- loop: 是否循环播放
-
- Returns:
- bool: 是否成功开始播放
- """
- try:
- # 检查配置文件
- if not self.config["videos"]:
- print("错误:没有可播放的视频配置")
- return False
-
- # 如果提供了视频路径,查找对应的视频配置
- if video_path:
- found = False
- for i, video in enumerate(self.config["videos"]):
- if video["path"] == video_path:
- self.current_video_index = i
- found = True
- break
- if not found:
- print(f"错误:找不到视频 {video_path} 的配置")
- return False
-
- # 如果提供了客户端索引,查找对应的片段
- if client_index is not None:
- current_video = self.config["videos"][self.current_video_index]
- if "segments" not in current_video:
- print("错误:当前视频没有片段配置")
- return False
-
- found = False
- for i, segment in enumerate(current_video["segments"]):
- if segment["client_index"] == client_index:
- self.current_segment_index = i - 1 # 减1是因为_play_next_segment会加1
- found = True
- break
- if not found:
- print(f"错误:找不到客户端 {client_index} 的片段配置")
- return False
-
- # 开始播放
- self.is_playing = True
- # 使用QTimer延迟执行第一个片段,避免阻塞
- QTimer.singleShot(0, self._play_next_segment)
- return True
-
- except Exception as e:
- print(f"开始播放时出错: {str(e)}")
- self.is_playing = False
- return False
-
- def interrupt(self):
- """
- 中断播放。
-
- Returns:
- bool: 是否成功中断
- """
- self.is_playing = False
- if self.playback_thread and self.playback_thread.is_alive():
- self.playback_thread.join(timeout=2)
-
- # 停止状态监控线程
- self.state_monitor.stop()
-
- return True
- @register_task("按钮监听串")
- class ButtonListenerTask(KodiPlayInterface):
- """
- 按钮监听任务类,用于监听外部按钮的串口信号并控制Kodi播放。
- 当接收到串口信号时,将信号解析为视频索引,并控制所有Kodi客户端播放对应的视频。
- 播放完成后自动播放默认视频。
- """
-
- def __init__(self, port=None, baudrate=None, timeout=1.0, kodi_server=None):
- """
- 初始化按钮监听任务。
-
- Args:
- port: 串口名称,如果为None则从配置文件读取
- baudrate: 波特率,如果为None则从配置文件读取
- timeout: 串口读取超时时间,默认为1秒
- kodi_server: KodiServer实例,如果为None则创建新实例
- """
- # 调用父类的初始化方法,不需要传递任何参数
- super().__init__()
-
- self.port = port
- self.baudrate = baudrate
- self.timeout = timeout
-
- self.serial = None
- self.thread = None
- self.running = False
- self.video_paths = []
-
- # 声音播放相关配置
- self.sound_client_index = -1 # 默认所有设备播放声音
- self.default_volume = 70 # 默认音量70%
-
- # 播放监控相关
- self.playback_monitor_thread = None
- self.is_monitoring_playback = False
- self.current_playing_video = None
-
- # 加载默认视频配置
- self.default_video = ConfigLoader.load_config('default_video')
-
- # 如果没有提供KodiServer实例,则创建一个
- self.kodi_server = kodi_server
- if self.kodi_server is None:
- print("创建新的KodiServer实例")
- self.kodi_server = KodiServer()
-
- # 创建状态监控线程
- self.state_monitor = KodiStateMonitorThread(self.kodi_server.clients)
- self.state_monitor.start()
-
- # 加载配置
- self._load_config()
-
- def _load_config(self):
- """加载配置文件"""
- try:
- # 使用ConfigLoader加载按钮监听器配置
- config = ConfigLoader.load_config('ButtonListenerTask')
-
- print(f"按钮监听任务配置加载结果: {config}")
-
- # 加载视频路径
- if 'video_paths' in config:
- self.video_paths = config['video_paths']
- print(f"已加载 {len(self.video_paths)} 个视频路径")
- # 打印每个视频路径,方便调试
- for i, path in enumerate(self.video_paths):
- print(f" 视频 {i}: {path}")
- else:
- print("警告: 配置文件中找不到 'video_paths' 键,请检查配置文件格式")
-
- # 加载串口配置
- if 'serial' in config:
- if not self.port and 'port' in config['serial']:
- self.port = config['serial']['port']
-
- if not self.baudrate and 'baudrate' in config['serial']:
- self.baudrate = config['serial']['baudrate']
-
- if 'timeout' in config['serial']:
- self.timeout = config['serial']['timeout']
-
- print(f"串口配置: 端口={self.port}, 波特率={self.baudrate}, 超时={self.timeout}")
- else:
- print("警告: 配置文件中找不到 'serial' 键,将使用默认串口配置")
-
- # 加载声音配置
- if 'sound' in config:
- if 'client_index' in config['sound']:
- self.sound_client_index = config['sound']['client_index']
- if 'volume' in config['sound']:
- self.default_volume = config['sound']['volume']
- print(f"声音配置: 客户端索引={self.sound_client_index}, 音量={self.default_volume}%")
- else:
- print("警告: 配置文件中找不到 'sound' 键,将使用默认声音配置")
-
- except Exception as e:
- print(f"加载配置文件时出错: {str(e)}")
- import traceback
- traceback.print_exc()
-
- def _open_serial(self):
- """打开串口连接"""
- try:
- self.serial = serial.Serial(
- port=self.port,
- baudrate=self.baudrate,
- timeout=self.timeout,
- bytesize=serial.EIGHTBITS,
- parity=serial.PARITY_NONE,
- stopbits=serial.STOPBITS_ONE
- )
- return True
- except Exception as e:
- print(f"打开串口失败: {str(e)}")
- return False
-
- def _close_serial(self):
- """关闭串口连接"""
- if self.serial and self.serial.is_open:
- self.serial.close()
-
- def _read_signal(self):
- """
- 从串口读取单字节信号。
-
- Returns:
- bytes: 读取到的原始字节数据
- """
- if not self.serial or not self.serial.is_open:
- return None
-
- try:
- if self.serial.in_waiting > 0:
- data = self.serial.read(1) # 只读取一个字节
- if data:
- print(f"接收到按钮信号: 0x{data.hex()}")
- return data
- return None
- except Exception as e:
- print(f"读取串口数据失败: {str(e)}")
- return None
-
- def _parse_button_signal(self, data):
- """
- 解析按钮信号,将其转换为视频索引。
-
- Args:
- data: 原始的字节数据
-
- Returns:
- int: 视频索引,如果解析失败则返回None
- """
- try:
- if not data or len(data) != 1:
- return None
-
- # 将字节转换为整数索引
- video_index = int(data[0])
-
- # 检查索引是否在有效范围内
- if 0 <= video_index < len(self.video_paths):
- print(f"解析按钮信号: 0x{data.hex()} -> 视频索引 {video_index}")
- return video_index
- else:
- print(f"无效的视频索引: {video_index},有效范围: 0-{len(self.video_paths)-1}")
- return None
-
- except Exception as e:
- print(f"解析按钮信号出错: {str(e)}")
- return None
-
- def _handle_serial_data(self, data):
- """
- 处理串口数据
-
- Args:
- data: 串口接收到的数据
- """
- try:
- # 解析按钮信号为视频索引
- video_index = self._parse_button_signal(data)
-
- # 如果解析成功
- if video_index is not None:
- # 重新加载视频路径配置,确保使用最新配置
- config = ConfigLoader.load_config('ButtonListenerTask')
-
- # 更新视频路径配置
- if 'video_paths' in config:
- self.video_paths = config['video_paths']
-
- # 更新声音客户端索引配置
- if 'sound' in config:
- if 'client_index' in config['sound']:
- self.sound_client_index = config['sound']['client_index']
- print(f"更新声音客户端索引为: {self.sound_client_index}")
- if 'volume' in config['sound']:
- self.default_volume = config['sound']['volume']
- print(f"更新默认音量为: {self.default_volume}%")
-
- # 检查是否成功加载了视频路径
- if not self.video_paths:
- print("错误:没有加载到视频路径配置,请检查ButtonListenerTask配置文件")
- return
-
- # 检查索引是否有效
- if video_index >= len(self.video_paths):
- print(f"错误:视频索引 {video_index} 超出范围,有效范围: 0-{len(self.video_paths)-1}")
- return
-
- # 获取对应的视频路径
- video_path = self.video_paths[video_index]
- print(f"准备播放视频: {video_path}")
-
- # 停止之前的监控线程
- if self.playback_monitor_thread and self.playback_monitor_thread.is_alive():
- self.is_monitoring_playback = False
- self.playback_monitor_thread.join(timeout=0.5)
-
- # 记录当前播放的视频
- self.current_playing_video = video_path
-
- # 创建客户端循环播放设置的列表,所有客户端都不循环播放
- # 通过监控主客户端的播放状态来控制整体播放结束
- master_client_index = self.sound_client_index
-
- # 如果sound_client_index为-1,则默认使用第一个客户端作为主客户端
- if master_client_index < 0 and len(self.kodi_server.clients) > 0:
- master_client_index = 0
- print(f"主客户端索引为-1,使用第一个客户端(索引0)作为主客户端")
-
- # 创建客户端循环设置列表 - 所有客户端都不循环播放
- client_loop_settings = []
- for i in range(len(self.kodi_server.clients)):
- client_loop_settings.append(False) # 所有客户端都不循环播放
- print(f"客户端 {i} 设置为不循环播放")
-
- print(f"主客户端索引: {master_client_index},将监控其播放状态来控制整体播放结束")
-
- # 在所有客户端上同步播放视频,使用自定义的循环设置
- result = self.kodi_server.sync_play_video(
- video_path,
- sound_client_index=self.sound_client_index,
- default_volume=self.default_volume,
- loop=False, # 这个参数将被client_loop_settings覆盖
- client_loop_settings=client_loop_settings # 添加客户端循环设置列表
- )
-
- if result.get("success", False):
- print("所有客户端开始播放视频")
-
- # 确保前一个监控线程不再运行
- self.is_monitoring_playback = False
- if self.playback_monitor_thread and self.playback_monitor_thread.is_alive():
- print("等待前一个监控线程结束")
- self.playback_monitor_thread.join(timeout=0.5)
-
- # 启动新的监控线程
- print("启动播放监控线程,将在主客户端视频结束时自动切换回默认视频")
- self.is_monitoring_playback = True
- self.playback_monitor_thread = threading.Thread(
- target=self._monitor_playback
- )
- self.playback_monitor_thread.daemon = True
- self.playback_monitor_thread.start()
- print(f"监控线程已启动: is_alive={self.playback_monitor_thread.is_alive()}")
-
- else:
- print(f"播放失败: {result.get('message', '未知错误')}")
- if result.get("failed_clients"):
- print(f"失败的客户端: {', '.join(result['failed_clients'])}")
- # 如果播放失败,尝试播放默认视频
- print("播放失败,尝试恢复默认视频")
- self._play_default_video(loop=True)
-
- except Exception as e:
- print(f"处理串口数据出错: {str(e)}")
- import traceback
- traceback.print_exc()
-
- def _monitor_playback(self):
- """监控主客户端的播放状态,当主客户端视频播放结束时自动播放默认视频"""
- try:
- print("开始监控主客户端的播放状态")
-
- # 确定主客户端索引(播放声音的客户端)
- master_client_index = self.sound_client_index
- if master_client_index < 0 or master_client_index >= len(self.kodi_server.clients):
- print(f"主客户端索引({master_client_index})无效,使用默认索引0")
- master_client_index = 0
-
- print(f"使用客户端 {master_client_index} 作为主客户端进行监控")
-
- # 获取主客户端
- master_client = self.kodi_server.clients[master_client_index]
-
- # 等待视频加载和开始播放
- print("等待5秒让视频充分加载并稳定播放...")
- time.sleep(5)
-
- # 获取主客户端视频总时长和当前播放的文件
- state = self.state_monitor.get_state(master_client_index)
- initial_file = state.get('current_file', '')
- video_duration = state.get('totaltime', 0)
-
- print(f"监控主客户端播放文件: {initial_file}, 总时长: {video_duration}秒")
-
- # 监控变量初始化
- last_playing = True
- last_time = 0
- consecutive_not_playing = 0
- check_counter = 0
-
- # 监控循环
- while self.is_monitoring_playback:
- check_counter += 1
-
- # 获取主客户端当前播放状态
- state = self.state_monitor.get_state(master_client_index)
- is_playing = state.get('playing', False) and not state.get('paused', True)
- current_time = state.get('time', 0)
- current_file = state.get('current_file', '')
-
- # 每10次检查输出一次详细状态信息
- if check_counter % 10 == 0:
- print(f"播放检查 #{check_counter}: 时间={current_time:.1f}/{video_duration:.1f}秒, 播放状态={is_playing}")
-
- # 检查是否文件已经变更(可能已切换到其他视频)
- if current_file != initial_file and initial_file and current_file:
- print(f"检测到文件变更: {initial_file} -> {current_file},视频可能已结束")
- self.is_monitoring_playback = False
- self._on_playback_finished()
- break
-
- # 如果能获取到总时长且接近结束,切换到默认视频
- if video_duration > 0 and current_time >= video_duration - 5:
- print(f"主客户端视频即将结束(当前进度: {current_time:.1f}/{video_duration:.1f}秒),准备切换到默认视频")
- self.is_monitoring_playback = False
- self._on_playback_finished()
- break
-
- # 监测播放状态 - 如果连续多次检测到不播放,认为视频已结束
- if not is_playing:
- if last_playing == False:
- consecutive_not_playing += 1
- if consecutive_not_playing >= 10: # 连续10次(约5秒)检测不到播放状态
- print(f"检测到连续{consecutive_not_playing}次不播放状态,视为播放结束")
- self.is_monitoring_playback = False
- self._on_playback_finished()
- break
- else:
- consecutive_not_playing = 1
- else:
- consecutive_not_playing = 0
-
- # 更新变量
- last_playing = is_playing
-
- # 短暂休眠
- time.sleep(0.5)
-
- except Exception as e:
- print(f"监控播放状态时出错: {str(e)}")
- import traceback
- traceback.print_exc()
- self.is_monitoring_playback = False
- # 如果监控出错,尝试播放默认视频
- self._play_default_video(loop=True)
-
- def _on_playback_finished(self):
- """视频播放完毕后的回调函数,并发下发播放默认视频的指令"""
- try:
- print("=========== 视频播放完毕回调函数开始执行 ===========")
-
- # 设置监控标志
- self.is_monitoring_playback = False
-
- print("第一阶段:停止当前播放")
- # 强制停止所有客户端的播放,确保没有遗留的循环播放
- for idx, client in enumerate(self.kodi_server.clients):
- try:
- print(f"强制停止客户端 {idx} 播放")
-
- # 首先尝试设置非循环播放,确保不会自动重新开始播放
- try:
- active_player_id = client._get_active_player_id()
- if active_player_id is not None:
- repeat_params = {"playerid": active_player_id, "repeat": "off"}
- repeat_response = client._send_request("Player.SetRepeat", repeat_params)
- print(f"设置客户端 {idx} 禁用循环播放: {repeat_response}")
- except Exception as e:
- print(f"设置客户端 {idx} 循环模式出错: {str(e)}")
-
- # 然后停止播放
- client.stop_playback()
-
- # 等待一小段时间确保停止命令被执行
- time.sleep(0.2)
- except Exception as e:
- print(f"停止客户端 {idx} 播放出错: {str(e)}")
-
- print("第二阶段:等待客户端停止")
- # 等待所有客户端完全停止播放
- print("等待所有客户端完全停止播放")
- time.sleep(2) # 增加等待时间到2秒
-
- print("第三阶段:再次验证和停止")
- # 再次验证所有客户端是否都已停止播放
- for idx, client in enumerate(self.kodi_server.clients):
- try:
- state = self.state_monitor.get_state(idx)
- is_playing = state.get('playing', False)
- if is_playing:
- print(f"警告:客户端 {idx} 仍在播放,强制再次停止")
- client.stop_playback()
- time.sleep(0.5) # 增加额外等待
- except Exception as e:
- print(f"检查客户端 {idx} 播放状态出错: {str(e)}")
-
- print("第四阶段:获取默认视频路径")
- # 获取默认视频路径 - 尝试所有可能的来源
- default_video_path = self._get_default_video_path()
- if not default_video_path:
- print("错误: 无法获取任何可用的视频路径,无法下发播放指令")
- return
-
- print("第五阶段:准备播放默认视频")
- # 等待一小段时间,确保之前的停止命令完全生效
- time.sleep(1) # 增加到1秒
-
- print("第六阶段:下发播放指令")
-
- # 创建客户端循环播放设置的列表,所有客户端都不循环播放
- # 通过监控所有客户端的播放状态,当任何一台结束时重新下发
- master_client_index = self.sound_client_index
-
- # 如果sound_client_index为-1,则默认使用第一个客户端作为主客户端
- if master_client_index < 0 and len(self.kodi_server.clients) > 0:
- master_client_index = 0
- print(f"主客户端索引为-1,使用第一个客户端(索引0)作为主客户端")
-
- # 创建客户端循环设置列表 - 所有客户端都不循环播放
- client_loop_settings = []
- for i in range(len(self.kodi_server.clients)):
- client_loop_settings.append(False) # 所有客户端都不循环播放
- print(f"客户端 {i} 设置为不循环播放默认视频")
-
- print(f"主客户端索引: {master_client_index},将监控所有客户端播放状态,任何一台结束时重新下发")
-
- # 立即下发播放指令,不使用循环播放,通过监控重新下发实现循环效果
- print(f"立即下发默认视频播放指令: {default_video_path} (监控重新下发模式)")
- result = self.kodi_server.sync_play_video(
- default_video_path,
- sound_client_index=self.sound_client_index,
- default_volume=self.default_volume,
- loop=False, # 不使用循环播放,通过监控重新下发实现循环效果
- client_loop_settings=client_loop_settings # 添加客户端循环设置列表
- )
-
- if result.get("success", False):
- print("播放指令下发成功")
-
- # 等待播放开始
- time.sleep(2)
-
- # 启动监控线程,监控所有客户端的播放状态
- print("启动默认视频监控线程,监控所有客户端播放状态")
- self.is_monitoring_playback = True
- self.playback_monitor_thread = threading.Thread(
- target=self._monitor_default_video_playback
- )
- self.playback_monitor_thread.daemon = True
- self.playback_monitor_thread.start()
- print(f"默认视频监控线程已启动: is_alive={self.playback_monitor_thread.is_alive()}")
-
- # 验证是否确实开始播放
- print("验证播放是否成功启动")
- for idx, client in enumerate(self.kodi_server.clients):
- try:
- state = client.get_player_state()
- is_playing = state.get('playing', False)
- if not is_playing:
- print(f"警告:客户端 {idx} 可能未正确播放,状态: {state}")
- except Exception as e:
- print(f"验证客户端 {idx} 播放状态出错: {str(e)}")
-
- else:
- print(f"播放指令下发失败: {result.get('message', '未知错误')}")
- if result.get("failed_clients"):
- print(f"失败的客户端: {', '.join(result['failed_clients'])}")
-
- # 如果同步播放失败,尝试逐个客户端单独播放
- print("第七阶段:失败后尝试逐个播放")
- print("尝试逐个客户端单独播放默认视频")
- for idx, client in enumerate(self.kodi_server.clients):
- try:
- # 设置音量
- if idx == self.sound_client_index or self.sound_client_index == -1:
- client.set_volume(self.default_volume)
- else:
- client.set_volume(0)
-
- # 所有客户端都不循环播放
- print(f"客户端 {idx} 单独播放,不循环播放")
-
- # 播放视频
- result = client.play_video(default_video_path, loop=False)
- print(f"客户端 {idx} 单独播放结果: {result}")
- time.sleep(0.5) # 增加间隔,避免同时发送太多命令
- except Exception as e:
- print(f"客户端 {idx} 单独播放出错: {str(e)}")
-
- # 即使单独播放,也启动监控线程
- print("启动默认视频监控线程(单独播放模式)")
- self.is_monitoring_playback = True
- self.playback_monitor_thread = threading.Thread(
- target=self._monitor_default_video_playback
- )
- self.playback_monitor_thread.daemon = True
- self.playback_monitor_thread.start()
-
- print("=========== 视频播放完毕回调函数执行结束 ===========")
-
- except Exception as e:
- print(f"重新下发播放指令时出错: {str(e)}")
- import traceback
- traceback.print_exc()
-
- def _get_default_video_path(self):
- """获取默认视频路径,尝试多种来源"""
- # 尝试从self.default_video获取
- if self.default_video:
- if isinstance(self.default_video, str):
- return self.default_video
- elif isinstance(self.default_video, dict) and 'path' in self.default_video:
- return self.default_video['path']
-
- # 尝试重新加载配置
- self.default_video = ConfigLoader.load_config('default_video')
- if self.default_video:
- if isinstance(self.default_video, str):
- return self.default_video
- elif isinstance(self.default_video, dict) and 'path' in self.default_video:
- return self.default_video['path']
-
- # 尝试使用视频路径列表的第一个
- if self.video_paths and len(self.video_paths) > 0:
- return self.video_paths[0]
-
- # 没有找到可用的视频路径
- return None
-
- def _task_loop(self):
- """任务循环,持续读取串口信号"""
- if not self._open_serial():
- print("串口打开失败,任务终止")
- return
-
- print(f"开始监听串口 {self.port},等待按钮信号...")
-
- while self.running:
- # 读取信号
- data = self._read_signal()
-
- # 如果读取到有效信号
- if data:
- self._handle_serial_data(data)
-
- # 短暂休眠,避免CPU使用率过高
- time.sleep(0.1)
-
- # 循环结束,关闭串口
- self._close_serial()
- print("按钮监听任务结束")
-
- def _play_default_video(self, loop=True):
- """
- 播放默认视频
-
- Args:
- loop: 是否循环播放,默认为True
-
- Returns:
- bool: 是否成功开始播放
- """
- try:
- print(f"========== 开始加载和播放默认视频(循环={loop}) ==========")
-
- # 从配置文件重新加载默认视频配置,确保使用最新配置
- self.default_video = ConfigLoader.load_config('default_video')
- print(f"加载到的default_video配置: {self.default_video}")
-
- # 重新加载声音客户端配置
- config = ConfigLoader.load_config('ButtonListenerTask')
- if 'sound' in config:
- if 'client_index' in config['sound']:
- old_index = self.sound_client_index
- self.sound_client_index = config['sound']['client_index']
- if old_index != self.sound_client_index:
- print(f"声音客户端索引已更新: {old_index} -> {self.sound_client_index}")
- if 'volume' in config['sound']:
- old_volume = self.default_volume
- self.default_volume = config['sound']['volume']
- if old_volume != self.default_volume:
- print(f"默认音量已更新: {old_volume}% -> {self.default_volume}%")
-
- # 获取默认视频路径
- default_video_path = None
-
- if self.default_video:
- if isinstance(self.default_video, str):
- # 向后兼容:如果是字符串,直接使用作为路径
- default_video_path = self.default_video
- print(f"默认视频路径(字符串格式): {default_video_path}")
- elif isinstance(self.default_video, dict):
- # 新格式:包含path和loop字段的字典
- if 'path' in self.default_video:
- default_video_path = self.default_video['path']
- print(f"默认视频路径(字典格式): {default_video_path}")
- # 如果指定了loop参数,则使用配置中的值,除非强制覆盖
- if 'loop' in self.default_video and loop is None:
- loop = self.default_video['loop']
- print(f"从配置中获取循环设置: {loop}")
-
- if not default_video_path:
- print("警告: 没有找到默认视频配置,无法播放默认视频")
- return False
-
- print(f"即将播放默认视频: {default_video_path} (循环播放: {loop})")
-
- # 创建客户端循环播放设置的列表,默认视频所有客户端都循环播放
- # 这样默认视频就会一直播放,不需要监控
- master_client_index = self.sound_client_index
-
- # 如果sound_client_index为-1,则默认使用第一个客户端作为主客户端
- if master_client_index < 0 and len(self.kodi_server.clients) > 0:
- master_client_index = 0
- print(f"主客户端索引为-1,使用第一个客户端(索引0)作为主客户端")
-
- # 创建客户端循环设置列表 - 默认视频所有客户端都循环播放
- client_loop_settings = []
- for i in range(len(self.kodi_server.clients)):
- client_loop_settings.append(True) # 默认视频所有客户端都循环播放
- print(f"客户端 {i} 设置为循环播放默认视频")
-
- print(f"主客户端索引: {master_client_index},默认视频将在所有客户端上循环播放")
-
- # 在所有客户端上同步播放默认视频
- print("开始在所有客户端上同步播放默认视频")
- result = self.kodi_server.sync_play_video(
- default_video_path,
- sound_client_index=self.sound_client_index,
- default_volume=self.default_volume,
- loop=True, # 作为默认值传入,确保循环播放
- client_loop_settings=client_loop_settings # 添加客户端循环设置列表
- )
-
- # 更新当前播放的视频
- self.current_playing_video = default_video_path
-
- if result.get("success", False):
- print(f"所有客户端开始播放默认视频, 结果: {result}")
-
- print(f"默认视频播放成功,由主客户端索引: {self.sound_client_index} 控制")
-
- print(f"========== 成功播放默认视频 ==========")
- return True
- else:
- print(f"播放默认视频失败: {result.get('message', '未知错误')}")
- if result.get("failed_clients"):
- print(f"失败的客户端: {', '.join(result['failed_clients'])}")
- print(f"========== 播放默认视频失败 ==========")
- return False
-
- except Exception as e:
- print(f"播放默认视频时出错: {str(e)}")
- import traceback
- traceback.print_exc()
- print(f"========== 播放默认视频出错 ==========")
- return False
-
- def start_monitoring(self):
- """
- 开始监听串口信号。
-
- Returns:
- bool: 是否成功启动监听
- """
- if self.thread and self.thread.is_alive():
- print("警告:监听任务已经在运行")
- return False
-
- if not self.serial or not self.serial.is_open:
- if not self._open_serial():
- print("错误:无法打开串口")
- return False
-
- self.running = True
- self.thread = SerialMonitorThread(
- self.serial,
- self._handle_serial_data,
- interval=0.1
- )
- self.thread.start()
-
- # 启动时自动播放默认视频(循环播放)
- self._play_default_video(loop=True)
-
- return True
-
- def interrupt(self):
- """
- 中断监听任务。
-
- Returns:
- bool: 是否成功中断
- """
- if not self.thread or not self.thread.is_alive():
- print("警告:没有运行中的监听任务")
- return False
-
- self.running = False
- self.thread.join(timeout=2) # 等待线程结束,最多等待2秒
-
- # 停止播放状态监控线程
- if self.playback_monitor_thread and self.playback_monitor_thread.is_alive():
- self.is_monitoring_playback = False
- self.playback_monitor_thread.join(timeout=1)
-
- # 停止状态监控线程
- self.state_monitor.stop()
-
- if self.thread.is_alive():
- print("警告:监听任务未能在超时时间内结束")
- return False
- else:
- print("监听任务已成功中断")
- return True
-
- def play(self, video_path=None, client_index=None, loop=False):
- """
- 播放视频方法,实现抽象基类的抽象方法。
-
- Args:
- video_path: 视频路径,如果为None则使用默认视频
- client_index: 客户端索引,如果为None则在所有客户端上播放
- loop: 是否循环播放
-
- Returns:
- bool: 是否成功开始播放
- """
- try:
- # 重新加载声音客户端配置
- config = ConfigLoader.load_config('ButtonListenerTask')
- if 'sound' in config:
- if 'client_index' in config['sound']:
- old_index = self.sound_client_index
- self.sound_client_index = config['sound']['client_index']
- if old_index != self.sound_client_index:
- print(f"声音客户端索引已更新: {old_index} -> {self.sound_client_index}")
- if 'volume' in config['sound']:
- old_volume = self.default_volume
- self.default_volume = config['sound']['volume']
- if old_volume != self.default_volume:
- print(f"默认音量已更新: {old_volume}% -> {self.default_volume}%")
-
- # 如果未指定视频路径,且有加载的视频路径列表,则使用第一个视频
- if video_path is None and self.video_paths:
- video_path = self.video_paths[0]
-
- # 如果仍未指定视频路径,使用默认视频
- if video_path is None and self.default_video:
- if isinstance(self.default_video, str):
- video_path = self.default_video
- elif isinstance(self.default_video, dict) and 'path' in self.default_video:
- video_path = self.default_video['path']
-
- if video_path is None:
- print("错误:未指定视频路径且无默认视频可用")
- return False
-
- # 停止之前的监控线程
- if self.playback_monitor_thread and self.playback_monitor_thread.is_alive():
- self.is_monitoring_playback = False
- self.playback_monitor_thread.join(timeout=0.5)
-
- # 记录当前播放的视频
- self.current_playing_video = video_path
-
- # 如果未指定客户端索引,则在所有客户端上播放
- if client_index is None:
- # 在所有客户端上同步播放视频
- result = self.kodi_server.sync_play_video(
- video_path,
- sound_client_index=self.sound_client_index,
- default_volume=self.default_volume,
- loop=loop
- )
- if result.get("success", False):
- print("所有客户端开始播放视频")
-
- # 如果不是循环播放,启动监控线程
- if not loop:
- # 启动监控线程,监控视频播放状态
- print("启动播放监控线程,将在主客户端视频结束时自动切换回默认视频")
- self.is_monitoring_playback = True
- self.playback_monitor_thread = threading.Thread(
- target=self._monitor_playback
- )
- self.playback_monitor_thread.daemon = True
- self.playback_monitor_thread.start()
-
- return True
- else:
- print(f"播放失败: {result.get('message', '未知错误')}")
- if result.get("failed_clients"):
- print(f"失败的客户端: {', '.join(result['failed_clients'])}")
- # 如果播放失败,尝试播放默认视频
- print("播放失败,尝试恢复默认视频")
- self._play_default_video(loop=True)
- else:
- # 在指定客户端上播放视频
- if client_index < len(self.kodi_server.clients):
- client = self.kodi_server.clients[client_index]
- # 为单个客户端设置音量
- client.set_volume(self.default_volume)
- result = client.play_video(video_path, loop=loop)
-
- if result:
- print(f"客户端 {client_index} 开始播放视频")
-
- # 如果不是循环播放,启动单客户端监控
- if not loop:
- # 使用简单的监控方法,而不是PlaybackMonitorThread
- print(f"启动客户端 {client_index} 的简单播放监控")
-
- # 创建一个简单的监控线程,只检测播放结束
- monitor_thread = threading.Thread(
- target=self._simple_monitor_client,
- args=(client_index,)
- )
- monitor_thread.daemon = True
- monitor_thread.start()
-
- return True
- else:
- print(f"播放失败")
- return False
- else:
- print(f"错误:无效的客户端索引 {client_index}")
- return False
-
- except Exception as e:
- print(f"播放视频时出错: {str(e)}")
- return False
- def _simple_monitor_client(self, client_index):
- """简单的客户端监控,只检测播放是否结束,不做seek操作"""
- try:
- print(f"开始监控客户端 {client_index} 的播放状态")
-
- # 等待视频加载
- time.sleep(2)
-
- # 持续检查客户端是否在播放
- while True:
- # 获取客户端状态
- state = self.state_monitor.get_state(client_index)
- is_playing = state.get('playing', False) and not state.get('paused', True)
-
- # 如果客户端不再播放,触发回调
- if not is_playing:
- print(f"客户端 {client_index} 已停止播放,准备切换到默认视频")
- self._on_playback_finished()
- break
-
- # 短暂休眠,避免CPU使用率过高
- time.sleep(1)
-
- except Exception as e:
- print(f"监控客户端 {client_index} 时出错: {str(e)}")
- # 如果出错,尝试播放默认视频
- self._play_default_video(loop=True)
- def _monitor_default_video_playback(self):
- """监控默认视频的播放状态,当任何一台客户端播放结束时重新下发播放指令"""
- try:
- print("开始监控所有客户端的默认视频播放状态")
-
- # 等待视频加载和开始播放
- print("等待5秒让默认视频充分加载并稳定播放...")
- time.sleep(5)
-
- # 获取所有客户端的初始状态
- initial_states = {}
- for idx in range(len(self.kodi_server.clients)):
- state = self.state_monitor.get_state(idx)
- initial_states[idx] = {
- 'file': state.get('current_file', ''),
- 'duration': state.get('totaltime', 0)
- }
- print(f"客户端 {idx} 初始状态: 文件={initial_states[idx]['file']}, 时长={initial_states[idx]['duration']}秒")
-
- # 监控变量初始化
- check_counter = 0
-
- # 监控循环
- while self.is_monitoring_playback:
- check_counter += 1
-
- # 检查所有客户端的播放状态
- any_client_finished = False
-
- for idx in range(len(self.kodi_server.clients)):
- try:
- # 获取当前客户端状态
- state = self.state_monitor.get_state(idx)
- is_playing = state.get('playing', False) and not state.get('paused', True)
- current_time = state.get('time', 0)
- current_file = state.get('current_file', '')
- duration = initial_states[idx]['duration']
-
- # 每20次检查输出一次详细状态信息
- if check_counter % 20 == 0:
- print(f"客户端 {idx} 播放检查 #{check_counter}: 时间={current_time:.1f}/{duration:.1f}秒, 播放状态={is_playing}")
-
- # 检查是否文件已经变更(可能已切换到其他视频)
- if current_file != initial_states[idx]['file'] and initial_states[idx]['file'] and current_file:
- print(f"客户端 {idx} 检测到文件变更: {initial_states[idx]['file']} -> {current_file},视频可能已结束")
- any_client_finished = True
- break
-
- # 如果能获取到总时长且接近结束,认为播放结束
- if duration > 0 and current_time >= duration - 3:
- print(f"客户端 {idx} 视频即将结束(当前进度: {current_time:.1f}/{duration:.1f}秒)")
- any_client_finished = True
- break
-
- # 如果客户端不在播放状态,认为播放结束
- if not is_playing:
- print(f"客户端 {idx} 不在播放状态,认为播放结束")
- any_client_finished = True
- break
-
- except Exception as e:
- print(f"检查客户端 {idx} 状态时出错: {str(e)}")
- # 如果检查状态出错,也认为需要重新下发
- any_client_finished = True
- break
-
- # 如果任何一台客户端播放结束,重新下发播放指令
- if any_client_finished:
- print("检测到有客户端播放结束,准备重新下发默认视频播放指令")
- self.is_monitoring_playback = False
-
- # 延迟一小段时间再重新下发,避免频繁操作
- time.sleep(1)
-
- # 重新下发播放指令
- self._play_default_video(loop=True)
- break
-
- # 短暂休眠
- time.sleep(0.5)
-
- except Exception as e:
- print(f"监控默认视频播放状态时出错: {str(e)}")
- import traceback
- traceback.print_exc()
- self.is_monitoring_playback = False
- # 如果监控出错,尝试重新播放默认视频
- time.sleep(2)
- self._play_default_video(loop=True)
|