本文档为 AI 编程助手提供 ScreenPresenter 项目的开发规范和上下文指南。
ScreenPresenter 是一款 macOS 原生设备投屏工具,支持同时展示 iOS 和 Android 设备屏幕,具备仿真设备边框渲染效果。
- 📱 iOS 投屏: QuickTime 同款路径 (CoreMediaIO + AVFoundation)
- 🤖 Android 投屏: scrcpy 码流 + VideoToolbox 硬件解码
- 🖥️ Metal 渲染: CVDisplayLink 驱动的 60fps 高性能渲染
- 🔄 双设备展示: 支持同时展示两台设备
- 📐 仿真边框: 根据真实设备型号绘制设备外观
- 🎛️ 纯 AppKit: 零 SwiftUI 依赖
- 🌐 多语言: 中英文双语支持
- macOS 14.0+
- Xcode 15+
- Swift 5.9+
| 层级 | 技术 |
|---|---|
| UI 框架 | AppKit (NSWindow / NSView) |
| 渲染引擎 | Metal (CAMetalLayer + CVMetalTextureCache) |
| 帧同步 | CVDisplayLink |
| iOS 捕获 | CoreMediaIO + AVFoundation |
| Android 捕获 | scrcpy-server + Socket + VideoToolbox |
| 音频播放 | AVAudioEngine |
| 状态管理 | Combine |
| 并发模型 | Swift Concurrency (async/await) |
- 协议驱动设计:
DeviceSource协议统一 iOS/Android 设备接口 - 单例模式:
AppState.shared,AppLogger.shared - 观察者模式: Combine 的 Publisher/Subscriber
- 策略模式: 不同编解码器的处理(AAC/OPUS/RAW)
ScreenPresenter/
├── AppDelegate.swift # 应用入口,菜单和工具栏配置
├── main.swift # 程序入口点
├── Core/
│ ├── AppState.swift # 全局状态管理 (@MainActor 单例)
│ ├── Audio/
│ │ ├── AudioPlayer.swift # AVAudioEngine 播放器
│ │ ├── AudioRegulator.swift # 音频缓冲调节器 (参考 scrcpy)
│ │ └── RingBuffer.swift # 环形缓冲区
│ ├── DeviceDiscovery/
│ │ ├── IOSDevice.swift # iOS 设备模型
│ │ ├── IOSDeviceProvider.swift
│ │ ├── AndroidDevice.swift # Android 设备模型
│ │ └── AndroidDeviceProvider.swift
│ ├── DeviceSource/
│ │ ├── DeviceSource.swift # 设备源协议与基类
│ │ ├── IOSDeviceSource.swift # iOS 实现 (AVCaptureSession)
│ │ ├── ScrcpyDeviceSource.swift # Android 实现
│ │ └── Scrcpy/ # scrcpy 相关组件
│ │ ├── ScrcpyServerLauncher.swift
│ │ ├── ScrcpySocketAcceptor.swift
│ │ ├── ScrcpyVideoStreamParser.swift
│ │ ├── ScrcpyAudioStreamParser.swift
│ │ └── Scrcpy*Decoder.swift
│ ├── Preferences/
│ │ └── UserPreferences.swift # 用户偏好设置
│ ├── Process/
│ │ ├── ProcessRunner.swift # 进程管理
│ │ └── ToolchainManager.swift # 工具链管理 (adb, scrcpy)
│ ├── Rendering/
│ │ ├── MetalRenderer.swift # Metal 渲染器
│ │ ├── MetalRenderView.swift # 渲染视图
│ │ ├── VideoToolboxDecoder.swift # H.264/H.265 解码器
│ │ ├── FramePipeline.swift # 帧管道 + CVDisplayLink
│ │ └── ColorCompensation/ # 色彩补偿
│ └── Utilities/
│ ├── Logger.swift # 日志框架
│ ├── Localization.swift # 本地化
│ └── ...
├── Views/
│ ├── MainViewController.swift # 主视图控制器
│ ├── PreferencesWindowController.swift
│ └── Components/
│ ├── DevicePanelView.swift # 设备面板
│ ├── DeviceBezelView.swift # 设备边框绘制
│ ├── DeviceModel.swift # 50+ 设备型号定义
│ └── ...
└── Resources/
├── Tools/ # 捆绑的工具 (adb, scrcpy)
├── en.lproj/ # 英文本地化
└── zh-Hans.lproj/ # 中文本地化
每个 Swift 文件必须包含标准文件头:
//
// FileName.swift
// ScreenPresenter
//
// Created by Sun on YYYY/MM/DD.
//
// 功能简述
// 详细说明(可选)
//使用 // MARK: - 分隔代码区域:
// MARK: - 属性
private var someProperty: String
// MARK: - 初始化
init() { }
// MARK: - 公开方法
func publicMethod() { }
// MARK: - 私有方法
private func privateMethod() { }| 类型 | 规范 | 示例 |
|---|---|---|
| 类/结构体/枚举 | 大驼峰 | DeviceSource, CapturedFrame |
| 属性/变量/函数 | 小驼峰 | isPlaying, startCapture() |
| 常量 | 小驼峰或全大写 | maxBuffering, PACKET_HEADER_SIZE |
| 协议 | 大驼峰 + 动词/形容词 | Sendable, Identifiable |
- 默认
private,需要时才放开 - 用
private(set)暴露只读属性 - 避免使用
open
// ✅ 正确: 使用 @MainActor 标注主线程类
@MainActor
final class AppState {
// ...
}
// ✅ 正确: 异步函数使用 async/await
func connect() async throws {
// ...
}
// ❌ 避免: 在 async 上下文中使用 DispatchQueue.main.async// 定义专用错误类型
enum DeviceSourceError: LocalizedError, Equatable {
case connectionFailed(String)
case permissionDenied
// ...
var errorDescription: String? {
switch self {
case .connectionFailed(let msg): return L10n.error.connectionFailed(msg)
// ...
}
}
}使用 AppLogger 的分类日志,基于 os.log:
// 可用的日志分类
AppLogger.app.info("应用启动")
AppLogger.device.info("发现设备: \(deviceName)")
AppLogger.capture.error("捕获失败: \(error)")
AppLogger.rendering.debug("渲染帧: \(frameCount)")
AppLogger.connection.warning("连接不稳定")
AppLogger.process.info("进程已启动: \(pid)")
// 日志级别: debug < info < warning < error- info: 正常业务流程的关键节点
- debug: 详细调试信息(生产环境可关闭)
- warning: 可恢复的异常情况
- error: 需要关注的错误
使用 L10n 结构体获取本地化字符串:
// ✅ 正确
label.stringValue = L10n.device.connecting
throw DeviceSourceError.connectionFailed(L10n.error.noDevice(L10n.platform.ios))
// ❌ 错误: 硬编码字符串
label.stringValue = "连接中..."本地化文件位于:
Resources/en.lproj/Localizable.stringsResources/zh-Hans.lproj/Localizable.strings
使用 XCTest 框架:
import XCTest
@testable import ScreenPresenter
final class SomeFeatureTests: XCTestCase {
func testSomeBehavior() {
// Given
let parser = ScrcpyVideoStreamParser(codecType: kCMVideoCodecType_H264)
// When
let result = parser.append(testData)
// Then
XCTAssertEqual(result.count, 1)
}
}将 AVCaptureAudioDataOutput 添加到会话会激活音频路径,即使不处理数据也可能产生噪声。
// ✅ 正确: 检查音频是否启用
private func setupAudioCapture(for session: AVCaptureSession, videoDevice: AVCaptureDevice) {
guard isAudioEnabled else {
return // 不添加音频输出
}
// ...
}音频/视频处理涉及多线程,使用锁或 Actor 保护共享状态:
// 使用 OSAllocatedUnfairLock
private let capturingLock = OSAllocatedUnfairLock(initialState: false)
func isCapturing() -> Bool {
capturingLock.withLock { $0 }
}使用 autoreleasepool 处理高频帧数据:
autoreleasepool {
guard let pcmBuffer = createPCMBuffer(from: data) else { return }
playerNode.scheduleBuffer(pcmBuffer)
}AVAudioEngine 需要 non-interleaved Float32 格式:
// interleaved [L0 R0 L1 R1 ...] → non-interleaved [L0 L1 ...] [R0 R1 ...]
for channel in 0..<channelCount {
for frame in 0..<frameCount {
channelData[channel][frame] = srcPtr[frame * channelCount + channel]
}
}cd /path/to/ScreenPresenter
xcodebuild -project ScreenPresenter.xcodeproj -scheme ScreenPresenter -configuration Debug buildxcodebuild test -project ScreenPresenter.xcodeproj -scheme ScreenPresenter./build_dmg.sh- docs/AUDIT_REPORT.md - 代码审计报告
- docs/ANDROID_AUDIT_REPORT.md - Android 支持审计
- docs/AUTO_UPDATE_SETUP.md - 自动更新配置
- README.md - 项目介绍
- 语言: 默认使用简体中文回复
- 代码风格: 遵循上述规范,保持与现有代码一致
- 测试: 修改核心逻辑时考虑编写/更新测试
- 本地化: 新增用户可见字符串时使用
L10n - 日志: 在关键节点添加适当级别的日志
- Git: 不执行
git push,git commit需用户确认
# 编译检查
xcodebuild -project ScreenPresenter.xcodeproj -scheme ScreenPresenter build
# 查找文件
find . -name "*.swift" -type f
# 搜索代码
grep -r "关键词" --include="*.swift"