mirror of
https://gitee.com/yqsphp/MediaCast.git
synced 2026-05-22 20:55:48 +08:00
453 lines
18 KiB
Python
453 lines
18 KiB
Python
import sys
|
||
import os
|
||
import threading
|
||
|
||
from PyQt5.QtCore import Qt
|
||
from PyQt5.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QListWidgetItem
|
||
from core.device_discovery import DeviceDiscovery
|
||
from core.dlna_controller import DLNAController
|
||
from core.http_file_server import HTTPFileServer
|
||
from core.logger import AppLogger
|
||
from core.device_list import DeviceList
|
||
from ui.ui_window import UiWindow
|
||
|
||
|
||
class MainWindow(QMainWindow, UiWindow):
|
||
"""智能媒体投屏器主窗口"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
# 驱动服务
|
||
self.device_discovery = DeviceDiscovery()
|
||
# 驱动控制
|
||
self.dlna_controller = None
|
||
# http服务端口
|
||
self.http_port = 19735
|
||
# http服务
|
||
self.http_server = HTTPFileServer(self.http_port)
|
||
# 当前选中的设备
|
||
self.selected_device = None
|
||
# 选中设备列表
|
||
self.selected_device_list = DeviceList()
|
||
# 选择文件
|
||
self.selected_file = None
|
||
# 播放速度
|
||
self.speed = 1.0;
|
||
# 日志
|
||
self.logger = AppLogger('MediaCastUI')
|
||
# 初始化ui
|
||
self.init_ui()
|
||
# 设备发现
|
||
self.refresh_devices()
|
||
# 启动HTTP文件服务器
|
||
self.start_http_server()
|
||
|
||
def start_http_server(self):
|
||
"""启动HTTP文件服务器"""
|
||
try:
|
||
if self.http_server.start():
|
||
self.log_message("HTTP文件服务器启动成功")
|
||
self.file_service_label.setText("运行中")
|
||
self.file_service_label.setStyleSheet("color: #20c997; font-weight: bold;")
|
||
else:
|
||
self.log_message("HTTP文件服务器启动失败", "error")
|
||
self.file_service_label.setText("异常")
|
||
self.file_service_label.setStyleSheet("color: #dc3545; font-weight: bold;")
|
||
except Exception as e:
|
||
self.log_message(f"启动HTTP服务器出错: {e}", "error")
|
||
|
||
def refresh_devices(self):
|
||
"""刷新设备列表"""
|
||
self.log_message("开始搜索网络中的投屏设备...", "info")
|
||
self.refresh_btn.setEnabled(False)
|
||
self.refresh_btn.setText("搜索中...")
|
||
|
||
# 在线程中进行,避免阻塞UI
|
||
def do_quick_discovery():
|
||
# 获取刷新后设备信息
|
||
new_device = self.device_discovery.discover_media_renderers()
|
||
self.update_device(new_device, self.selected_device)
|
||
"""启用刷新按钮"""
|
||
self.refresh_btn.setEnabled(True)
|
||
self.refresh_btn.setText("刷新设备")
|
||
|
||
# 确保在UI线程中更新
|
||
thread = threading.Thread(target=do_quick_discovery, daemon=True)
|
||
thread.start()
|
||
|
||
def update_device(self, new_devices, current_selected_device=None):
|
||
"""
|
||
更新设备列表显示,保留选中状态(使用UDN作为唯一标识)
|
||
Args:
|
||
new_devices: 搜索的设备
|
||
current_selected_device:当前选中的设备
|
||
"""
|
||
#当前选中设备
|
||
current_selected_udn = None
|
||
if current_selected_device:
|
||
current_selected_udn = current_selected_device.get('udn')
|
||
|
||
# 创建新设备字典,以UDN为键
|
||
new_device_dict = {}
|
||
for device in new_devices:
|
||
device_udn = device.get('udn')
|
||
if device_udn: # 只有有UDN的设备才处理
|
||
new_device_dict[device_udn] = device
|
||
|
||
# 获取之前列表中所有设备的UDN
|
||
current_device_udn_list = []
|
||
for i in range(self.device_list.count()):
|
||
item = self.device_list.item(i)
|
||
device = item.data(Qt.UserRole)
|
||
device_udn = device.get('udn')
|
||
if device_udn:
|
||
current_device_udn_list.append(device_udn)
|
||
|
||
# 移除不存在的设备
|
||
items_to_remove = []
|
||
for i in range(self.device_list.count()):
|
||
item = self.device_list.item(i)
|
||
device = item.data(Qt.UserRole)
|
||
|
||
# 获取设备的标识(优先使用UDN,没有则用IP)
|
||
device_udn = device.get('udn')
|
||
# 如果设备标识不在新设备列表中,标记为删除
|
||
if device_udn not in new_device_dict:
|
||
items_to_remove.append(i)
|
||
|
||
# 从后往前删除,避免索引变化
|
||
for index in reversed(items_to_remove):
|
||
self.device_list.takeItem(index)
|
||
# 如果删除的是当前选中的设备,清空选中状态
|
||
if item and item == self.device_list.currentItem():
|
||
self.device_list.clearSelection()
|
||
#移除选中设备列表中的不存在的设备
|
||
self.selected_device = None
|
||
|
||
# 更新或添加设备
|
||
current_selected_item = None
|
||
|
||
for device in new_devices:
|
||
# 获取设备标识(优先使用UDN,没有则用IP)
|
||
device_identifier = device.get('udn')
|
||
|
||
device_name = device.get('friendly_name', '未知设备')
|
||
device_ip = device.get('ip', '未知IP')
|
||
|
||
# 创建显示文本
|
||
if device.get('udn'):
|
||
# 简化UDN显示,只显示最后一部分
|
||
udn_parts = device_identifier.split(':')
|
||
udn_display = udn_parts[-1][:8] if len(udn_parts) > 1 else device_identifier[:8]
|
||
item_text = f"{device_name} [{device_ip}]"
|
||
else:
|
||
item_text = f"{device_name} [{device_ip}] (无UDN)"
|
||
|
||
# 检查设备是否已存在
|
||
found_item = None
|
||
for i in range(self.device_list.count()):
|
||
item = self.device_list.item(i)
|
||
existing_device = item.data(Qt.UserRole)
|
||
|
||
# 比较设备标识
|
||
existing_identifier = existing_device.get('udn')
|
||
if not existing_identifier:
|
||
existing_identifier = existing_device.get('ip', '')
|
||
|
||
if existing_identifier == device_identifier:
|
||
found_item = item
|
||
break
|
||
|
||
if found_item:
|
||
# 设备已存在,更新显示文本和设备数据
|
||
if found_item.text() != item_text:
|
||
found_item.setText(item_text)
|
||
# 更新设备数据(可能有新信息)
|
||
found_item.setData(Qt.UserRole, device)
|
||
|
||
# 检查是否是之前选中的设备
|
||
if current_selected_udn and device.get('udn') == current_selected_udn:
|
||
current_selected_item = found_item
|
||
else:
|
||
# 设备不存在,添加新项目
|
||
item = QListWidgetItem(item_text)
|
||
item.setData(Qt.UserRole, device)
|
||
self.device_list.addItem(item)
|
||
|
||
# 检查是否是之前选中的设备
|
||
if current_selected_udn and device.get('udn') == current_selected_udn:
|
||
current_selected_item = item
|
||
|
||
# 恢复选中状态
|
||
if current_selected_item:
|
||
self.device_list.setCurrentItem(current_selected_item)
|
||
self.selected_device = current_selected_item.data(Qt.UserRole)
|
||
|
||
device_name = self.selected_device.get('friendly_name', '未知设备')
|
||
self.log_message(f"已恢复选择设备: {device_name}")
|
||
elif self.device_list.count() > 0 and self.device_list.currentItem() is None:
|
||
# 如果之前选中的设备不存在了,清空选中
|
||
self.device_list.clearSelection()
|
||
self.selected_device = None
|
||
elif self.device_list.count() == 0:
|
||
# 没有设备
|
||
self.selected_device = None
|
||
|
||
# 更新设备数量统计
|
||
device_count = self.device_list.count()
|
||
if device_count > 0:
|
||
self.log_message(f"发现 {device_count} 个投屏设备")
|
||
|
||
# 如果有选中的设备,启用控制按钮
|
||
if self.selected_device:
|
||
device_name = self.selected_device.get('friendly_name', '未知设备')
|
||
self.log_message(f"当前选中: {device_name}")
|
||
else:
|
||
self.log_message("未发现任何投屏设备", "warning")
|
||
self.selected_device = None
|
||
|
||
def select_device(self, item):
|
||
"""从列表中选择设备"""
|
||
device = item.data(Qt.UserRole)
|
||
if device:
|
||
self.selected_device = device
|
||
self.log_message(f"已选择设备: {device.get('friendly_name', '未知')}")
|
||
# 将选中设备添加到已选中设备列表中
|
||
self.selected_device_list.add_device(device)
|
||
# 获取当前选中设备当前播放状态
|
||
stat = self.selected_device_list.get_device(device)
|
||
#如果当前设备已存在,将控制对象赋值
|
||
if stat != False and stat["controller"] is not None:
|
||
# 重新刷新判断获取设备uuid的location是否一致
|
||
if stat["location"] == device["location"]:
|
||
self.dlna_controller = stat["controller"]
|
||
else:
|
||
# 当投屏设备发生强制结束时,再次启动它的端口会变,这时从新实例化控制对象
|
||
self.dlna_controller = DLNAController(device)
|
||
# 还原控制按钮
|
||
self.selected_device_list.update_device(device, False, self.dlna_controller, False, device["location"])
|
||
self.enable_control_buttons(True)
|
||
|
||
# 当前选中设备如果正在播放中,控制按钮都不变
|
||
if stat != False and stat["stat"] == True:
|
||
self.enable_control_buttons(False)
|
||
self.selected_device_list.update_device(device, True)
|
||
|
||
# 获取当前设备是否是暂停播放
|
||
if stat["pause"]:
|
||
self.start_btn.setText("继续播放")
|
||
self.start_btn.setEnabled(True)
|
||
self.pause_btn.setEnabled(False)
|
||
else:
|
||
# 启用控制按钮
|
||
self.start_btn.setText("开始投屏")
|
||
self.enable_control_buttons(True)
|
||
|
||
def browse_file(self):
|
||
"""浏览并选择媒体文件"""
|
||
file_dialog = QFileDialog()
|
||
file_dialog.setFileMode(QFileDialog.ExistingFile)
|
||
|
||
# 设置文件过滤器
|
||
file_dialog.setNameFilter(
|
||
"媒体文件 (*.mp4 *.avi *.mkv *.mov *.mp3 *.wav *.flac *.wmv *.flv *.webm "
|
||
"*.mpeg *.mpg *.m4v *.3gp *.m4a *.aac *.ogg *.wma *.ape *.alac "
|
||
"*.amr *.opus *.ac3 *.dts *.ts *.m2ts *.vob *.ogv *.asf *.rm "
|
||
"*.rmvb *.divx *.xvid *.f4v *.m2v *.mpe *.mp2 *.mp1 *.ra *.ram "
|
||
"*.mid *.midi *.cda *.gsm *.vox *.voc *.dvf *.raw *.mka *.tta "
|
||
"*.spx *.8svx *.aiff *.au *.snd *.paf *.svx *.nist *.ircam "
|
||
"*.voc *.smp *.vox *.sou);;"
|
||
)
|
||
|
||
if file_dialog.exec_():
|
||
files = file_dialog.selectedFiles()
|
||
if files:
|
||
self.selected_file = files[0]
|
||
file_name = os.path.basename(self.selected_file)
|
||
self.file_label.setText(file_name)
|
||
#选择文件后启用播放按钮
|
||
self.enable_control_buttons(True)
|
||
self.log_message(f"已选择文件: {file_name}")
|
||
|
||
def start_casting(self):
|
||
"""开始投屏"""
|
||
if not self.select_device_file_stat():
|
||
return
|
||
|
||
try:
|
||
# 检查当前选中设备的控制对象
|
||
device = self.selected_device_list.get_device(self.selected_device)
|
||
# 如果是第一次播放,需要创建DLNA控制器并设置URI
|
||
if device != False and device["controller"] is None:
|
||
# 创建DLNA控制器
|
||
self.dlna_controller = DLNAController(self.selected_device)
|
||
# 更新选中设备的播放控制器
|
||
self.selected_device_list.update_device(self.selected_device, True, self.dlna_controller, False)
|
||
# 启动HTTP服务器并获取文件URL
|
||
file_name = os.path.basename(self.selected_file)
|
||
dir_bool = self.http_server.set_root_directory(self.selected_file)
|
||
self.log_message("成功" if dir_bool else "失败")
|
||
|
||
# 获取IP地址(从IP标签中提取)
|
||
ip_text = self.ip_label.text()
|
||
file_url = f"http://{ip_text}/{file_name}"
|
||
self.log_message(file_url)
|
||
|
||
# 设置播放URI并开始播放
|
||
if self.dlna_controller.set_av_transport_uri(file_url, file_name):
|
||
self.dlna_controller.play(self.speed)
|
||
#设置控制按钮
|
||
self.enable_control_buttons(False)
|
||
|
||
self.log_message(f"开始投屏: {file_name}")
|
||
self.statusBar().showMessage(f"正在投屏: {file_name}")
|
||
#self.is_paused = False # 确保不是暂停状态
|
||
#self.selected_device_list.update_device(self.selected_device, False)
|
||
else:
|
||
QMessageBox.critical(self, "错误", "投屏失败,请检查设备连接")
|
||
return
|
||
else:
|
||
# 如果已经创建了DLNA控制器
|
||
if device != False and device["pause"] == True:
|
||
# 从暂停状态恢复播放
|
||
if self.dlna_controller.play(self.speed):
|
||
self.selected_device_list.update_device(self.selected_device, True, pause=False)
|
||
# 设置控制按钮
|
||
self.enable_control_buttons(False)
|
||
self.log_message("继续播放")
|
||
else:
|
||
QMessageBox.critical(self, "错误", "继续播放失败")
|
||
else:
|
||
# 如果不是暂停状态,说明是停止后的重新播放
|
||
# 需要重新设置URI
|
||
file_name = os.path.basename(self.selected_file)
|
||
ip_text = self.ip_label.text()
|
||
dir_bool = self.http_server.set_root_directory(self.selected_file)
|
||
self.log_message("成功" if dir_bool else "失败")
|
||
file_url = f"http://{ip_text}/{file_name}"
|
||
|
||
if self.dlna_controller.set_av_transport_uri(file_url, file_name):
|
||
self.dlna_controller.play(self.speed)
|
||
self.log_message(f"播放速度:{self.speed}")
|
||
#设置暂停状态
|
||
self.selected_device_list.update_device(self.selected_device, True, pause=False)
|
||
self.enable_control_buttons(False)
|
||
|
||
self.log_message(f"重新开始投屏: {file_name}")
|
||
self.statusBar().showMessage(f"正在投屏: {file_name}")
|
||
else:
|
||
QMessageBox.critical(self, "错误", "重新投屏失败")
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"投屏时出错: {str(e)}")
|
||
self.log_message(f"投屏失败: {str(e)}", "error")
|
||
|
||
def pause_casting(self):
|
||
"""暂停投屏"""
|
||
if not self.select_device_file_stat():
|
||
return
|
||
|
||
if self.dlna_controller:
|
||
self.dlna_controller.pause()
|
||
# 重置暂停状态
|
||
self.selected_device_list.update_device(self.selected_device, True, pause=True)
|
||
self.start_btn.setText("继续播放")
|
||
self.start_btn.setEnabled(True)
|
||
self.pause_btn.setEnabled(False)
|
||
|
||
self.log_message("已暂停播放")
|
||
|
||
def stop_casting(self):
|
||
"""停止投屏"""
|
||
if not self.select_device_file_stat():
|
||
return
|
||
|
||
if self.dlna_controller:
|
||
self.dlna_controller.stop()
|
||
# 重置停止状态
|
||
self.selected_device_list.update_device(self.selected_device, False, pause=False)
|
||
# 重置控制
|
||
self.start_btn.setText("开始投屏")
|
||
self.enable_control_buttons(True)
|
||
self.log_message("已停止播放")
|
||
|
||
def volume_changed(self, value):
|
||
"""音量改变"""
|
||
if not self.select_device_file_stat():
|
||
return
|
||
|
||
if self.dlna_controller:
|
||
self.dlna_controller.set_volume(value)
|
||
|
||
def toggle_mute(self, checked):
|
||
"""切换静音"""
|
||
if not self.select_device_file_stat():
|
||
return
|
||
|
||
if self.dlna_controller:
|
||
self.dlna_controller.set_mute(checked)
|
||
icon = "🔇" if checked else "🔊"
|
||
self.mute_btn.setText(icon)
|
||
|
||
status = "静音" if checked else "取消静音"
|
||
self.log_message(status)
|
||
|
||
def speed_selected(self):
|
||
"""
|
||
设置播放速度
|
||
:return:
|
||
"""
|
||
if not self.select_device_file_stat():
|
||
return
|
||
|
||
self.speed = self.speed_btn.currentData()
|
||
self.log_message(f"设置播放速度:{self.speed_btn.currentText()}")
|
||
|
||
def log_message(self, message, level="info"):
|
||
self.logger.info(message)
|
||
# 同时输出到状态栏
|
||
if level == "error":
|
||
self.statusBar().showMessage(f"错误: {message}", 1000)
|
||
elif level == "warning":
|
||
self.statusBar().showMessage(f"警告: {message}", 1000)
|
||
else:
|
||
self.statusBar().showMessage(message, 1000)
|
||
|
||
def enable_control_buttons(self, enabled):
|
||
"""启用/禁用控制按钮"""
|
||
if self.selected_device and self.selected_file:
|
||
self.start_btn.setEnabled(enabled)
|
||
self.pause_btn.setEnabled(False if enabled else True)
|
||
self.stop_btn.setEnabled(False if enabled else True)
|
||
self.volume_slider.setEnabled(False if enabled else True)
|
||
self.mute_btn.setEnabled(False if enabled else True)
|
||
|
||
def select_device_file_stat(self):
|
||
"""
|
||
控制按钮都需要验证
|
||
:return:
|
||
"""
|
||
if not self.selected_device :
|
||
self.log_message("请先选择设备", "error")
|
||
return False
|
||
if not self.selected_file:
|
||
self.log_message("请先选择文件", "error")
|
||
return False
|
||
|
||
return True
|
||
|
||
def closeEvent(self, event):
|
||
"""窗口关闭事件"""
|
||
# 停止HTTP服务器
|
||
if self.http_server:
|
||
self.http_server.stop()
|
||
|
||
# 停止DLNA控制器
|
||
if self.dlna_controller:
|
||
try:
|
||
self.dlna_controller.stop()
|
||
except:
|
||
pass
|
||
|
||
self.log_message("应用程序已关闭")
|
||
event.accept() |