Files
MediaCast3/ui/main_window.py

453 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()