# 会议投屏系统

## 项目简介

面向32台终端的内网低延迟屏幕广播系统，支持网络投屏、硬件接口互通、一键自动化运维。

本项目提供 **Python (PySide6)** 和 **Rust (egui)** 两个版本的投屏控制客户端。

## 需求说明

当前会议投屏需求涉及通过视频矩阵系统向32台终端设备进行同步投屏。具体模式可分为以下两类：

1. **模式1（主播投屏）**：单台终端设备作为信号源，向MediaMTX服务器投送本地画面（流的形式），其余31台终端设备读取MediaMTX服务器的流。
   - **特殊情况**：单独架设1台机器，用浏览器读取流，然后通过HDMI线将显示画面输出到矩阵系统，矩阵再发送给第三方视频会议系统。这台机器可利用墙上的插座。
2. **模式2（视频会议）**：在MediaMTX服务器上用采集卡和FFmpeg读取设备内容，发布流，全部32台终端设备用浏览器读取MediaMTX服务器的流。

技术说明：上述32台终端设备均通过有线网络实现互联互通。

## 解决方案

参考文档：<https://docs.qq.com/doc/DQXFpdHN5Rk9yRGpC>

## 客户端版本对比

| 特性 | Python (PySide6) | Rust (egui) |
|------|------------------|-------------|
| **GUI 框架** | PySide6 (Qt) | egui (即时模式) |
| **编译方式** | PyInstaller | Cargo + GNU 工具链 |
| **文件大小** | ~50MB | ~4MB |
| **启动速度** | 较慢 | 快 |
| **依赖** | 需 Python 运行时 | 独立可执行文件 |
| **界面风格** | 原生 Windows 风格 | 自定义风格 |

## 整体部署步骤（细化版）

### 步骤1：MediaMTX服务器端

1. **下载并解压MediaMTX**
   - 下载地址：`https://github.com/bluenviron/mediamtx/releases`（windows-amd64版）
   - 解压到 `D:\ScreenCast\mediamtx` 目录
2. **配置MediaMTX**
   - 编辑 `D:\ScreenCast\mediamtx\mediamtx.yml` 文件：
     - 设置 `webRTCAddress` 和 `rtmpAddress`
     - 添加 `screen` 路径配置，暂时不设鉴权
3. **启动MediaMTX**
   - 运行 `mediamtx.exe`
   - 观察控制台是否监听对应端口
4. **安装依赖**
   - 安装Python：`https://www.python.org/downloads/windows/`（勾选「Add Python to PATH」）
   - 安装FFmpeg：`https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z`（解压到 `D:\ScreenCast\ffmpeg`）
   - 安装Python依赖（管理员cmd执行）：
     ```
     pip install pyautogui requests subprocess32 psutil mss numpy
     ```
5. **开启权限**
   - 给服务器开启「屏幕录制权限」（设置→隐私和安全性→屏幕录制→允许 Python/CMD）

## 模式1：主播投屏（本机屏幕 → MediaMTX → 多终端）

单台终端作为信号源，将本机屏幕画面推送到MediaMTX服务器，其他终端通过浏览器观看。

### 主播端：推送本机屏幕

**方案A：使用 Rust 客户端（推荐）**

1. **下载预编译版本**
   - 从 Releases 下载 `push_screen.exe`
   - 或使用源码自行编译（见下方编译说明）

2. **运行程序**
   - 双击运行 `push_screen.exe`
   - 点击「设置」按钮配置服务器IP和FFmpeg路径
   - 点击「开始全屏投屏」按钮开始推流

**方案B：使用 Python 客户端**

- 双击运行 `push_screen.py`
- 点击「一键全屏投屏」按钮
- 脚本会自动启动MediaMTX并开始推流

**方案C：使用mss抓屏+FFmpeg推流**

```python
import subprocess
import time
import mss
import numpy as np

# ---------- 配置 ----------
RTMP_URL = "rtmp://服务器IP:1935/screen"   # MediaMTX 的推流地址
FPS = 24
WIDTH, HEIGHT = 1280, 720

# 初始化屏幕捕获
with mss.mss() as sct:
    monitor = sct.monitors[1]  # 主显示器

    # FFmpeg 命令，编码为 H.264 并推 RTMP
    command = [
        'ffmpeg',
        '-y',
        '-f', 'rawvideo',
        '-vcodec', 'rawvideo',
        '-pix_fmt', 'bgr24',
        '-s', f'{WIDTH}x{HEIGHT}',
        '-r', str(FPS),
        '-i', '-',
        '-c:v', 'libx264',
        '-pix_fmt', 'yuv420p',
        '-preset', 'ultrafast',
        '-f', 'flv',
        RTMP_URL
    ]

    process = subprocess.Popen(command, stdin=subprocess.PIPE)
    last_time = time.time()

    try:
        while True:
            img = np.array(sct.grab(monitor))
            frame = img[..., :3]
            process.stdin.write(frame.tobytes())

            elapsed = time.time() - last_time
            sleep_time = (1.0 / FPS) - elapsed
            if sleep_time > 0:
                time.sleep(sleep_time)
            last_time = time.time()

    except KeyboardInterrupt:
        print("用户停止推流")
    finally:
        process.stdin.close()
        process.wait()
```

### 观众端：通过浏览器观看

1. **打开浏览器**
   - 在任意观众机上打开 Chrome 浏览器
2. **访问流地址**
   - 输入地址：`http://服务器IP:8889/screen`
   - MediaMTX 会自动处理 WebRTC 协商，把 RTMP 流转成 WebRTC 给浏览器
   - 确认能看到主播机屏幕画面
3. **批量部署**
   - 在31台终端上运行 `auto_receive.py` 脚本
   - 脚本会自动打开Chrome并跳转到流地址

### 特殊情况：HDMI矩阵输出

单独架设1台机器，通过HDMI线将显示画面输出到视频矩阵系统。

1. **准备一台小机器**
   - 配置要求较低，能运行Chrome浏览器即可
2. **访问流地址**
   - 打开浏览器访问：`http://服务器IP:8889/screen`
3. **HDMI输出到矩阵**
   - 通过HDMI线将小机器连接到视频矩阵系统
   - 小机器播放的流画面将输出到矩阵系统

## 模式2：视频会议（采集卡 → MediaMTX → 多终端）

在MediaMTX服务器上用采集卡和FFmpeg读取外部视频信号，发布流，全部32台终端用浏览器观看。

### 步骤1：安装FFmpeg

- 下载FFmpeg：`https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z`
- 解压到 `D:\ScreenCast\ffmpeg` 目录
- 确保 `D:\ScreenCast\ffmpeg\bin\ffmpeg.exe` 可用

### 步骤2：列出DirectShow设备

在命令行执行以下命令，列出所有 DirectShow 设备：

```bat
ffmpeg -list_devices true -f dshow -i dummy
```

输出示例：

```
[dshow @ ...] DirectShow video devices
[dshow @ ...]  "USB Video Device"
[dshow @ ...]  "AVerMedia HD Capture"
```

记下要使用的设备名称。

### 步骤3：配置MediaMTX

编辑 `mediamtx.yml`，添加 `hdmi` 路径配置：

```yaml
paths:
  hdmi:
    # source 不写，或者写 publisher，表示"由客户端推流进来"
    # source: publisher

    # 局域网可以先不开认证
    # publishUser: ""
    # publishPass: ""

    # 有读者时才拉源（这里用不到，因为是 publisher 模式）
    sourceOnDemand: no
```

### 步骤4：启动MediaMTX

```bat
./mediamtx
```

看到日志里 RTMP/RTSP/WebRTC 端口监听就表示启动成功。

### 步骤5：用FFmpeg采集采集卡并推流

假设：

- MediaMTX 服务器 IP：192.168.1.100
- 推流路径名：hdmi
- DirectShow 设备名称："AVerMedia HD Capture"

```bat
ffmpeg -f dshow -i video="AVerMedia HD Capture" ^
  -c:v libx264 -preset ultrafast -tune zerolatency ^
  -profile:v baseline -pix_fmt yuv420p ^
  -b:v 4000k -maxrate 4000k -bufsize 8000k ^
  -g 30 ^
  -f flv rtmp://192.168.1.100/hdmi
```

### 步骤6：客户端观看

在浏览器中直接访问：

```
http://192.168.1.100:8889/hdmi
```

MediaMTX 会返回内置的 WebRTC 播放页面，浏览器会自动播放HDMI信号。

所有32台终端均可通过浏览器访问上述地址观看视频会议内容。

## Rust 客户端编译指南

### 环境准备

1. **安装 Rust**
   - 下载地址：`https://rustup.rs/`
   - 安装时选择 `x86_64-pc-windows-gnu` 目标

2. **安装 MinGW-w64**（GNU 工具链）
   - 推荐通过 MSYS2 安装：`pacman -S mingw-w64-x86_64-toolchain`
   - 或下载 winlibs MinGW-w64

3. **添加 Rust 目标**
   ```bash
   rustup target add x86_64-pc-windows-gnu
   ```

### 项目结构

```
push_screen_rust/
├── Cargo.toml          # 项目配置
├── build.rs            # 构建脚本（用于嵌入图标）
├── app.rc              # Windows 资源文件
├── vi.ico              # 应用程序图标
└── src/
    └── main.rs         # 主程序
```

### 依赖库对照（PySide6 vs Rust）

| Python/PySide6 | Rust 等效库 | 用途 |
|----------------|-------------|------|
| PySide6 (Qt) | `eframe` + `egui` | GUI 框架 |
| `QThread` | `std::thread` + `mpsc` | 多线程 |
| `QTimer` | `std::time::Duration` + 线程休眠 | 定时器 |
| `socket` | `std::net::TcpStream` | 网络检测 |
| `subprocess` | `std::process::Command` | 进程管理 |
| `json` | `serde` + `serde_json` | JSON 序列化 |
| `logging` | `tracing` + `tracing-subscriber` | 日志记录 |
| `requests` | `reqwest` | HTTP 客户端 |

### 编译参数对照（PyInstaller vs Cargo）

| PyInstaller 参数 | Cargo/Rust 等效配置 | 说明 |
|------------------|---------------------|------|
| `--onefile` | `lto = true` + `strip = true` | 优化体积 |
| `--windowed` | `#![windows_subsystem = "windows"]` | 隐藏控制台 |
| `--icon=vi.ico` | `windres app.rc -O coff -o app.res` + 链接 | 嵌入图标 |
| `--noconsole` | `windows_subsystem` 属性 | 无控制台窗口 |

### 编译步骤

1. **进入项目目录**
   ```bash
   cd push_screen_rust
   ```

2. **编译资源文件（图标）**
   ```bash
   windres app.rc -O coff -o app.res
   ```

3. **配置构建脚本**（`build.rs`）
   ```rust
   use std::env;

   fn main() {
       let target = env::var("TARGET").unwrap();
       if target.contains("windows") {
           println!("cargo:rustc-link-arg=app.res");
       }
       println!("cargo:rerun-if-changed=app.rc");
       println!("cargo:rerun-if-changed=vi.ico");
   }
   ```

4. **配置 Cargo.toml**
   ```toml
   [package]
   name = "push_screen_rust"
   version = "0.1.0"
   edition = "2021"
   build = "build.rs"

   [dependencies]
   eframe = { version = "0.24", features = ["default"] }
   egui = "0.24"
   tokio = { version = "1", features = ["full"] }
   serde = { version = "1.0", features = ["derive"] }
   serde_json = "1.0"
   reqwest = { version = "0.11", features = ["json"] }
   tracing = "0.1"
   tracing-subscriber = "0.3"
   winapi = { version = "0.3", features = ["winuser", "windef", "wingdi", ...] }

   [profile.release]
   opt-level = 3
   lto = true
   # strip = true  # 取消注释以进一步减小体积

   [[bin]]
   name = "push_screen"
   path = "src/main.rs"
   ```

5. **编译发布版本**
   ```bash
   cargo build --release --target x86_64-pc-windows-gnu
   ```

6. **输出文件**
   - 位置：`target/x86_64-pc-windows-gnu/release/push_screen.exe`
   - 大小：约 4-5 MB

### 关键代码说明

**隐藏控制台窗口**
```rust
#![windows_subsystem = "windows"]
```

**设置 UTF-8 编码（解决中文乱码）**
```rust
#[cfg(windows)]
extern "system" {
    fn SetConsoleOutputCP(wCodePageID: u32) -> i32;
    fn SetConsoleCP(wCodePageID: u32) -> i32;
}

fn set_utf8_encoding() {
    #[cfg(windows)]
    unsafe {
        SetConsoleOutputCP(65001); // UTF-8
        SetConsoleCP(65001);
    }
}
```

**加载中文字体**
```rust
fn configure_fonts(ctx: &egui::Context) {
    let mut fonts = egui::FontDefinitions::default();
    
    // 从系统加载中文字体
    let font_paths = [
        r"C:\Windows\Fonts\msyh.ttc",      // 微软雅黑
        r"C:\Windows\Fonts\simsun.ttc",    // 宋体
    ];
    
    for font_path in &font_paths {
        if let Ok(font_data) = std::fs::read(font_path) {
            // 加载字体...
        }
    }
    
    ctx.set_fonts(fonts);
}
```

## 测试方案

使用1台台式机模拟MediaMTX服务器，2台笔记本分别模拟信号源和观众，测试两种场景。

### 设备准备

| 设备 | 角色 | IP地址示例 |
|------|------|-----------|
| 台式机 | MediaMTX服务器 | 192.168.1.100 |
| 笔记本A | 信号源（主播） | 192.168.1.101 |
| 笔记本B | 观众端 | 192.168.1.102 |

### 前置条件

1. 三台设备处于同一局域网
2. 台式机已安装MediaMTX和FFmpeg
3. 笔记本A已安装Python和FFmpeg
4. 所有设备关闭防火墙或开放对应端口

### 场景1测试：主播投屏

**拓扑**：笔记本A（信号源）→ 台式机（MediaMTX）→ 笔记本B（观众）

**步骤**：

1. **台式机启动MediaMTX**
   ```
   D:\ScreenCast\mediamtx\mediamtx.exe
   ```
   确认控制台显示端口监听正常

2. **笔记本A启动推流**
   - 运行 `push_screen.exe` (Rust) 或 `push_screen.py` (Python)
   - 或使用命令行推流：
   ```bat
   ffmpeg -f gdigrab -framerate 30 -i desktop -c:v libx264 -preset ultrafast -tune zerolatency -f rtsp rtsp://192.168.1.100:8554/screen
   ```

3. **笔记本B观看**
   - 打开Chrome浏览器
   - 访问：`http://192.168.1.100:8889/screen`
   - 确认能看到笔记本A的屏幕画面

**验证点**：
- [ ] 推流端无错误
- [ ] MediaMTX日志显示有客户端连接
- [ ] 观众端能流畅播放画面
- [ ] 延迟在可接受范围内（<1秒）

### 场景2测试：视频会议（采集卡模式）

**拓扑**：外部HDMI信号 → 台式机（采集卡+FFmpeg）→ 台式机（MediaMTX）→ 笔记本A/B（观众）

**前置条件**：台式机已连接视频采集卡

**步骤**：

1. **列出采集卡设备**
   ```bat
   ffmpeg -list_devices true -f dshow -i dummy
   ```
   记录设备名称

2. **台式机启动MediaMTX**
   ```
   D:\ScreenCast\mediamtx\mediamtx.exe
   ```

3. **台式机启动FFmpeg推流**
   ```bat
   ffmpeg -f dshow -i video="采集卡设备名称" -c:v libx264 -preset ultrafast -tune zerolatency -f flv rtmp://192.168.1.100/hdmi
   ```

4. **笔记本A和B观看**
   - 打开Chrome浏览器
   - 访问：`http://192.168.1.100:8889/hdmi`
   - 确认能看到采集卡的HDMI输入画面

**验证点**：
- [ ] 采集卡设备被正确识别
- [ ] FFmpeg推流无错误
- [ ] MediaMTX正确转发流
- [ ] 多客户端能同时观看

### 测试记录表

| 测试项 | 预期结果 | 实际结果 | 是否通过 |
|--------|----------|----------|----------|
| MediaMTX启动 | 端口监听正常 | | |
| 场景1推流 | 无错误 | | |
| 场景1观看 | 画面流畅 | | |
| 场景2推流 | 无错误 | | |
| 场景2多客户端 | 至少2台同时观看 | | |

## 核心文件结构（统一放在服务器D:\ScreenCast）

```
D:\ScreenCast
├── mediamtx/                # MediaMTX解压目录
│   └── mediamtx.exe
├── ffmpeg/                  # FFmpeg解压目录
│   └── bin/ffmpeg.exe
├── push_screen_rust/        # Rust 客户端源码
│   ├── Cargo.toml
│   ├── build.rs
│   ├── app.rc
│   ├── vi.ico
│   └── src/main.rs
├── push_screen.exe          # Rust 编译后的可执行文件（推荐）
├── push_screen.py           # Python 投屏源推流脚本
├── auto_receive.py          # 终端自动打开浏览器脚本
└── manage_server.py         # 运维总控脚本
```

## 核心脚本说明

### Rust 客户端（push_screen_rust）

**功能**：轻量级投屏控制客户端，单文件可执行，无需运行时依赖。

**特性**：
- 实时状态检测（FFmpeg、服务器、端口）
- 配置持久化（config.json）
- 一键开始/停止推流
- 中文字体支持
- 自定义应用程序图标

**编译命令**：
```bash
cd push_screen_rust
windres app.rc -O coff -o app.res
cargo build --release --target x86_64-pc-windows-gnu
```

### Python 客户端（push_screen.py）

**功能**：基于 PySide6 的投屏控制界面，功能完善。

**特性**：
- 可视化配置界面
- 实时状态指示器
- 日志显示
- 设置对话框

**运行命令**：
```bash
python push_screen.py
```

### 终端自动打开浏览器脚本（auto_receive.py）

**功能**：双击脚本自动打开Chrome浏览器（全屏），直接跳转到投屏/会议流页面；支持批量部署到32台终端，无需手动输入地址。

### 运维总控脚本（manage_server.py）

**功能**：一站式运维（启动MediaMTX、切换两种模式、批量控制32台终端、停止所有服务），适合管理员操作。
