From 90c882997d68cfd6c1ccd45a9eb08a839288f7a9 Mon Sep 17 00:00:00 2001 From: yqsphp Date: Mon, 12 Jan 2026 10:48:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AA=92=E4=BD=93=E6=8A=95=E5=B1=8F=E5=88=9D?= =?UTF-8?q?=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 70 ++++ __init__.py | 21 + core/__init__.py | 15 + core/device_discovery.py | 488 +++++++++++++++++++++++ core/dlna_controller.py | 658 ++++++++++++++++++++++++++++++ core/http_file_server.py | 693 ++++++++++++++++++++++++++++++++ core/logger.py | 83 ++++ icon.ico | Bin 0 -> 106528 bytes main.py | 30 ++ ui/__init__.py | 11 + ui/main_window.py | 841 +++++++++++++++++++++++++++++++++++++++ 12 files changed, 2911 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 core/__init__.py create mode 100644 core/device_discovery.py create mode 100644 core/dlna_controller.py create mode 100644 core/http_file_server.py create mode 100644 core/logger.py create mode 100644 icon.ico create mode 100644 main.py create mode 100644 ui/__init__.py create mode 100644 ui/main_window.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a09c56d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..00afa76 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# 媒体投屏器 (Media Caster) + +

+ 媒体投屏器 Logo +
+ 一个优雅、高效的本地媒体投屏工具 +

+ +

+ 功能特性 • + 快速开始 • + 使用指南 • + 技术架构 • + 开发指南 +

+ +

+ Python版本 + PyQt5 + 许可证 + 平台支持 +

+ +## 🌟 简介 + +媒体投屏器是一款基于 Python 和 PyQt5 开发的高性能媒体投屏工具,支持将本地音视频文件通过 DLNA/UPnP 协议投屏到智能电视、投影仪等设备。 + +### 主要特点 +- 🎯 **一键投屏**:简单三步完成媒体投屏 +- 📱 **设备自动发现**:智能扫描局域网内的投屏设备 +- ⚡ **高性能传输**:内置 HTTP 服务器,流畅播放体验 + +## ✨ 功能特性 + +### 🚀 核心功能 +| 功能 | 描述 | 状态 | +|------|------|------| +| **设备发现** | 自动扫描局域网内 DLNA/UPnP 设备 | ✅ | +| **文件浏览** | 支持多种音视频格式选择 | ✅ | +| **投屏控制** | 播放、暂停、停止、音量控制 | ✅ | +| **音量调节** | 滑块控制、静音切换 | ✅ | +| **多设备支持** | 同时发现和管理多个设备 | ✅ | + + +### 🖥️ 系统要求 +- **操作系统**: Windows 10/11, macOS 10.15+, Ubuntu 18.04+ +- **Python版本**: Python 3.7 或更高版本 +- **内存要求**: 最少 4GB RAM +- **网络要求**: 设备与电脑需在同一局域网 + +## 🚀 快速开始 + +### 安装方法 + +#### 方法一:从源代码安装(推荐) +```bash +# 1. 克隆仓库 +git clone https://github.com/yqsphp/media-caster.git +cd media-caster + +# 2. 创建虚拟环境 +python -m venv venv + +# Windows +venv\Scripts\activate +# Linux/Mac +source venv/bin/activate + +# 3. 运行程序 +python main.py \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f89fb24 --- /dev/null +++ b/__init__.py @@ -0,0 +1,21 @@ +# __init__.py (项目根目录) +""" +本地媒体投屏应用 + +此应用支持将本地媒体文件投屏到投影仪、电视等设备。 +支持多协议自动检测,包括DLNA、Chromecast等设备。 +""" + +__version__ = "1.0.1" +__author__ = "Local Cast" +__description__ = "本地媒体投屏工具" + +# 定义公开的接口 +__all__ = [ + "DeviceDiscovery", + "MainWindow" +] + +# 导入核心功能,便于直接访问 +from core.device_discovery import * +from ui.main_window import * diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..655c966 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1,15 @@ +# core/__init__.py +""" +核心功能模块 +包含设备发现、投屏服务和设备管理等功能类。 +""" +from .logger import AppLogger +from .device_discovery import DeviceDiscovery +from .dlna_controller import DLNAController +from .http_file_server import HTTPFileServer +__all__ = [ + "AppLogger", + "DeviceDiscovery", + "DLNAController", + "HTTPFileServer" +] diff --git a/core/device_discovery.py b/core/device_discovery.py new file mode 100644 index 0000000..45617e3 --- /dev/null +++ b/core/device_discovery.py @@ -0,0 +1,488 @@ +import socket +import time +import urllib.request +import urllib.error +import xml.etree.ElementTree as ET +from core.logger import AppLogger + +class DeviceDiscovery: + """ + DLNA/UPnP 媒体渲染器发现类 + 用于发现局域网中的投屏设备 + """ + + def __init__(self): + """ + 初始化设备发现器 + """ + self.logger = AppLogger('DeviceDiscovery') + self.discovered_devices = [] + + def discover_media_renderers(self, timeout=3): + """ + 发现局域网中的媒体渲染器(投屏设备) + 只返回 deviceType 为 urn:schemas-upnp-org:device:MediaRenderer:1 的设备 + 通过 UUID 去重 + + Args: + timeout: 搜索超时时间(秒) + + Returns: + list: 发现的设备列表 + """ + # 首先检查网络连接 + try: + test_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + test_sock.settimeout(1) + test_sock.sendto(b'test', ('239.255.255.250', 1900)) + test_sock.recvfrom(1024) + test_sock.close() + except socket.timeout: + self.logger.warning("网络可能没有响应,检查防火墙设置") + except Exception as e: + self.logger.warning(f"网络测试异常: {e}") + + devices_by_uuid = {} # 用UUID去重 + pending_devices = {} # 待处理的设备,key为location,value为(ip, headers) + + # SSDP 发现消息 + ssdp_msg = ( + "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "MX: 3\r\n" + "ST: ssdp:all\r\n" + "USER-AGENT: Python DLNA Discovery\r\n" + "\r\n" + ) + + # 创建 UDP socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + sock.bind(('0.0.0.0', 0)) + + # 发送发现请求 + sock.sendto(ssdp_msg.encode(), ('239.255.255.250', 1900)) + sock.setblocking(0) # 设置为非阻塞模式 + + self.logger.info(f"开始搜索媒体渲染器,超时时间: {timeout}秒...") + + # 使用 select 监听socket,避免while循环 + import select + start_time = time.time() + response_count = 0 + + while True: + # 计算剩余超时时间 + remaining_time = timeout - (time.time() - start_time) + if remaining_time <= 0: + break + + # 使用select监听socket + ready_to_read, _, _ = select.select([sock], [], [], remaining_time) + + if sock in ready_to_read: + try: + data, addr = sock.recvfrom(4096) + response_count += 1 + response = data.decode('utf-8', errors='ignore') + + # 解析SSDP响应头 + lines = response.split('\r\n') + headers = {} + for line in lines: + if ':' in line: + key, value = line.split(':', 1) + headers[key.strip().upper()] = value.strip() + + # 检查是否有LOCATION头 + location = headers.get('LOCATION') + if location: + pending_devices[location] = (addr[0], headers) + + except socket.error as e: + self.logger.debug(f"接收数据时出错: {e}") + continue + else: + # select 超时 + break + + sock.close() + + # 并发获取设备详情 + if pending_devices: + from concurrent.futures import ThreadPoolExecutor, as_completed + + self.logger.info(f"收到{response_count}个SSDP响应,准备并发获取{len(pending_devices)}个设备详情...") + + with ThreadPoolExecutor(max_workers=min(10, len(pending_devices))) as executor: + # 提交所有设备详情获取任务 + future_to_location = { + executor.submit(self._get_device_details, location, ip, headers): location + for location, (ip, headers) in pending_devices.items() + } + + # 收集结果 + for future in as_completed(future_to_location): + try: + device_info = future.result(timeout=5) + if device_info: + uuid = device_info.get('udn', '') + if uuid and uuid not in devices_by_uuid: + devices_by_uuid[uuid] = device_info + device_name = device_info.get('friendly_name', '未知') + self.logger.info(f"发现新设备: {device_name} (UUID: {uuid[:20]}...)") + except Exception as e: + location = future_to_location[future] + self.logger.debug(f"获取设备详情失败 {location}: {e}") + + self.logger.info(f"搜索完成: 发现{len(devices_by_uuid)}个媒体渲染器") + self.discovered_devices = list(devices_by_uuid.values()) + + return self.discovered_devices + + def _get_device_details(self, location, ip, headers): + """ + 获取单个设备的详细信息(提取出来便于并发调用) + + Args: + location: 设备描述文档URL + ip: 设备IP地址 + headers: SSDP响应头 + + Returns: + dict: 设备信息,如果不是MediaRenderer则返回None + """ + usn = headers.get('USN', '') + st = headers.get('ST', headers.get('NT', '')) + + try: + # 获取设备描述文档 + req = urllib.request.Request(location) + req.add_header('User-Agent', 'Python DLNA Client') + req.timeout = 3 + + with urllib.request.urlopen(req, timeout=3) as response: + xml_data = response.read().decode('utf-8', errors='ignore') + + # 解析 XML + root = ET.fromstring(xml_data) + ns = {'upnp': 'urn:schemas-upnp-org:device-1-0'} + + # 提取设备信息 + device = root.find('.//upnp:device', ns) + if device is None: + self.logger.debug(f"从 {location} 未找到设备信息") + return None + + # 检查设备类型 + device_type_elem = device.find('upnp:deviceType', ns) + if device_type_elem is None or device_type_elem.text != 'urn:schemas-upnp-org:device:MediaRenderer:1': + return None # 不是 MediaRenderer 设备,跳过 + + # 提取设备信息 + friendly_name_elem = device.find('upnp:friendlyName', ns) + manufacturer_elem = device.find('upnp:manufacturer', ns) + model_name_elem = device.find('upnp:modelName', ns) + model_description_elem = device.find('upnp:modelDescription', ns) + udn_elem = device.find('upnp:UDN', ns) + + # 提取服务列表 + services = [] + service_list = root.find('.//upnp:serviceList', ns) + if service_list is not None: + for service in service_list.findall('upnp:service', ns): + service_type_elem = service.find('upnp:serviceType', ns) + if service_type_elem is not None: + services.append(service_type_elem.text) + + device_info = { + 'ip': ip, + 'location': location, + 'server': headers.get('SERVER', ''), + 'usn': usn, + 'st': st, + 'friendly_name': friendly_name_elem.text if friendly_name_elem is not None else '', + 'manufacturer': manufacturer_elem.text if manufacturer_elem is not None else '', + 'model_name': model_name_elem.text if model_name_elem is not None else '', + 'model_description': model_description_elem.text if model_description_elem is not None else '', + 'device_type': device_type_elem.text, + 'udn': udn_elem.text if udn_elem is not None else '', + 'services': services, + 'response_time': time.strftime('%H:%M:%S') + } + + self.logger.debug(f"成功获取设备详情: {device_info.get('friendly_name')} ({ip})") + return device_info + + except (urllib.error.URLError, socket.timeout, ConnectionError) as e: + self.logger.debug(f"无法访问设备描述文档 {location}: {e}") + return None + except ET.ParseError as e: + self.logger.debug(f"解析设备XML失败: {location}, 错误: {e}") + return None + except Exception as e: + self.logger.debug(f"获取设备详情时出错: {e}") + return None + + def _get_device_details_from_response(self, response, ip): + """ + 从SSDP响应中获取设备详情,只返回 MediaRenderer 设备 + + Args: + response: SSDP响应数据 + ip: 设备IP地址 + + Returns: + dict: 设备信息,如果不是MediaRenderer则返回None + """ + # 解析SSDP响应头 + lines = response.split('\r\n') + headers = {} + + for line in lines: + if ':' in line: + key, value = line.split(':', 1) + headers[key.strip().upper()] = value.strip() + + # 检查是否有LOCATION头 + location = headers.get('LOCATION') + if not location: + return None + + # 先检查USN是否包含MediaRenderer字样,快速过滤 + usn = headers.get('USN', '') + st = headers.get('ST', headers.get('NT', '')) + + try: + # 获取设备描述文档 + req = urllib.request.Request(location) + req.add_header('User-Agent', 'Python DLNA Client') + + with urllib.request.urlopen(req, timeout=4) as response: + xml_data = response.read().decode('utf-8', errors='ignore') + + # 解析 XML + root = ET.fromstring(xml_data) + ns = {'upnp': 'urn:schemas-upnp-org:device-1-0'} + + # 提取设备信息 + device = root.find('.//upnp:device', ns) + if device is None: + self.logger.debug(f"从 {location} 未找到设备信息") + return None + + # 检查设备类型 + device_type_elem = device.find('upnp:deviceType', ns) + if device_type_elem is None or device_type_elem.text != 'urn:schemas-upnp-org:device:MediaRenderer:1': + return None # 不是 MediaRenderer 设备,跳过 + + # 提取设备信息 + friendly_name_elem = device.find('upnp:friendlyName', ns) + manufacturer_elem = device.find('upnp:manufacturer', ns) + model_name_elem = device.find('upnp:modelName', ns) + model_description_elem = device.find('upnp:modelDescription', ns) + udn_elem = device.find('upnp:UDN', ns) + + # 提取服务列表 + services = [] + service_list = root.find('.//upnp:serviceList', ns) + if service_list is not None: + for service in service_list.findall('upnp:service', ns): + service_type_elem = service.find('upnp:serviceType', ns) + if service_type_elem is not None: + services.append(service_type_elem.text) + + device_info = { + 'ip': ip, + 'location': location, + 'server': headers.get('SERVER', ''), + 'usn': usn, + 'st': st, + 'friendly_name': friendly_name_elem.text if friendly_name_elem is not None else '', + 'manufacturer': manufacturer_elem.text if manufacturer_elem is not None else '', + 'model_name': model_name_elem.text if model_name_elem is not None else '', + 'model_description': model_description_elem.text if model_description_elem is not None else '', + 'device_type': device_type_elem.text, + 'udn': udn_elem.text if udn_elem is not None else '', + 'services': services, + 'response_time': time.strftime('%H:%M:%S') + } + + self.logger.debug(f"成功获取设备详情: {device_info.get('friendly_name')} ({ip})") + return device_info + + except urllib.error.URLError as e: + self.logger.debug(f"无法访问设备描述文档 {location}: {e}") + return None + except socket.timeout: + self.logger.debug(f"访问设备描述文档超时: {location}") + return None + except ET.ParseError as e: + self.logger.debug(f"解析设备XML失败: {location}, 错误: {e}") + return None + except Exception as e: + self.logger.debug(f"获取设备详情时出错: {e}") + return None + + def display_discovered_devices(self): + """ + 显示已发现的媒体渲染器列表 + """ + if not self.discovered_devices: + self.logger.info("未发现任何媒体渲染器(投屏设备)") + return + + self.logger.info(f"发现 {len(self.discovered_devices)} 个媒体渲染器(投屏设备):") + + for i, device in enumerate(self.discovered_devices, 1): + device_name = device.get('friendly_name', '未知') + device_ip = device.get('ip', '未知') + manufacturer = device.get('manufacturer', '未知') + model = device.get('model_name', '未知') + model_desc = device.get('model_description', '') + + self.logger.info(f"设备{i}:{device_name},IP地址:{device_ip},制造商:{manufacturer},型号:{model},描述:{model_desc}") + + def check_device_isline(self, devices, timeout=2, max_threads=5): + """ + 快速检测指定的设备列表是否在线 + 通过访问设备的 location URL 来检测 + + Args: + devices: 待检测的设备列表 + timeout: 每个设备的检测超时时间(秒) + max_threads: 最大并发线程数,默认5个 + + Returns: + dict: 包含在线设备列表和离线设备列表的字典 + """ + if not devices: + self.logger.info("没有需要检测的设备") + return [] + + from concurrent.futures import ThreadPoolExecutor, as_completed + + self.logger.info(f"开始检测 {len(devices)} 个设备是否在线,超时时间: {timeout}秒") + + online_devices = [] + + def check_single_device(device): + """检测单个设备是否在线""" + uuid = device.get('udn', '未知') + device_name = device.get('friendly_name', '未知') + location = device.get('location', '') + ip = device.get('ip', '未知') + + if not location: + self.logger.debug(f"设备 {device_name} 没有location信息") + return device, False + + try: + # 设置超时访问设备的location URL + req = urllib.request.Request(location) + req.add_header('User-Agent', 'Python DLNA Availability Checker') + req.timeout = timeout + + with urllib.request.urlopen(req, timeout=timeout) as response: + # 检查响应状态码 + if response.status == 200: + self.logger.debug(f"设备 {device_name} ({ip}) 在线") + return device, True + else: + self.logger.debug(f"设备 {device_name} ({ip}) 响应异常: {response.status}") + return device, False + + except urllib.error.URLError as e: + self.logger.debug(f"设备 {device_name} ({ip}) 无法访问: {e}") + return device, False + except socket.timeout: + self.logger.debug(f"设备 {device_name} ({ip}) 检测超时") + return device, False + except Exception as e: + self.logger.debug(f"设备 {device_name} ({ip}) 检测出错: {e}") + return device, False + + # 使用线程池并发检测设备 + with ThreadPoolExecutor(max_workers=max_threads) as executor: + # 提交所有检测任务 + future_to_device = { + executor.submit(check_single_device, device): device + for device in devices + } + + # 收集结果 + for future in as_completed(future_to_device): + try: + device, is_online = future.result(timeout=timeout + 1) + if is_online: + online_devices.append(device) + self.logger.info(f"设备:{device.get('friendly_name')}[{device.get('ip')}]在线") + except Exception as e: + device = future_to_device[future] + self.logger.error(f"检测设备 {device.get('friendly_name', '未知')} 时出错: {e}") + + return online_devices + + def test_raw_discovery(self, timeout=5): + """ + 测试原始的SSDP发现,查看所有响应 + """ + ssdp_msg = ( + "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "MX: 3\r\n" + "ST: ssdp:all\r\n" # 搜索所有设备 + "USER-AGENT: Python DLNA Discovery\r\n" + "\r\n" + ) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + sock.bind(('', 0)) + + # 发送到所有可能的端口 + sock.sendto(ssdp_msg.encode(), ('239.255.255.250', 1900)) + sock.settimeout(timeout) + + responses = [] + start_time = time.time() + + while time.time() - start_time < timeout: + try: + data, addr = sock.recvfrom(4096) + response = data.decode('utf-8', errors='ignore') + responses.append((addr, response)) + + # 打印原始响应 + print(f"\n收到来自 {addr} 的响应:") + print(response[:200] + "..." if len(response) > 200 else response) + + # 尝试解析USN + lines = response.split('\r\n') + for line in lines: + if 'USN:' in line or 'ST:' in line or 'LOCATION:' in line: + print(f" {line.strip()}") + + except socket.timeout: + break + except Exception as e: + print(f"接收错误: {e}") + + sock.close() + print(f"\n总共收到 {len(responses)} 个原始响应") + return responses + + +def main(): + discovery = DeviceDiscovery() + devices = discovery.discover_media_renderers() + discovery.display_discovered_devices() + + print(devices) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/core/dlna_controller.py b/core/dlna_controller.py new file mode 100644 index 0000000..900f40c --- /dev/null +++ b/core/dlna_controller.py @@ -0,0 +1,658 @@ +import urllib.parse +import xml.etree.ElementTree as ET +import requests +from urllib.parse import urlparse + +from core import AppLogger + + +class DLNAController: + """ + DLNA 媒体渲染器控制器 + 只负责控制已发现的设备,不包含发现功能 + """ + + def __init__(self, device_info): + """ + 初始化控制器 + + Args: + device_info: 设备信息字典,必须包含: + - 'location': 设备描述文档的URL + - 'friendly_name': 设备友好名称 (可选) + - 'ip': 设备IP地址 (可选) + """ + if 'location' not in device_info: + raise ValueError("device_info 必须包含 'location' 字段") + + self.device_info = device_info + self.location = device_info['location'] + self.base_url = None + self.av_transport_url = None + self.rendering_control_url = None + self.connection_manager_url = None + + # 解析设备信息 + self._parse_device_info() + + self.logger = AppLogger('DLNAController') + + def _parse_device_info(self): + """ + 解析设备描述文档,提取控制URL + """ + try: + # 获取基础URL + parsed = urlparse(self.location) + self.base_url = f"{parsed.scheme}://{parsed.netloc}" + + # 获取设备描述文档 + response = requests.get(self.location, timeout=5) + response.raise_for_status() + + # 解析XML + root = ET.fromstring(response.content) + ns = {'upnp': 'urn:schemas-upnp-org:device-1-0'} + + # 查找服务列表 + service_list = root.find('.//upnp:device/upnp:serviceList', ns) + if service_list is None: + raise ValueError("设备描述中未找到服务列表") + + # 遍历服务,提取控制URL + for service in service_list.findall('upnp:service', ns): + service_type_elem = service.find('upnp:serviceType', ns) + control_url_elem = service.find('upnp:controlURL', ns) + + if service_type_elem is None or control_url_elem is None: + continue + + service_type = service_type_elem.text + control_url = control_url_elem.text + + # 确保URL是完整的 + if control_url.startswith('/'): + control_url = f"{self.base_url}{control_url}" + + if 'AVTransport' in service_type: + self.av_transport_url = control_url + elif 'RenderingControl' in service_type: + self.rendering_control_url = control_url + elif 'ConnectionManager' in service_type: + self.connection_manager_url = control_url + + # 验证必要的服务是否存在 + if not self.av_transport_url: + raise ValueError("设备不支持 AVTransport 服务") + + except requests.exceptions.RequestException as e: + raise ConnectionError(f"无法连接设备: {e}") + except ET.ParseError as e: + raise ValueError(f"设备描述文档格式错误: {e}") + except Exception as e: + raise RuntimeError(f"解析设备信息失败: {e}") + + def _send_soap_request(self, service_url, action, body): + """ + 发送SOAP请求到设备 + + Args: + service_url: 服务控制URL + action: SOAP Action + body: SOAP Body内容 + + Returns: + tuple: (success, response_xml) - 成功标志和响应XML + """ + soap_envelope = f""" + + + {body} + +""" + + headers = { + 'Content-Type': 'text/xml; charset="utf-8"', + 'SOAPAction': f'"{action}"', + 'User-Agent': 'Python DLNA Controller' + } + + try: + response = requests.post(service_url, data=soap_envelope, + headers=headers, timeout=5) + response.raise_for_status() + return True, response.text + except requests.exceptions.Timeout: + return False, "请求超时" + except requests.exceptions.ConnectionError: + return False, "连接失败" + except Exception as e: + return False, str(e) + + # ================== AVTransport 服务方法 ================== + + def set_av_transport_uri(self, media_url, metadata=""): + """ + 设置播放URI + + Args: + media_url: 媒体文件的URL + metadata: 媒体元数据(可选) + + Returns: + bool: 是否成功 + """ + if not self.av_transport_url: + return False + + # 转义XML特殊字符 + media_url_escaped = media_url.replace('&', '&').replace('<', '<').replace('>', '>') + media_url_escaped = urllib.parse.quote(media_url_escaped,safe=':/') + metadata_escaped = metadata.replace('&', '&').replace('<', '<').replace('>', '>') + + soap_body = f""" + 0 + {media_url_escaped} + {metadata_escaped} +""" + + action = "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI" + success, response = self._send_soap_request(self.av_transport_url, action, soap_body) + self.logger.info(f"媒体播放地址:{media_url_escaped}") + self.logger.info(f"DLNA控制响应:{response}") + + return success + + def play(self, speed="1"): + """ + 开始播放 + + Args: + speed: 播放速度,默认为1 + + Returns: + bool: 是否成功 + """ + if not self.av_transport_url: + return False + + soap_body = f""" + 0 + {speed} +""" + + action = "urn:schemas-upnp-org:service:AVTransport:1#Play" + success, response = self._send_soap_request(self.av_transport_url, action, soap_body) + + return success + + def pause(self): + """ + 暂停播放 + + Returns: + bool: 是否成功 + """ + if not self.av_transport_url: + return False + + soap_body = """ + 0 +""" + + action = "urn:schemas-upnp-org:service:AVTransport:1#Pause" + success, response = self._send_soap_request(self.av_transport_url, action, soap_body) + + return success + + def stop(self): + """ + 停止播放 + + Returns: + bool: 是否成功 + """ + if not self.av_transport_url: + return False + + soap_body = """ + 0 +""" + + action = "urn:schemas-upnp-org:service:AVTransport:1#Stop" + success, response = self._send_soap_request(self.av_transport_url, action, soap_body) + + return success + + def seek(self, unit, target): + """ + 跳转到指定位置 + + Args: + unit: 时间单位,如 "REL_TIME"(相对时间)或 "TRACK_NR"(曲目号) + target: 目标位置,如 "00:05:30" 或 "2"(第2首) + + Returns: + bool: 是否成功 + """ + if not self.av_transport_url: + return False + + target_escaped = target.replace('&', '&').replace('<', '<').replace('>', '>') + + soap_body = f""" + 0 + {unit} + {target_escaped} +""" + + action = "urn:schemas-upnp-org:service:AVTransport:1#Seek" + success, response = self._send_soap_request(self.av_transport_url, action, soap_body) + + return success + + def get_transport_info(self): + """ + 获取传输状态信息 + + Returns: + dict: 传输状态信息,包含: + - state: 当前传输状态 + - status: 当前传输状态 + - speed: 当前播放速度 + None: 获取失败 + """ + if not self.av_transport_url: + return None + + soap_body = """ + 0 +""" + + action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" + success, response = self._send_soap_request(self.av_transport_url, action, soap_body) + + if not success: + return None + + try: + # 解析响应XML + root = ET.fromstring(response) + ns = {'s': 'http://schemas.xmlsoap.org/soap/envelope/', + 'u': 'urn:schemas-upnp-org:service:AVTransport:1'} + + # 查找Body中的响应 + body = root.find('.//s:Body', ns) + if body is None: + return None + + # 获取状态信息 + current_transport_state = body.find('.//CurrentTransportState') + current_transport_status = body.find('.//CurrentTransportStatus') + current_speed = body.find('.//CurrentSpeed') + + return { + 'state': current_transport_state.text if current_transport_state is not None else '未知', + 'status': current_transport_status.text if current_transport_status is not None else '未知', + 'speed': current_speed.text if current_speed is not None else '未知' + } + + except ET.ParseError: + return None + + def get_position_info(self): + """ + 获取当前位置信息 + + Returns: + dict: 位置信息,包含: + - track: 当前曲目 + - track_duration: 曲目时长 + - track_meta_data: 曲目元数据 + - track_uri: 曲目URI + - rel_time: 相对时间 + - abs_time: 绝对时间 + - rel_count: 相对计数 + - abs_count: 绝对计数 + None: 获取失败 + """ + if not self.av_transport_url: + return None + + soap_body = """ + 0 +""" + + action = "urn:schemas-upnp-org:service:AVTransport:1#GetPositionInfo" + success, response = self._send_soap_request(self.av_transport_url, action, soap_body) + + if not success: + return None + + try: + root = ET.fromstring(response) + ns = {'s': 'http://schemas.xmlsoap.org/soap/envelope/', + 'u': 'urn:schemas-upnp-org:service:AVTransport:1'} + + body = root.find('.//s:Body', ns) + if body is None: + return None + + # 提取位置信息 + info = {} + fields = [ + 'Track', 'TrackDuration', 'TrackMetaData', 'TrackURI', + 'RelTime', 'AbsTime', 'RelCount', 'AbsCount' + ] + + for field in fields: + elem = body.find(f'.//{field}') + info[field.lower()] = elem.text if elem is not None else '' + + return info + + except ET.ParseError: + return None + + # ================== RenderingControl 服务方法 ================== + + def set_volume(self, volume, channel='Master'): + """ + 设置音量 + + Args: + volume: 音量值 (0-100) + channel: 声道,默认为 'Master' + + Returns: + bool: 是否成功 + """ + if not self.rendering_control_url: + return False + + # 确保音量在0-100范围内 + volume = max(0, min(100, int(volume))) + + soap_body = f""" + 0 + {channel} + {volume} +""" + + action = "urn:schemas-upnp-org:service:RenderingControl:1#SetVolume" + success, response = self._send_soap_request(self.rendering_control_url, action, soap_body) + + return success + + def get_volume(self, channel='Master'): + """ + 获取当前音量 + + Args: + channel: 声道,默认为 'Master' + + Returns: + int: 音量值 (0-100),None表示获取失败 + """ + if not self.rendering_control_url: + return None + + soap_body = f""" + 0 + {channel} +""" + + action = "urn:schemas-upnp-org:service:RenderingControl:1#GetVolume" + success, response = self._send_soap_request(self.rendering_control_url, action, soap_body) + + if not success: + return None + + try: + root = ET.fromstring(response) + ns = {'s': 'http://schemas.xmlsoap.org/soap/envelope/', + 'u': 'urn:schemas-upnp-org:service:RenderingControl:1'} + + body = root.find('.//s:Body', ns) + if body is None: + return None + + current_volume = body.find('.//CurrentVolume') + if current_volume is not None and current_volume.text: + return int(current_volume.text) + + except (ET.ParseError, ValueError): + pass + + return None + + def set_mute(self, mute, channel='Master'): + """ + 设置静音 + + Args: + mute: 是否静音 (True/False) + channel: 声道,默认为 'Master' + + Returns: + bool: 是否成功 + """ + if not self.rendering_control_url: + return False + + desired_mute = "1" if mute else "0" + + soap_body = f""" + 0 + {channel} + {desired_mute} +""" + + action = "urn:schemas-upnp-org:service:RenderingControl:1#SetMute" + success, response = self._send_soap_request(self.rendering_control_url, action, soap_body) + + return success + + def get_mute(self, channel='Master'): + """ + 获取静音状态 + + Args: + channel: 声道,默认为 'Master' + + Returns: + bool: 是否静音,None表示获取失败 + """ + if not self.rendering_control_url: + return None + + soap_body = f""" + 0 + {channel} +""" + + action = "urn:schemas-upnp-org:service:RenderingControl:1#GetMute" + success, response = self._send_soap_request(self.rendering_control_url, action, soap_body) + + if not success: + return None + + try: + root = ET.fromstring(response) + ns = {'s': 'http://schemas.xmlsoap.org/soap/envelope/', + 'u': 'urn:schemas-upnp-org:service:RenderingControl:1'} + + body = root.find('.//s:Body', ns) + if body is None: + return None + + current_mute = body.find('.//CurrentMute') + if current_mute is not None and current_mute.text: + return current_mute.text == "1" + + except ET.ParseError: + pass + + return None + + # ================== ConnectionManager 服务方法 ================== + + def get_protocol_info(self): + """ + 获取设备支持的协议信息 + + Returns: + dict: 协议信息,包含: + - source: 源协议信息 + - sink: 接收协议信息 + None: 获取失败 + """ + if not self.connection_manager_url: + return None + + soap_body = """ +""" + + action = "urn:schemas-upnp-org:service:ConnectionManager:1#GetProtocolInfo" + success, response = self._send_soap_request(self.connection_manager_url, action, soap_body) + + if not success: + return None + + try: + root = ET.fromstring(response) + ns = {'s': 'http://schemas.xmlsoap.org/soap/envelope/', + 'u': 'urn:schemas-upnp-org:service:ConnectionManager:1'} + + body = root.find('.//s:Body', ns) + if body is None: + return None + + source = body.find('.//Source') + sink = body.find('.//Sink') + + return { + 'source': source.text if source is not None else '', + 'sink': sink.text if sink is not None else '' + } + + except ET.ParseError: + return None + + # ================== 高级方法 ================== + + def play_media(self, media_url, metadata="", wait=1): + """ + 播放媒体文件(组合操作) + + Args: + media_url: 媒体文件URL + metadata: 媒体元数据 + wait: 设置URI后的等待时间(秒) + + Returns: + bool: 是否成功 + """ + import time + + if self.set_av_transport_uri(media_url, metadata): + time.sleep(wait) + return self.play() + return False + + def get_device_status(self): + """ + 获取设备完整状态 + + Returns: + dict: 设备状态信息 + """ + status = { + 'device_info': { + 'friendly_name': self.device_info.get('friendly_name', '未知'), + 'ip': self.device_info.get('ip', '未知'), + 'location': self.device_info.get('location', '未知') + }, + 'transport_info': self.get_transport_info(), + 'position_info': self.get_position_info(), + 'volume': self.get_volume(), + 'mute': self.get_mute(), + 'protocol_info': self.get_protocol_info(), + 'services': { + 'av_transport': self.av_transport_url is not None, + 'rendering_control': self.rendering_control_url is not None, + 'connection_manager': self.connection_manager_url is not None + } + } + + return status + + def get_supported_formats(self): + """ + 获取设备支持的媒体格式 + + Returns: + list: 支持的格式列表 + """ + protocol_info = self.get_protocol_info() + if not protocol_info or 'sink' not in protocol_info: + return [] + + # 解析sink字段,提取支持的格式 + sink_info = protocol_info['sink'] + formats = [] + + # DLNA格式通常是逗号分隔的 + for fmt in sink_info.split(','): + fmt = fmt.strip() + if fmt: + formats.append(fmt) + + return formats + + def can_play_format(self, media_url): + """ + 检查设备是否可能支持指定格式 + + Args: + media_url: 媒体URL + + Returns: + bool: 是否可能支持 + """ + import mimetypes + + # 获取文件扩展名 + if '.' in media_url: + ext = media_url.split('.')[-1].lower() + + # 常见扩展名到MIME类型的映射 + ext_to_mime = { + 'mp4': 'video/mp4', + 'mp3': 'audio/mp3', + 'm4a': 'audio/mp4', + 'wav': 'audio/wav', + 'flac': 'audio/flac', + 'mkv': 'video/x-matroska', + 'avi': 'video/x-msvideo', + 'mov': 'video/quicktime', + 'wmv': 'video/x-ms-wmv', + 'flv': 'video/x-flv', + 'webm': 'video/webm', + 'ogg': 'audio/ogg', + 'oga': 'audio/ogg', + 'ogv': 'video/ogg', + 'm3u8': 'application/x-mpegURL', + 'mpd': 'application/dash+xml' + } + + mime_type = ext_to_mime.get(ext) + if mime_type: + # 检查设备是否支持该MIME类型 + supported_formats = self.get_supported_formats() + for fmt in supported_formats: + if mime_type in fmt: + return True + + return False \ No newline at end of file diff --git a/core/http_file_server.py b/core/http_file_server.py new file mode 100644 index 0000000..bf0cbc9 --- /dev/null +++ b/core/http_file_server.py @@ -0,0 +1,693 @@ +# core/http_file_server.py +""" +支持流式传输的HTTP文件服务器,适合大文件投屏 +支持动态修改根目录和流式传输 +""" +import http.server +import socketserver +import threading +import os +import urllib.parse +import mimetypes +import time +from typing import Optional, Tuple +from core.logger import AppLogger + +class StreamingHTTPRequestHandler(http.server.BaseHTTPRequestHandler): + """支持流式传输的HTTP请求处理器""" + + # 协议版本 + protocol_version = 'HTTP/1.1' + + # 支持的MIME类型映射 + VIDEO_MIME_TYPES = { + '.mp4': 'video/mp4', + '.mkv': 'video/x-matroska', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.wmv': 'video/x-ms-wmv', + '.flv': 'video/x-flv', + '.webm': 'video/webm', + '.m4v': 'video/x-m4v', + '.3gp': 'video/3gpp', + '.ts': 'video/mp2t', + '.m2ts': 'video/mp2t', + '.mts': 'video/mp2t', + } + + AUDIO_MIME_TYPES = { + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + '.flac': 'audio/flac', + '.aac': 'audio/aac', + '.ogg': 'audio/ogg', + '.m4a': 'audio/mp4', + '.wma': 'audio/x-ms-wma', + } + + IMAGE_MIME_TYPES = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.bmp': 'image/bmp', + '.webp': 'image/webp', + } + + def __init__(self, *args, **kwargs): + self.extensions_map = {} + self.extensions_map.update(self.VIDEO_MIME_TYPES) + self.extensions_map.update(self.AUDIO_MIME_TYPES) + self.extensions_map.update(self.IMAGE_MIME_TYPES) + + # 文本和其他类型 + self.extensions_map.update({ + '.html': 'text/html', + '.htm': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.json': 'application/json', + '.txt': 'text/plain', + '.xml': 'application/xml', + '.pdf': 'application/pdf', + '.ico': 'image/x-icon', + '.svg': 'image/svg+xml', + }) + + self.logger = AppLogger('StreamingHTTPServer') + super().__init__(*args, **kwargs) + + def get_server_root(self) -> str: + """获取服务器当前根目录""" + if hasattr(self.server, 'current_directory'): + return self.server.current_directory + return "." + + def translate_path(self, path: str) -> str: + """ + 转换URL路径为文件系统路径 + + Args: + path: URL路径 + + Returns: + 文件系统路径,如果无效则返回空字符串 + """ + try: + # 解析URL + parsed_path = urllib.parse.urlparse(path) + url_path = urllib.parse.unquote(parsed_path.path) + + # 获取根目录 + root_dir = self.get_server_root() + + # 去掉开头的斜杠 + if url_path.startswith('/'): + url_path = url_path[1:] + + # 安全检查:防止目录遍历攻击 + if '..' in url_path: + self.logger.warning(f"检测到目录遍历攻击: {path}") + return "" + + # 构建完整路径 + full_path = os.path.join(root_dir, url_path) + + # 标准化路径 + full_path = os.path.normpath(full_path) + + # 再次安全检查:确保路径在根目录下 + abs_root = os.path.abspath(root_dir) + abs_path = os.path.abspath(full_path) + + if not abs_path.startswith(abs_root): + self.logger.warning(f"路径超出根目录范围: {full_path}") + return "" + + return full_path + + except Exception as e: + self.logger.error(f"路径转换失败: {e}") + return "" + + def guess_mime_type(self, file_path: str) -> str: + """ + 根据文件扩展名猜测MIME类型 + + Args: + file_path: 文件路径 + + Returns: + MIME类型字符串 + """ + # 获取扩展名 + _, ext = os.path.splitext(file_path) + ext = ext.lower() + + # 使用自定义映射 + if ext in self.extensions_map: + return self.extensions_map[ext] + + # 使用系统猜测 + mime_type, _ = mimetypes.guess_type(file_path) + if mime_type: + return mime_type + + # 默认类型 + return 'application/octet-stream' + + def parse_range_header(self, range_header: str, file_size: int) -> Optional[Tuple[int, int]]: + """ + 解析Range请求头 + + Args: + range_header: Range头值 + file_size: 文件大小 + + Returns: + (start, end)元组,或None表示无效范围 + """ + if not range_header: + return None + + try: + # 格式: bytes=start-end + if range_header.startswith('bytes='): + range_str = range_header[6:] # 去掉'bytes=' + + if '-' in range_str: + start_str, end_str = range_str.split('-', 1) + + start = 0 + end = file_size - 1 + + if start_str: + start = int(start_str) + if end_str: + end = int(end_str) + else: + # 格式: bytes=start- + end = file_size - 1 + + # 验证范围 + if start < 0 or end >= file_size or start > end: + return None + + return (start, end) + + except Exception as e: + self.logger.error(f"解析Range头失败: {e}") + + return None + + def send_file_streaming(self, file_path: str, start_pos: int = 0, end_pos: Optional[int] = None): + """ + 流式发送文件内容 + + Args: + file_path: 文件路径 + start_pos: 开始位置 + end_pos: 结束位置,None表示到文件末尾 + """ + try: + file_size = os.path.getsize(file_path) + + # 计算实际发送范围 + if end_pos is None: + end_pos = file_size - 1 + + content_length = end_pos - start_pos + 1 + + # 打开文件 + with open(file_path, 'rb') as f: + # 定位到开始位置 + f.seek(start_pos) + + # 设置传输编码为分块传输(Chunked) + self.send_response(200 if start_pos == 0 and end_pos == file_size - 1 else 206) + + # 对于范围请求,发送Content-Range头 + if start_pos > 0 or end_pos < file_size - 1: + self.send_header('Content-Range', f'bytes {start_pos}-{end_pos}/{file_size}') + self.send_header('Accept-Ranges', 'bytes') + + # 设置Content-Length而不是使用Transfer-Encoding: chunked + # 这样可以支持视频播放器的进度控制和跳转 + self.send_header('Content-Length', str(content_length)) + self.send_header('Content-Type', self.guess_mime_type(file_path)) + self.send_header('Accept-Ranges', 'bytes') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Range') + self.send_header('Access-Control-Expose-Headers', 'Content-Length, Content-Range') + + # 缓存控制头 + self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') + self.send_header('Pragma', 'no-cache') + self.send_header('Expires', '0') + + self.end_headers() + + # 流式发送数据 + remaining = content_length + chunk_size = 1024 * 256 # 256KB chunk size for streaming + + while remaining > 0: + # 读取数据块 + read_size = min(chunk_size, remaining) + data = f.read(read_size) + + if not data: + break + + # 发送数据块 + try: + self.wfile.write(data) + self.wfile.flush() # 确保数据立即发送 + except (ConnectionError, BrokenPipeError): + self.logger.debug("客户端断开连接") + break + + remaining -= len(data) + + # 可选:小延迟以避免占用太多CPU + if read_size == chunk_size: + time.sleep(0.001) # 1ms延迟 + + self.logger.debug(f"文件传输完成: {file_path} ({content_length} bytes)") + + except Exception as e: + self.logger.error(f"流式发送文件失败: {e}") + raise + + def do_GET(self): + """处理GET请求""" + try: + client_ip = self.client_address[0] + self.logger.debug(f"GET请求: {self.path} from {client_ip}") + + # 转换路径 + file_path = self.translate_path(self.path) + if not file_path: + self.send_error(404, "文件未找到") + return + + # 检查文件是否存在 + if not os.path.exists(file_path): + self.send_error(404, f"文件不存在: {self.path}") + return + + if not os.path.isfile(file_path): + self.send_error(403, f"不是文件: {self.path}") + return + + # 获取文件信息 + file_size = os.path.getsize(file_path) + mime_type = self.guess_mime_type(file_path) + + # 检查Range请求 + range_header = self.headers.get('Range') + range_info = self.parse_range_header(range_header, file_size) + + # 如果是视频或音频文件,支持范围请求 + is_media_file = any(ext in file_path.lower() for ext in + ['.mp4', '.mkv', '.avi', '.mov', '.mp3', '.wav', '.flac']) + + if is_media_file and range_info: + # 范围请求 + start_pos, end_pos = range_info + self.send_file_streaming(file_path, start_pos, end_pos) + else: + # 完整文件请求 + # 对于大文件,也使用流式传输 + self.send_file_streaming(file_path, 0, file_size - 1) + + except ConnectionError: + self.logger.debug("客户端断开连接") + except Exception as e: + self.logger.error(f"处理GET请求失败: {e}") + if not self.headers_sent: + self.send_error(500, "内部服务器错误") + + def do_HEAD(self): + """处理HEAD请求""" + try: + self.logger.debug(f"HEAD请求: {self.path}") + + # 转换路径 + file_path = self.translate_path(self.path) + if not file_path: + self.send_error(404, "文件未找到") + return + + # 检查文件是否存在 + if not os.path.exists(file_path): + self.send_error(404, f"文件不存在: {self.path}") + return + + if not os.path.isfile(file_path): + self.send_error(403, f"不是文件: {self.path}") + return + + # 获取文件信息 + file_size = os.path.getsize(file_path) + mime_type = self.guess_mime_type(file_path) + + # 设置响应头 + self.send_response(200) + self.send_header('Content-Type', mime_type) + self.send_header('Content-Length', str(file_size)) + self.send_header('Accept-Ranges', 'bytes') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS') + self.end_headers() + + except Exception as e: + self.logger.error(f"处理HEAD请求失败: {e}") + self.send_error(500, "内部服务器错误") + + def do_OPTIONS(self): + """处理OPTIONS请求(CORS预检)""" + self.send_response(200) + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Range') + self.send_header('Access-Control-Max-Age', '86400') + self.end_headers() + + def log_message(self, format, *args): + """重写日志消息""" + # 减少日志输出,避免性能影响 + message = format % args + if 'code' in message and 'message' in message: + # 只记录错误响应 + if '200' not in message: + self.logger.info(f"HTTP响应: {message}") + +class StreamingHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + """支持流式传输和多线程的HTTP服务器""" + + allow_reuse_address = True + daemon_threads = True + + def __init__(self, server_address, request_handler_class, initial_directory="."): + """ + 初始化流式HTTP服务器 + + Args: + server_address: 服务器地址和端口 + request_handler_class: 请求处理器类 + initial_directory: 初始根目录 + """ + self.current_directory = os.path.abspath(initial_directory) + self.request_queue_size = 50 # 增加请求队列大小 + super().__init__(server_address, request_handler_class) + + def set_directory(self, directory: str) -> bool: + """ + 设置新的根目录 + + Args: + directory: 新的根目录 + + Returns: + 是否设置成功 + """ + new_dir = os.path.dirname(directory) + if os.path.exists(directory) and os.path.isdir(new_dir): + old_dir = self.current_directory + self.current_directory = new_dir + return True + return False + # 设置新的根目录 + new_dir = os.path.abspath(directory) + old_dir = self.httpd.current_directory + self.httpd.current_directory = new_dir + self.logger.info(f"HTTP服务器根目录已更改: {old_dir} -> {new_dir}") + +class StreamingFileServer: + """流式文件服务器管理器""" + + def __init__(self, port: int = 8000, bind_address: str = "0.0.0.0"): + """ + 初始化流式文件服务器 + + Args: + port: 服务器端口 + bind_address: 绑定地址 + """ + self.port = port + self.bind_address = bind_address + self.httpd: Optional[StreamingHTTPServer] = None + self.server_thread: Optional[threading.Thread] = None + self.is_running = False + self.logger = AppLogger.create_default_logger('StreamingFileServer') + + # 初始化MIME类型 + mimetypes.init() + + def start(self, directory: str = ".") -> bool: + """ + 启动流式文件服务器 + + Args: + directory: 初始根目录 + + Returns: + 是否启动成功 + """ + try: + if self.is_running: + self.logger.warning("服务器已经在运行") + return False + + # 检查目录 + if not os.path.exists(directory): + self.logger.error(f"目录不存在: {directory}") + return False + + if not os.path.isdir(directory): + self.logger.error(f"路径不是目录: {directory}") + return False + + # 创建服务器 + self.httpd = StreamingHTTPServer( + (self.bind_address, self.port), + StreamingHTTPRequestHandler, + initial_directory=directory + ) + + # 设置超时(秒) + self.httpd.timeout = 30 + + # 启动服务器线程 + self.server_thread = threading.Thread( + target=self._run_server, + daemon=True, + name="StreamingHTTPServer" + ) + self.server_thread.start() + + self.is_running = True + self.logger.info(f"流式文件服务器已启动: http://{self.bind_address}:{self.port}") + self.logger.info(f"初始根目录: {directory}") + self.logger.info("服务器支持流式传输和断点续传") + + return True + + except Exception as e: + self.logger.error(f"启动服务器失败: {e}") + return False + + def _run_server(self): + """运行服务器线程""" + try: + self.logger.info(f"服务器监听在 {self.bind_address}:{self.port}") + self.httpd.serve_forever() + except Exception as e: + if self.is_running: + self.logger.error(f"服务器错误: {e}") + finally: + self.is_running = False + + def set_root_directory(self, directory: str) -> bool: + """ + 动态设置根目录 + + Args: + directory: 新的根目录 + + Returns: + 是否设置成功 + """ + if not self.is_running or not self.httpd: + self.logger.error("服务器未运行") + return False + + try: + if not os.path.exists(directory): + self.logger.error(f"目录不存在: {directory}") + return False + + if self.httpd.set_directory(directory): + self.logger.info(f"根目录已更改为: {directory}") + return True + else: + self.logger.error(f"设置根目录失败: {directory}") + return False + + except Exception as e: + self.logger.error(f"设置根目录时出错: {e}") + return False + + def get_root_directory(self) -> str: + """获取当前根目录""" + if self.httpd: + return self.httpd.current_directory + return "." + + def stop(self): + """停止服务器""" + try: + self.is_running = False + if self.httpd: + self.httpd.shutdown() + self.httpd.server_close() + self.logger.info("流式文件服务器已停止") + except Exception as e: + self.logger.error(f"停止服务器时出错: {e}") + + def get_file_url(self, file_path: str) -> Optional[str]: + """ + 获取文件的HTTP URL + + Args: + file_path: 文件路径(绝对或相对) + + Returns: + 文件的HTTP URL,如果无效则返回None + """ + try: + # 获取当前根目录 + root_dir = self.get_root_directory() + + # 如果是绝对路径 + if os.path.isabs(file_path): + abs_file_path = os.path.abspath(file_path) + abs_root_dir = os.path.abspath(root_dir) + + # 确保文件在根目录下 + if not abs_file_path.startswith(abs_root_dir): + self.logger.warning(f"文件不在当前根目录下: {file_path}") + return None + + # 获取相对路径 + relative_path = os.path.relpath(abs_file_path, abs_root_dir) + else: + # 相对路径 + relative_path = file_path + + # 检查文件是否存在 + full_path = os.path.join(root_dir, relative_path) + if not os.path.exists(full_path) or not os.path.isfile(full_path): + self.logger.warning(f"文件不存在: {full_path}") + return None + + # URL编码路径 + encoded_path = urllib.parse.quote(relative_path.replace('\\', '/')) + + # 构建URL + return f"http://{self.bind_address}:{self.port}/{encoded_path}" + + except Exception as e: + self.logger.error(f"生成文件URL失败: {e}") + return None + + def get_server_info(self) -> dict: + """获取服务器信息""" + return { + 'is_running': self.is_running, + 'bind_address': self.bind_address, + 'port': self.port, + 'root_directory': self.get_root_directory(), + 'url': f"http://{self.bind_address}:{self.port}" + } + +# 高级流式服务器类 +class HTTPFileServer(StreamingFileServer): + """高级流式服务器,支持更多功能""" + + def __init__(self, port: int = 8000, bind_address: str = "0.0.0.0", + max_chunk_size: int = 1024 * 256): # 256KB + """ + 初始化高级流式服务器 + + Args: + port: 服务器端口 + bind_address: 绑定地址 + max_chunk_size: 最大块大小(字节) + """ + super().__init__(port, bind_address) + self.max_chunk_size = max_chunk_size + self.active_connections = 0 + self.total_served_bytes = 0 + self.stats_lock = threading.Lock() + + def _run_server(self): + """运行服务器线程,包含统计信息""" + self.logger.info(f"高级流式服务器启动,块大小: {self.max_chunk_size}字节") + super()._run_server() + + def get_stats(self) -> dict: + """获取服务器统计信息""" + with self.stats_lock: + return { + 'active_connections': self.active_connections, + 'total_served_bytes': self.total_served_bytes, + 'is_running': self.is_running, + 'root_directory': self.get_root_directory() + } + + +# 使用示例 +def test_streaming_server(): + """测试流式服务器""" + import time + + # 创建服务器 + server = HTTPFileServer(port=8080) + + print("启动流式文件服务器...") + + # 启动服务器 + if server.start("."): + print(f"服务器已启动: http://localhost:8080") + print(f"当前目录: {server.get_root_directory()}") + + # 测试获取文件URL + test_file = "test.mp4" # 假设存在一个测试文件 + file_url = server.get_file_url(test_file) + if file_url: + print(f"文件URL: {file_url}") + print("你可以使用以下命令测试:") + print(f" curl -I {file_url} # 查看文件信息") + print(f" curl --range 0-999 {file_url} # 测试范围请求") + + # 运行一段时间 + try: + while True: + time.sleep(1) + stats = server.get_stats() + print(f"\r活跃连接: {stats['active_connections']}, " + f"已传输: {stats['total_served_bytes'] / 1024 / 1024:.2f} MB", end='') + except KeyboardInterrupt: + print("\n\n正在停止服务器...") + finally: + server.stop() + print("服务器已停止") + else: + print("启动服务器失败") + + +if __name__ == "__main__": + test_streaming_server() \ No newline at end of file diff --git a/core/logger.py b/core/logger.py new file mode 100644 index 0000000..d563867 --- /dev/null +++ b/core/logger.py @@ -0,0 +1,83 @@ +# logger.py +import logging +import sys +from datetime import datetime +import os + + +class AppLogger: + """ + 应用程序日志工具类 + 提供统一的日志记录功能 + """ + + def __init__(self, name='', log_level=logging.INFO, log_to_file=False, log_file_path=None): + """ + 初始化日志记录器 + + Args: + name: 日志记录器名称 + log_level: 日志级别 + log_to_file: 是否记录到文件 + log_file_path: 日志文件路径,如果为None则使用默认路径 + """ + self.logger = logging.getLogger(name) + + # 避免重复添加处理器 + if self.logger.handlers: + return + + self.logger.setLevel(log_level) + + # 创建格式化器 + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # 创建控制台处理器 + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(log_level) + console_handler.setFormatter(formatter) + self.logger.addHandler(console_handler) + + # 如果需要记录到文件 + if log_to_file: + if log_file_path is None: + # 创建logs目录 + if not os.path.exists('logs'): + os.makedirs('logs') + # 生成日志文件名 + timestamp = datetime.now().strftime('%Y%m%d') + log_file_path = f'logs/{timestamp}.log' + + file_handler = logging.FileHandler(log_file_path, encoding='utf-8') + file_handler.setLevel(log_level) + file_handler.setFormatter(formatter) + self.logger.addHandler(file_handler) + + def info(self, message): + """记录信息级别日志""" + self.logger.info(message) + + def debug(self, message): + """记录调试级别日志""" + self.logger.debug(message) + + def warning(self, message): + """记录警告级别日志""" + self.logger.warning(message) + + def error(self, message): + """记录错误级别日志""" + self.logger.error(message) + + def critical(self, message): + """记录严重级别日志""" + self.logger.critical(message) + + def set_level(self, level): + """设置日志级别""" + self.logger.setLevel(level) + for handler in self.logger.handlers: + handler.setLevel(level) \ No newline at end of file diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fbbf82c81ff30bc00599c04b17deae1c23d9e983 GIT binary patch literal 106528 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

^y-EWB0w=NH(q@OjqqV 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 UMu=Q0`v3p{07*qoM6N<$f 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 + elif current_selected_device and device.get('ip') == current_selected_device.get('ip'): + 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 + elif current_selected_device and device.get('ip') == current_selected_device.get('ip'): + 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: + self.enable_control_buttons(True) + device_name = self.selected_device.get('friendly_name', '未知设备') + self.log_message("设备选择", f"当前选中: {device_name}") + else: + self.log_message("设备发现", "未发现任何投屏设备", "warning") + self.selected_device = None + self.enable_control_buttons(False) + + def select_device_from_list(self, item): + """从列表中选择设备""" + device = item.data(Qt.UserRole) + if device: + self.selected_device = device + + self.log_message("设备选择", f"已选择设备: {device.get('friendly_name', '未知')}") + + # 启用控制按钮 + 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.start_btn.setEnabled(True) + self.log_message("文件选择", f"已选择文件: {file_name}") + + def start_casting(self): + """开始投屏""" + if not self.selected_device : + self.log_message("投屏", "请先选择设备", "error") + return + if not self.selected_file: + self.log_message("投屏", "请先选择文件", "error") + return + try: + # 如果是第一次播放,需要创建DLNA控制器并设置URI + if not self.dlna_controller: + # 创建DLNA控制器 + self.dlna_controller = DLNAController(self.selected_device) + + # 启动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): + self.dlna_controller.play() + self.start_btn.setEnabled(False) + self.pause_btn.setEnabled(True) + self.stop_btn.setEnabled(True) + + self.log_message("投屏", f"开始投屏: {file_name}") + self.statusBar().showMessage(f"正在投屏: {file_name}") + self.is_paused = False # 确保不是暂停状态 + else: + QMessageBox.critical(self, "错误", "投屏失败,请检查设备连接") + return + else: + # 如果已经创建了DLNA控制器,可能是从暂停状态恢复 + if self.is_paused: + # 从暂停状态恢复播放 + if self.dlna_controller.play(): + self.is_paused = False + self.start_btn.setEnabled(False) + self.pause_btn.setEnabled(True) + self.stop_btn.setEnabled(True) + self.log_message("投屏", "继续播放") + else: + QMessageBox.critical(self, "错误", "继续播放失败") + else: + # 如果不是暂停状态,说明是停止后的重新播放 + # 需要重新设置URI + file_name = os.path.basename(self.selected_file) + ip_text = self.ip_label.text() + file_url = f"http://{ip_text}/{file_name}" + + if self.dlna_controller.set_av_transport_uri(file_url): + self.dlna_controller.play() + self.start_btn.setEnabled(False) + self.pause_btn.setEnabled(True) + self.stop_btn.setEnabled(True) + + 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 self.dlna_controller: + if self.dlna_controller.pause(): + self.is_paused = True # 标记为暂停状态 + + self.start_btn.setText("▶ 继续播放") + self.start_btn.setEnabled(True) + self.pause_btn.setEnabled(False) + + self.log_message("投屏", "已暂停播放") + + def stop_casting(self): + """停止投屏""" + if self.dlna_controller: + if self.dlna_controller.stop(): + self.is_paused = False # 重置暂停状态 + + self.start_btn.setText("▶ 开始投屏") + self.start_btn.setEnabled(True) + self.pause_btn.setEnabled(False) + self.stop_btn.setEnabled(False) + + self.log_message("投屏", "已停止播放") + + def volume_changed(self, value): + """音量改变""" + if self.dlna_controller: + self.dlna_controller.set_volume(value) + + def toggle_mute(self, checked): + """切换静音""" + 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("控制", f"{status}") + + def log_message(self, category, message, level="info"): + """记录日志消息""" + # timestamp = datetime.now().strftime("%H:%M:%S") + # + # if level == "error": + # color = "#dc3545" + # prefix = "❌" + # elif level == "warning": + # color = "#ffc107" + # prefix = "⚠️" + # else: + # color = "#0d6efd" + # prefix = "ℹ️" + # + # log_entry = f'[{timestamp}]{category}: {prefix} {message}
' + # + # self.log_text.insertHtml(log_entry) + # + # # 自动滚动到底部 + # scrollbar = self.log_text.verticalScrollBar() + # scrollbar.setValue(scrollbar.maximum()) + + # 同时输出到状态栏 + 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): + """启用/禁用控制按钮""" + self.pause_btn.setEnabled(False) + self.stop_btn.setEnabled(False) + self.volume_slider.setEnabled(enabled) + self.mute_btn.setEnabled(enabled) + + 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() \ No newline at end of file