跳到主要內容
Flow

Flow 架構

範圍:macOS 定位:專為 Apple Silicon 最佳化的原生影片播放器,結合「播放引擎 + 影像處理框架 + 可替換模組」。


1. 設計目標與原則

  • 目標:見 REQUIREMENTS.md。
  • 原則:見 AGENTS.md。
  • 核心/模組邊界:核心決定幀「怎麼流」(執行緒、佇列、時鐘、generation、何時處理與丟棄);模組決定幀「長什麼樣」(解析、解碼、像素變換、GPU 編碼、輸出)。核心只把幀當不透明的 IOSurface-backed CVPixelBuffer 搬運,不讀寫內容。

2. 技術選型

層級選擇模組理由
語言Swift、Metal Shading Language、少量 C(vendored 橋接)—C 只用於 vendored 函式庫邊界
最低系統macOS 26(以 Xcode 27 建置)—可直接使用 Metal 4、新版 SwiftUI;免去大量 #available 分支
容器解析(Demux)FFmpeg libavformat(LGPL,動態連結)FlowFFmpeg支援 MKV/MP4/TS/WebM/AVI 等幾乎所有容器與串流協定
視訊解碼VideoToolbox 硬解為主;dav1d(AV1)、libavcodec 軟解為備援FlowVideoToolbox、FlowDav1d、FlowFFmpeg硬解省電;不支援的編碼由軟解補足
音訊解碼libavcodec + AudioToolboxFlowFFmpeg涵蓋 DTS/TrueHD/FLAC/Opus 等
音訊輸出AVAudioEngine(AVAudioSourceNode)、空間音訊/多聲道FlowAVAudio系統整合(AirPods 空間音訊、裝置切換)
GPUMetal 4(MTL4CommandQueue、Argument Table)FlowMetal4低開銷提交
字幕解碼:libavcodec(文字字幕轉 ASS 事件、點陣字幕轉 RGBA);渲染:libass(ASS)FlowFFmpeg、FlowLibasslibavcodec 涵蓋多數字幕格式並統一為 ASS;libass 是 ASS 渲染事實標準
Dolby Visionlibdovi(RPU 解析)+ 自製 Metal reshaping/tone mappingFlowDolbyVision硬解只給 base layer,需要自行套用 RPU
神經網路Core ML(ANE 優先,GPU 備援),輸入輸出為 IOSurface-backed CVPixelBufferFlowCoreMLANE 省電且不佔 GPU;zero-copy
UISwiftUI;影片顯示層以 NSViewRepresentable 包 CAMetalLayerFlowUI、FlowMetal4顯示層需要精準控制
Shader內建:MSL,建置期編譯為 metallib;使用者:mpv 相容 GLSL(glslang → SPIR-V → SPIRV-Cross 轉 MSL)或 MSL,執行期編譯FlowMetal4、FlowGLSL、FlowUserShaders內建零編譯成本;GLSL 沿用 mpv 生態,MSL 可用 Metal 專屬功能
模組僅第一方,靜態連結、in-process;不支援外部載入Modules/*完整 SwiftUI、熱路徑零開銷;免外部程式碼的簽章與信任問題
持久化SwiftDataFlowLibrary媒體庫、播放紀錄、續播點
建置Xcode 專案 + 本地 Swift Packages;vendored 函式庫以 XCFramework 形式提供—模組邊界清楚、可單獨測試
發行GitHub Release + Homebrew Cask,見 .agents/skills/release/—不受沙盒與審查限制

授權:本體為 Apache-2.0。LGPL 元件(FFmpeg、FriBidi、libsmb2)以動態 framework 隨附,其餘靜態連結;清單與履行方式見 THIRD_PARTY_NOTICES.md。開源授權不含編解碼器專利,專利敏感格式優先交由 VideoToolbox/AudioToolbox。內建 shader 與移植程式碼限寬鬆授權;libplacebo、mpv 只作行為參考,不移植程式碼。


3. 系統總覽

3.1 結構

flowchart TB
    subgraph App["App 層"]
        UI[FlowUI<br/>播放控制/媒體庫/設定]
        Platform[平台整合<br/>Now Playing/PiP/App Intents/MCP/快捷鍵]
        Root[composition root<br/>選用模組]
    end

    subgraph Core["核心:契約 + 控制平面"]
        Cmd[FlowController<br/>PlayerAPI 實作/Profiles]
        Engine[FlowEngine<br/>狀態機/執行緒/時鐘/元件選擇]
        Graph[FlowGraph<br/>Frame/Display Graph 拓撲]
        Host[FlowModuleHost<br/>啟用/停用]
        Reg[FlowRegistry<br/>已註冊實作與撤銷]
        API[[FlowInterfaces<br/>擴充點協定]]
        PAPI[[FlowPlayerAPI<br/>PlayerAPI/Command/Property/Event]]
    end

    subgraph Mods["模組:實作"]
        Media[MediaSource/Demuxer<br/>FFmpeg、Network]
        Dec[Decoder<br/>VideoToolbox、Dav1d、FFmpeg]
        RT[NodeRuntime<br/>Metal4、VapourSynth、CoreML]
        Nodes[Node/前端<br/>Effects、GLSL、UserShaders、DolbyVision]
        Out[Presenter/AudioOutput<br/>Metal4、AVAudio]
        Sub[Subtitle<br/>FFmpeg、Libass]
    end

    UI --> PAPI
    Platform --> PAPI
    Cmd -.->|實作| PAPI
    API --> PAPI
    Root --> Host
    Cmd --> Engine
    Engine --> Graph
    Engine -->|選擇| Reg
    Engine -->|呼叫| API
    Graph -->|呼叫| API
    Host -->|註冊| Reg
    Reg --> API
    Mods -.->|實作| API

3.2 資料流

flowchart LR
    Src[MediaSource] --> Dmx[Demuxer] --> PR((packet ring))
    PR --> VD[VideoDecoder] --> FS((FrameSink)) --> Seg[Frame Graph 段<br/>FrameRuntime] --> FQ((FrameQueue)) --> DP[DisplayPipeline] --> Pr[Presenter]
    PR --> AD[AudioDecoder] --> AR((PCM ring)) --> AO[AudioOutput] <-->|時間戳/速率| Clk((Clock))
    PR --> SDec[SubtitleDecoder] --> SD[SubtitleRenderer] -->|overlay| DP
    Clk <-->|vsync/顯示時間| Pr

圓形為核心擁有的佇列與時鐘;方框為模組實作的介面。


4. 模組劃分(Swift Packages)

Flow/
├── Apps/
│   └── FlowMac/                 # App target;只經 FlowComposition 的 FlowPlayer 與 PlayerAPI 操作播放器
├── Benchmarks/                  # package-benchmark:micro、播放情境、size、baseline(docs/BENCHMARK.md)
├── Packages/
│   ├── Core/                    # 不含任何實作的播放器核心;各 target 位於 Sources/<target>/
│   │   │                        # 契約:模組、UI 與自動化編譯時依賴的型別與協定
│   │   ├── FlowBase/            # MediaTime、ColorInfo、FrameGeometry、TrackInfo、SPSC ring、Logger、Signpost、計數器
│   │   ├── FlowPlayerAPI/       # PlayerAPI、Command/Property/Event 與內建名稱;UI 與自動化唯一能碰的介面
│   │   ├── FlowInterfaces/      # 擴充點協定、FlowModule、資料平面型別(Packet、VideoFrame、PCMFormat)
│   │   │                        # 控制平面:只有 Composition、IntegrationTests、Benchmarks 連結
│   │   ├── FlowRegistry/        # ComponentRegistry:依擁有者記錄與撤銷註冊
│   │   ├── FlowGraph/           # Frame/Display Graph 拓撲、分段、規格推導、預讀、bypass
│   │   ├── FlowController/      # PlayerController(PlayerAPI 實作)、按鍵綁定、Profiles
│   │   ├── FlowEngine/          # PlayerSession 狀態機、執行緒、FrameQueue/PCM ring、時鐘與 A/V 同步、Seek、元件選擇與 fallback、Governor
│   │   ├── FlowModuleHost/      # 模組啟用/停用
│   │   │                        # 開發用:App 不連結;Core 自身的測試也用,故留在 Core
│   │   ├── FlowSampling/        # 行程/執行緒/記憶體取樣器;測試與 benchmark 用
│   │   └── FlowTestSupport/     # mock 模組、TestModuleContext、fixture 定位、MemorySource;Core、模組與整合測試共用
│   ├── Composition/             # FlowComposition:composition root,唯一組裝 Modules/* 之處;App 與 Benchmarks 共用
│   ├── IntegrationTests/        # 真實模組 + mock 輸出的端到端播放測試(§11.2)
│   ├── Library/                 # FlowLibrary,App 層:媒體庫、播放紀錄、續播(SwiftData)
│   ├── MCP/                     # FlowMCP,App 層:MCP server(Streamable HTTP,僅 loopback)
│   └── UI/                      # FlowUI,App 層:SwiftUI 元件(控制列、OSD、設定頁)
├── Modules/                     # 實作;模組本身靜態連結(LGPL 函式庫除外,見 §2)
│   ├── File/                    # FileSource:本機檔案 MediaSource
│   ├── FFmpeg/                  # FFmpegDemuxer(含 HLS)、FFmpegVideoDecoder、FFmpegAudioDecoder、FFmpegSubtitleDecoder、點陣字幕渲染
│   ├── VideoToolbox/            # VTVideoDecoder
│   ├── Dav1d/                   # Dav1dVideoDecoder
│   ├── Network/                 # SMB(libsmb2)/WebDAV/HTTP MediaSource
│   ├── AVAudio/                 # AVAudioEngine AudioOutput
│   ├── Libass/                  # ASS 字幕渲染
│   ├── Metal4/                  # Metal4DisplayRuntime、MTL4ShaderNode、Presenter、內建步驟與 fallback 節點
│   │   ├── Plugins/             # MetalLibraryPlugin:建置期把 Shaders/*.metal 編譯為 FlowMetal4.metallib
│   │   └── Shaders/
│   │       ├── Include/Flow/    # Shader ABI(§7.4),使用者 .metal 亦依賴
│   │       └── *.metal          # 色彩轉換、bilinear、基本 tone mapping、dither
│   ├── DolbyVision/             # libdovi RPU 解析 + reshaping 節點(依賴 Metal4)
│   ├── GLSL/                    # GLSL → MSL 前端、GLSLShaderNode(依賴 Metal4)
│   ├── UserShaders/             # mpv `//!` 解析、匯入管理(依賴 Metal4、GLSL)
│   ├── Effects/                 # 高品質縮放、去色帶、銳化、去交錯、動態 tone mapping(依賴 Metal4)
│   ├── VapourSynth/             # VSRuntime、VapourSynthFilterNode
│   └── CoreML/                  # CoreMLRuntime、神經網路超解析/降噪節點
├── Vendor/                      # 由 scripts/ 產生,不入庫
│   ├── libav*.xcframework       # FFmpeg(libavformat、libavcodec、libavutil、libswresample);LGPL,動態;scripts/build-ffmpeg.sh
│   ├── Licenses/                # 各函式庫授權全文,隨 App 附於 Resources/Licenses/
│   ├── dav1d.xcframework
│   ├── libass.xcframework       # 含 freetype/harfbuzz
│   ├── fribidi.xcframework      # LGPL,動態
│   ├── libsmb2.xcframework      # LGPL,動態;僅 FlowNetwork 使用
│   ├── libdovi.xcframework
│   ├── glslang.xcframework      # 僅 FlowGLSL 使用(C API)
│   └── SPIRV-Cross.xcframework  # 同上
├── Tests/(各 package 內)     # 單元、Golden image、效能測試
└── docs/

命名:資料夾與 package 名稱為角色(Core、Composition、UI、FFmpeg),product 與 Swift module 加 Flow 前綴(FlowUI、FlowFFmpeg),以免與 Apple framework(AVAudio、VideoToolbox)或其他 module 撞名。Core 以外皆為單一 product Flow<資料夾>。

分層:

層位置可依賴
契約FlowBase、FlowPlayerAPI、FlowInterfaces契約
控制平面FlowRegistry、FlowGraph、FlowController、FlowEngine、FlowModuleHost契約、控制平面
模組Modules/*契約、runtime 模組
組裝Composition契約、控制平面、模組
前端UI、MCP、LibraryFlowBase、FlowPlayerAPI
AppApps/FlowMac前端、FlowComposition、FlowBase、FlowPlayerAPI
工具IntegrationTests、Benchmarks、FlowSampling、FlowTestSupport不限;App 不連結

依賴方向(只能向下):

Apps → {FlowUI, FlowMCP, FlowLibrary, FlowComposition}
FlowComposition → {FlowModuleHost, FlowEngine, Modules/*}
{FlowUI, FlowMCP} → FlowPlayerAPI
FlowModuleHost → FlowRegistry
FlowEngine → {FlowGraph, FlowRegistry, FlowController}
{FlowGraph, FlowRegistry} → FlowInterfaces → FlowPlayerAPI → FlowBase
FlowController → FlowPlayerAPI
Modules/* → [runtime 模組,如 Metal4] → FlowInterfaces

邊界規則:

  • Core/ 只定義契約並掌管邏輯、生命週期與控制平面;不得匯入 SwiftUI、AppKit、Network、Metal、VideoToolbox、AVFAudio 與任何 vendored C 函式庫(CoreVideo 僅作為 CVPixelBuffer/IOSurface 的不透明載體)。SwiftLint custom rule(core_banned_import、core_banned_link)檢查 Packages/Core 的 import 與連結。
  • Modules/ 只能依賴契約、自身的 vendored 函式庫,以及所需的 runtime 模組;不得依賴控制平面。模組測試另可用 FlowTestSupport(TestModuleContext 記錄註冊)。
  • App、FlowUI 與 FlowMCP 只依賴 FlowPlayerAPI(不依賴 FlowInterfaces、FlowEngine),經 PlayerAPI 操作播放器;FlowPlayer 只以 @_spi(Benchmark) 開放統計、display runtime、額外 presenter 與 PresentationSurface 給 Benchmarks,不暴露 registry 與 PlayerSession。
  • 依賴由 Package.swift 宣告強制,scripts/check-deps.sh(prek)依上表檢查各 manifest、project.yml 與 App 的 import,並檢查命名;package 的 import 由 explicit module build 限制在依賴閉包內;所有 package 與 App target 啟用 MemberImportVisibility(只能使用本檔 import 的模組成員)、ExistentialAny、InternalImportsByDefault(import 預設 internal,公開 API 用到的型別才 public import,vendored C 型別因此無法出現在公開 API),警告視為錯誤。
  • 模組只公開 FlowModule 型別;其餘實作為 internal,測試以 @testable import 存取。Core 與模組中只有 Benchmarks 需要的型別(如 FrameQueue、PlaybackClock)以 @_spi(Benchmark) 開放,不設為 public。
  • 是否「必要、不可停用」由 App 組裝時標記(§7.1),不由資料夾決定;核心不內建任何實作,沒有模組時無法播放。
  • 官方功能用 FlowInterfaces 寫不出來即視為介面缺陷。

5. 播放引擎

5.1 執行緒模型

執行緒由 FlowEngine 建立與擁有;模組方法只在下表指定的執行緒被呼叫。

執行緒QoS/優先權職責
DemuxuserInitiated呼叫 Demuxer.readPacket,寫入 packet ring
Video decodeuserInitiated呼叫 VideoDecoder.decode;VT 等非同步解碼器由其 callback 寫入 FrameSink
Audio decodeuserInteractive解碼並重取樣到輸出格式,寫入 PCM ring
Audio render即時(Core Audio IO 執行緒)AudioOutput 從 PCM ring 取樣本,禁止上鎖與配置記憶體
Frame GraphuserInitiated(Swift Concurrency)依 PTS 順序 pull 各 runtime 段,寫入 FrameQueue;非熱路徑
SubtitleuserInitiated呼叫 SubtitleDecoder.decode;依 FrameQueue 中即將顯示的 PTS 預先呼叫 SubtitleRenderer.render,寫入 overlay 佇列
RenderuserInteractive,CAMetalDisplayLink選幀與已備妥的 OverlayFrame、呼叫 DisplayPipeline.render

資料平面元件之間以核心提供的單一生產者/單一消費者 lock-free ring buffer 溝通;跨平面以 AsyncStream 回報事件(狀態變化、錯誤、統計)。

5.2 核心協定

定義於 FlowInterfaces;實作以 factory 註冊,由 FlowEngine 選擇與建立。

public protocol MediaSource: Sendable {
    var url: URL { get }
    var isSeekable: Bool { get }
    func read(into buffer: UnsafeMutableRawBufferPointer, at offset: Int64) throws -> Int
    var length: Int64? { get }
}

public protocol DemuxerFactory: Sendable {
    var id: String { get }
    func probe(_ source: any MediaSource) -> ProbeScore   // .unsupported 表示不支援
    /// `opener` 供需另開 URL 的 demuxer(HLS 的 playlist、segment)使用
    func make(_ source: any MediaSource, opener: any MediaOpener) throws -> any Demuxer
}

/// 核心實作;依 URL scheme 選用已註冊的 `MediaSourceFactory`
public protocol MediaOpener: Sendable {
    func open(_ url: URL) throws -> any MediaSource
}

public protocol Demuxer: AnyObject {
    var tracks: [TrackInfo] { get }
    /// 第一次 readPacket 前呼叫一次;其他軌的 packet 可不讀
    func setEnabledTracks(_ indices: Set<Int>)
    func readPacket() throws -> Packet?            // nil = EOF
    func seek(to time: MediaTime, mode: SeekMode) throws
    // 待實作:
    var attachments: [Attachment] { get }          // 內嵌字型等(MKV attachment)
}

public protocol VideoDecoderFactory: Sendable {
    var id: String { get }
    func support(for format: VideoFormat) -> DecoderSupport  // .hardware / .software / .none
    /// 解出的幀寫入 `sink`;佇列由核心擁有
    func make(_ format: VideoFormat, sink: FrameSink) throws -> any VideoDecoder
}

public protocol VideoDecoder: AnyObject {
    /// 每幀的像素格式;make 後固定,讓 Display Graph 在首幀前編譯
    var outputFormat: VideoPixelFormat { get }
    /// 非同步解碼器已收下、尚未產出的 packet 數;核心計入佇列高水位(同步解碼器為 0)
    var framesInFlight: Int { get }
    func decode(_ packet: Packet) throws
    /// EOF 時送出內部剩餘幀(如重排序中的 B 幀)
    func drain() throws
    func flush()
    // 待實作:播放落後時由 `FlowEngine` 在 Video decode 執行緒設定;不支援者可忽略
    var skip: DecodeSkip { get set }   // .none / .nonReference
}

/// 核心實作,不阻塞;背壓由 `FlowEngine` 暫停呼叫 `decode` 達成
public protocol FrameSink: AnyObject, Sendable {
    /// Video decode 執行緒或解碼器 callback 呼叫,一次一個;依 PTS 遞增寫入
    func push(_ frame: VideoFrame)
    /// 解碼器 callback 中 framesInFlight 減少但未 push(丟棄、失敗)時呼叫,讓核心恢復解碼
    func framesInFlightDidDecrease()
}

// CVPixelBuffer 非 Sendable;建立後唯讀故安全。CoreVideo 標註 Sendable 後移除 @unchecked
public struct VideoFrame: @unchecked Sendable {
    public let pixelBuffer: CVPixelBuffer   // 一律 IOSurface-backed
    public let pts: MediaTime
    public let duration: MediaTime
    public let color: ColorInfo             // primaries / transfer / matrix / range / chroma location
    public let geometry: FrameGeometry      // 旋轉(90° 倍數)、鏡像、像素長寬比
    public let generation: UInt32           // seek 世代,用來丟棄過期幀
    // 待實作(VID-02):
    public let hdr: HDRMetadata?            // mastering display、CLL、DV RPU、HDR10+
}

MediaSource、AudioDecoder、AudioOutput、SubtitleDecoder、SubtitleRenderer、Presenter 同樣採 factory + 實例協定(字幕見 §9)。

  • HLS:屬 demux 層,無法以單一 MediaSource 的位元組 offset 表達。playlist 以 MediaSource 開啟並經 probe 辨識,demuxer 再經 MediaOpener 開啟 variant playlist 與 segment;網路 I/O 仍由 FlowNetwork 的 HTTP MediaSource 負責。FlowFFmpeg 以 libavformat 的 HLS demuxer 實作,經 AVFormatContext.io_open 接上 MediaOpener。直播時總時長未知(§6.3)。
  • 顯示幾何:FrameGeometry 由 demuxer 自容器(MP4 display matrix、MKV 欄位)或碼流(SAR)取得,解碼器沿用 packet 上的值填入;像素保持儲存方向,由 Display Graph 的 scale 套用(§6.4)。

生命週期契約(所有元件適用):

階段方法執行緒規則
建立Factory.make、configure控制平面(可 async)可配置記憶體、可拋錯;失敗時 FlowEngine 換下一個實作
處理readPacket、decode、render 等資料平面(§5.1)不得 await;上鎖、配置記憶體、阻塞依執行緒分級(硬即時/軟即時/吞吐)限制,見 swift-realtime-media Skill
Flushflush、seek、reset該元件的資料平面執行緒只丟棄內部狀態,不釋放資源
重設configure控制平面,元件暫停時FlowEngine 保證不與處理並行
釋放deinit控制平面FlowEngine 保證在最後一次處理之後

輸出契約:所有解碼器輸出 IOSurface-backed CVPixelBuffer 並填妥 ColorInfo,不做色彩轉換;YUV→RGB 由 Display Graph 掌控以保證品質一致。輸出須依 PTS 遞增,重排序由解碼器模組負責(如 VT:HEVC 即使開啟 temporal processing 仍依解碼順序輸出,由 FrameReorder 依 PTS 重排)。

5.3 解碼策略

  1. FlowEngine 依 VideoFormat(codec、profile、bit depth、解析度)詢問所有已註冊 factory 的 support():.hardware 優先於 .software,同級依 App 組裝順序。
  2. 建立或解碼發生致命錯誤時,FlowEngine 改用下一個 factory,並從前一關鍵幀重新解碼。
  3. 實作細節屬各模組,例如 FlowVideoToolbox 依內容選輸出格式(8-bit → 420YpCbCr8BiPlanar;10-bit/HDR → 420YpCbCr10BiPlanar;VideoRange/FullRange 依來源 range;ProRes 4444 → 4444AYpCbCr16),軟解模組寫入 CVPixelBufferPool 的 IOSurface buffer。

5.4 時鐘與 A/V 同步

時鐘與同步策略屬 FlowEngine;AudioOutput 與 Presenter 只回報時間戳。

時鐘

  • PlaybackClock 為錨點映射 (hostTime, mediaTime, rate),以原子快照發布,其他執行緒無鎖讀取;display-resample 由 Render 執行緒依 vsync 推進,audio 由 Audio render 執行緒依音訊時間更新。
  • 音訊時間=AudioOutput 回報的 (sample time, host time) 扣除輸出延遲(裝置、串流、safety offset、緩衝);裝置變更時重新查詢延遲。無音軌時以 host time 推進。
  • 換裝置、音軌結束、underrun 時重新錨定,媒體時間保持單調、不跳躍。
  • AudioOutput 停止並重啟後,回報的 host time 須與已消耗的樣本一致;核心另保證「聽到的時間」不早於現在,防止輸出的 host time 停住時時鐘倒退。
  • 核心經 HostClock 協定取得時間,測試時注入虛擬時鐘。

同步模式(Property video.sync)

  • display-resample(預設):vsync 主導,視訊每幀固定 N 個 vsync、速度調整 ≤ ±1%,音訊同比例重取樣並修正漂移;條件為刷新率在 ±1% 內為輸出幀率的整數倍。
  • audio:音訊時鐘主導,視訊丟/重複幀,音訊不調整;用於其他情況。
  • 輸出幀率一律取 Frame Graph 輸出的 StreamSpec.fps,不用來源幀率。
  • 條件不符時自動降為 audio;條件恢復且穩定一段時間後才回到 display-resample(遲滯)。實際模式顯示於統計 OSD。
  • 刷新率:FlowEngine 依輸出幀率設定 preferredFrameRateRange(ProMotion/VRR,如 24p → 120Hz 的 5:5 pacing),全螢幕時可請求切換刷新率。刷新率只由輸出幀率決定;依刷新率決定輸出的節點只讀顯示器能力(§6.5),避免兩者互相追逐。

選幀(Render 執行緒,每 vsync)

  1. 以 display link 的 targetPresentationTimestamp 加上量測到的呈現延遲(實際上屏時間 − targetPresentationTimestamp),換算該次繪製實際顯示時的媒體時間。
  2. display-resample 依累計誤差分配每幀的 vsync 數(如 3:2 交替);audio 選 pts ≤ t 的最新幀,邊界加遲滯避免 cadence 抖動。
  3. 交由 DisplayPipeline 繪製;Presenter 回報實際顯示時間,用來校正呈現延遲並統計延遲幀與丟幀。
  4. vsync 停住(如視窗被遮住)時,Render 執行緒以備援 timer 依時鐘消耗到期的幀,避免解碼卡在高水位。

落後時降級(依序、含遲滯,每一步記錄於統計 OSD)

  • 無時間窗與 .retimed 節點:降低節點品質層級(Governor,§6.7)→ Frame Graph 前丟幀 → 解碼器略過非參考幀(VideoDecoder.skip)→ 顯示端丟幀。
  • 有時間窗或 .retimed 節點:降低節點品質層級(Governor,§6.7)→ 降低重定時候選(§6.5,如 120 → 60)→ bypass 該節點 → 顯示端丟幀。

丟輸入幀會讓時間窗缺幀,故有時間窗節點時不在 Frame Graph 前丟幀。

起播、Seek、暫停、倍速

  • 起播與 seek:新 generation 的首個音訊樣本與視訊幀都就緒才啟動時鐘;音訊裁掉目標時間前的樣本,視訊先顯示首幀。
  • 暫停時 rate = 0 並停止 display link,只在畫面需更新(seek、字幕/OSD、顯示目標或參數變更)時繪製單幀;Frame Graph 預讀補滿即停止。playback.rate 改變時重新錨定,音訊以保持音高的 time-stretch 播放。
  • EOF:音訊先結束改以 host time 推進;視訊結束時停在最後一幀。
  • audio.delay 只修改音訊時間映射;字幕依實際顯示幀的 pts 對齊,丟幀時不失步。

5.5 狀態機與 Seek

idle → loading → ready ⇄ playing ⇄ paused
                   ↓         ↓
                seeking ←────┘ → (ready | playing | paused)
任何狀態 → error/ended
  • Seek 時控制平面只遞增 generation;各 ring 由消費端丟棄舊世代的幀與樣本(控制平面不清空 SPSC ring,以維持單一生產者、單一消費者),各元件的 flush 在其資料平面執行緒呼叫,不需要同步等待。
  • Seek 完成條件:新世代的首幀已渲染,或首幀就緒後已等滿 displayWaitLimit(display link 被節流時);完成後回到 seek 前的狀態,暫停中 seek 仍為暫停。
  • SeekMode:.keyframe(拖動時即時預覽)與 .exact(放開後精準定位,從前一關鍵幀解碼至目標)。
  • Demux 快取:核心提供前向快取(依秒數 + 位元組上限)與後向快取,讓短距離倒退無需重新讀取網路。

5.6 Presenter

Presenter 協定定義於核心,回報顯示目標(尺寸、像素格式、刷新率與可用範圍;EDR headroom 待實作)與每幀實際顯示時間。顯示目標變化時通知 FlowEngine:尺寸、像素格式變化才重新編譯 Display Graph(DisplayPlan 相同即沿用);EDR headroom、刷新率等參數只更新 uniform 與 preferredFrameRateRange,並重新判斷同步模式(§5.4)。實作在 FlowMetal4:

  • MetalLayerPresenter:預設。CAMetalLayer,EDR 開啟時 wantsExtendedDynamicRangeContent = true、rgba16Float + extended linear Display P3。目前(VID-02 前)把 layer colorspace 標成來源色彩空間,drawable 依位元深度用 bgra8Unorm/bgr10a2Unorm,色域與轉移函數交給系統 compositor;HDR 是否改走 EDR 於 VID-02 決定。
  • SampleBufferPresenter:子母畫面(PiP)。將 Display Graph 輸出寫回 IOSurface CVPixelBuffer → CMSampleBuffer → AVSampleBufferDisplayLayer,搭配 AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer:),讓 PiP 也保有影像處理效果;controlTimebase 由 PlaybackClock 驅動。

6. 影像處理框架

6.1 雙圖模型

影像處理依時間域分成兩張圖:

flowchart LR
    Dec[Decoder] -->|FrameSink| FG[Frame Graph<br/>使用者排序/預先處理]
    FG --> Q[FrameQueue]
    Q --> DG[Display Graph<br/>每 vsync/固定 hook]
    DG --> P[Presenter]
Frame GraphDisplay Graph
驅動依 PTS 順序 pull,領先播放位置執行每 vsync 一次,不得阻塞
節點runtime 為 FrameRuntime 的節點,可混用多個 runtime僅 ShaderNode,且 runtime 為目前的 DisplayRuntime
順序使用者任意排序依 hook 固定
能力時間窗內前後幀、可改幀率/幀數、延遲不固定一進一出,可讀已顯示的歷史幀
限制不得依賴顯示參數(視窗尺寸、EDR headroom、OSD);需要時由節點訂閱唯讀 Property 自行重設不得依賴未來幀

依賴顯示參數的步驟(縮放到輸出尺寸、tone mapping、字幕/OSD、dither)固定在 Display Graph,其餘處理放 Frame Graph。

6.2 節點與 Runtime

核心只定義節點階層與 runtime 介面;節點(做什麼)、runtime(怎麼執行)、語言前端(怎麼寫) 三者分離:

// FlowInterfaces(不依賴任何 GPU API)
public protocol RenderNode: AnyObject, Sendable {
    var descriptor: NodeDescriptor { get }   // id、placement(.frame / .display(hook))、時間窗、需求色彩空間、品質層級數、是否逐像素
    var runtime: RuntimeID { get }           // 由哪個 runtime 執行
    /// 格式或參數改變時呼叫;回傳輸出規格,讓 FlowGraph 規劃資源與輸出時間
    func configure(input: NodeSpec, context: NodeContext) throws -> NodeSpec
    /// Governor 切換品質層級(0 為最高,§6.7);由 runtime 在兩次處理之間呼叫
    func setQuality(_ level: Int)
}

/// 非同步,可用 CPU/外部 runtime;只能放 Frame Graph
public protocol FilterNode: RenderNode {}

/// 同步編碼 GPU 指令、不得阻塞;可放的圖由 runtime 能力決定
public protocol ShaderNode: RenderNode {}

/// 只提供身分,供 FlowGraph 依節點的 `runtime` 分段與驗證
public protocol NodeRuntime: AnyObject, Sendable {
    var id: RuntimeID { get }
}

/// 能執行 Frame Graph 段
public protocol FrameRuntime: NodeRuntime {
    /// 把連續同 runtime 的節點編成一段,經 `input` 向上游要幀
    func makeSegment(_ nodes: [any RenderNode], input: any FrameSource) throws -> any FrameSource
}

/// 能執行 Display Graph;同時只有一個
public protocol DisplayRuntime: NodeRuntime {
    /// 依已排序的 hook 計畫建立 pipeline;內建步驟與 fallback 由此 runtime 提供
    func makePipeline(_ plan: DisplayPlan, presenter: any Presenter) throws -> any DisplayPipeline
}

public protocol DisplayPipeline: AnyObject {
    /// Render 執行緒,每 vsync 最多一次;不得阻塞。幀與 plan 都沒變時核心不呼叫,畫面維持上一個 drawable
    func render(_ frame: VideoFrame, at time: HostTime)
    // 待實作(SUB-02):overlay 由 runtime 擁有的佇列傳入,不在每個 vsync 配置陣列
}
  • 具體編碼介面屬 runtime 模組:如 FlowMetal4 定義 MTL4ShaderNode: ShaderNode(encode(_:input:output:),輸出為 MTLTexture);runtime 以 as? 取得自己的節點型別。
  • 逐像素節點:輸出像素只取決於輸入的同一像素、且不改尺寸的節點(YUV→RGB、線性化、tone mapping 等)於 descriptor 標示逐像素,可與相鄰步驟合併成同一個 pass(§6.6);需讀鄰近像素或改變尺寸者為取樣節點。
  • 能力即規則:runtime 依需要組合 FrameRuntime/DisplayRuntime(如 Metal 4 runtime 兩者皆實作、VSRuntime 只實作前者)。FlowGraph 編譯時驗證:Frame Graph 節點的 runtime 須為 FrameRuntime;Display Graph 只接受 runtime 為目前 DisplayRuntime 的 ShaderNode,違反者不得加入。
  • 擴充方式:
    • 節點:實作某 runtime 的節點協定(如 FlowEffects 的縮放器實作 MTL4ShaderNode)。
    • Shader 語言:前端模組,編譯為 runtime 可執行的形式,節點實作該 runtime 協定(如 FlowGLSL → MSL → GLSLShaderNode: MTL4ShaderNode)。
    • 執行環境:runtime 模組,實作 FrameRuntime/DisplayRuntime 與自己的節點協定(如 FlowVapourSynth、FlowCoreML)。

連續同 runtime 的節點不論來源語言都合併進同一段(同一個 command buffer、共用 heap aliasing 與 GPU 計時);跨 runtime 邊界才有轉換成本。

職責分工:

FlowGraph(核心)NodeRuntime(模組)
拓撲依使用者順序/hook 排序、依 runtime 分段、驗證 placement、切分 pass—
規格呼叫 configure 推導 NodeSpec、時間窗、輸出時間格—
時間驗證 PTS 單調遞增且不偏離輸入;違反時 bypass依時間窗與輸出時間驅動節點
資源預讀深度與記憶體上限紋理、heap aliasing、buffer pool
執行排程、generation、逾時 bypass合併 pass、編碼與提交、pipeline cache、GPU 計時
轉換決定哪裡需要色彩空間或格式轉換提供轉換實作

6.3 Frame Graph

一切皆 FrameSource:解碼器輸出是根,每個 runtime 段包住上游再成為新的 FrameSource。

public protocol FrameSource: AnyObject, Sendable {
    /// 尺寸、格式、色彩、幾何、名目 fps;總時長可為 nil(直播)
    var spec: StreamSpec { get }
    /// 依 PTS 遞增回傳下一幀;nil = 本 generation 結束
    func next() async throws -> VideoFrame?
    /// seek 或參數變更時由 FlowGraph 呼叫;丟棄內部狀態
    func reset()
}
  • 串流模型:以 PTS 而非幀編號定位,總幀數與時長可未知(VFR、直播)。每個 generation 是一段獨立串流,從 seek 落點開始(.keyframe 為關鍵幀、.exact 為目標幀),以 nil 結束。需要隨機存取的 runtime 在段內自行轉換(§7.6)。

  • 根:核心把 FrameSink 包成根 FrameSource,以有界佇列把 push 轉成 pull:下游 await 至新幀到達;佇列達高水位時 FlowEngine 暫停呼叫 decode。demux EOF 後呼叫 drain,排空即回傳 nil。

  • 編譯:依使用者順序,連續同 runtime 的節點合併成一段交給 FrameRuntime.makeSegment,各段首尾相接(如連續 MTL4ShaderNode 編成單一 command buffer、連續 VS 節點編成單一 VS 圖)。

  • 格式:段間一律傳 IOSurface-backed VideoFrame;預設維持原生 YUV(4K rgba16Float 一幀約 66 MB,不利預讀),節點可宣告需要線性 RGB,由下游 runtime 轉換。

  • 時間:輸出幀自帶 pts/duration,改變幀率或幀數(補幀、IVTC)的節點經 NodeSpec.timing 宣告(§6.5);FlowGraph 只驗證 PTS 單調遞增且輸出時間不偏離輸入(總時長不變),違反時 bypass 該段,以免音訊失步。StreamSpec.fps 為名目值,僅供預讀估算與顯示同步。

  • 顯示資訊:框架不因顯示參數變化重新編譯 Frame Graph;節點若要依顯示參數決定輸出,自行訂閱唯讀 Property 並更新設定,走一般重新編譯流程。補幀等重定時節點例外,輸出幀率見 §6.5。

  • 即時性:前以 FrameSink、後以 FrameQueue 隔離,以預讀深度(預設約 0.5 s,並受記憶體上限約束)吸收延遲抖動;高輸出幀率下記憶體上限使預讀低於約 0.2 s 時,改觸發 §5.4 降級而非繼續縮短。不屬熱路徑,可使用 Swift Concurrency。

  • Seek/參數變更:遞增 generation、取消進行中的 pull,由根往下呼叫各段 reset 後從新位置重新處理,避免已預讀的舊結果延遲生效。

  • 失敗:某段逾時或拋錯時 bypass 該段並通知使用者,播放不中斷。

  • 可量測:每段耗時、預讀水位、跨 runtime 邊界數顯示於統計 OSD。

6.4 Display Graph 與 Hook Points

內建步驟(chroma 升取樣、YUV→RGB、線性化)之間開放 Hook Point,順序對齊 mpv,以相容 mpv 使用者 shader(§7.5):

flowchart LR
    A[FrameQueue] --> P[plane] --> CU([chroma 升取樣]) --> PS[plane-scaled] --> MG([合併]) --> N[native]
    N --> RGB([YUV→RGB]) --> M[main] --> LIN([線性化*]) --> L[linear*] --> SIG([sigmoid 化*]) --> SG[sigmoid*]
    SG --> S[scale] --> SD[scaled] --> T[tonemap] --> O[output] --> OV[overlay] --> D[dither]

* 依 render.scale-light 啟用。

Hookmpv 對應色彩空間/格式典型節點
planeLUMA、CHROMA、ALPHA、RGB、XYZ解碼器原生單一平面亮度預放大(FSRCNNX、RAVU)、chroma 重建(KrigBilateral)
plane-scaledCHROMA_SCALED、ALPHA_SCALED單一平面,已縮放到 luma 尺寸
nativeNATIVE合併平面,4:4:4 原生 YUV去色帶
mainMAIN、MAINPRESUB非線性 RGB(來源轉移函數)Anime4K
linearLINEAR顯示線性 RGB,rgba16Float
sigmoidSIGMOIDsigmoid 化的顯示線性 RGB
scalePREKERNEL → 縮放器 → POSTKERNEL依 render.scale-light內建縮放器(EWA Lanczos、Bicubic、FSR)
scaledSCALED輸出解析度,sigmoid 已還原銳化、Film grain
tonemap—顯示線性,依 EDR headroomBT.2390、Hable、動態 tone mapping
outputOUTPUT顯示參照,目標轉移函數編碼
overlay—同 output字幕、OSD(OverlayFrame 由 runtime 合成)
dither—目標 drawable 格式Dither
  • 內建步驟:chroma 升取樣、YUV→RGB、線性化為必填插槽,預設節點由 DisplayRuntime 提供;模組可取代(如 FlowDolbyVision 於有 RPU 時取代 YUV→RGB 做 reshaping)。線性化在 render.scale-light 為 gamma 時延到 tonemap 前。
  • 幾何:scale 依 VideoFrame.geometry 與 Property video.rotate/video.aspect 套用旋轉、鏡像與像素長寬比,並等比縮放置中於輸出(letterbox);Frame Graph 與 scale 之前的 hook 皆處理未旋轉的儲存畫面(同 mpv)。
  • Fallback:單選 hook 的 fallback 亦由 DisplayRuntime 提供(FlowMetal4:scale 硬體 bilinear、tonemap 基本 BT.2390、dither ordered),只啟用必要模組時畫面仍正確。
  • Hook 語意(由 FlowGraph 處理):scale、tonemap、dither 為單選,模組節點取代 fallback;其餘 hook 可疊加,同一 hook 先執行模組節點、再依 render.user-shaders 順序執行使用者 shader;scale 的 PREKERNEL/POSTKERNEL 節點包在所選縮放器前後。
  • 平面:plane/plane-scaled 節點須指定平面,如 .display(.plane(.luma));來源為 RGB 時只有 RGB 觸發(同 mpv)。
  • 縮放光域:Property render.scale-light(auto/linear/sigmoid/gamma)決定 linear/sigmoid 是否觸發及 scale、scaled 的光域。auto 對齊 mpv 預設:升頻 sigmoid、降頻線性、HDR 降頻 gamma(避免極端亮度差放大 ringing)。
  • 尺寸:任一 hook 的節點都可改變尺寸;chroma 升取樣以 hook 後的 luma 尺寸為目標,已同尺寸則略過(如 KrigBilateral);scale 只處理剩餘倍率。
  • 實體化:hook 只是邏輯位置;只有掛上節點或結果被讀取時才寫出紋理,否則前後步驟合併在同一個 pass(同 mpv,§6.6)。
  • 紋理命名空間:每個 hook 的結果以 mpv 名稱(LUMA、NATIVE、MAIN、PREKERNEL…)登記,連同 //!SAVE 紋理供後續節點綁定;FlowGraph 依最後一個讀取者決定生命週期。
  • 座標:每個紋理帶相對來源的 offset/transform(對應 mpv NAME_off/NAME_rot),由 FlowGraph 沿 hook 傳遞,最後由縮放器補償,避免預放大造成的半像素偏移。
  • 線性光約定:顯示線性(套用來源 EOTF),1.0 = SDR 參考白(203 nits,BT.2408),與 EDR 一致;兩張圖與所有模組共用。
  • output 編碼:一律為目標轉移函數編碼;drawable 為 extended linear 時,由 runtime 在 dither 轉換。

節點可在 descriptor 宣告需要的色彩空間,FlowGraph 決定插入位置,轉換節點由 runtime 提供。

6.5 NodeSpec 與時間

public struct NodeSpec: Sendable {
    public var texture: TextureSpec
    /// 相對來源座標的 offset/transform;對應 mpv `NAME_off`/`NAME_rot`
    public var transform: TextureTransform
    /// `.passthrough` 或 `.retimed(fps:)`;Display Graph 只接受 `.passthrough`
    public var timing: FrameTiming
}
  • 時間窗:descriptor 宣告需要前 N/後 M 幀;Display Graph 的 M 必須為 0。generation 起點與 EOF 處不足時以邊界幀補齊並標記;窗內幀由 FlowGraph 以滑動視窗保留,計入預讀記憶體上限。
  • 重定時:.retimed 節點只宣告輸出幀率,FlowGraph 依此產生輸出時間格,runtime 對每個輸出時間驅動一次節點;節點仍是一次一張輸出,不管理 PTS。
  • 重定時候選:節點從 NodeContext.retimeRates 選擇輸出幀率(可依自身上限選較小者)。FlowEngine 依 display.max-refresh-rate、playback.rate 與目前降級層級提供 最高刷新率 ÷ (k × rate)(k 為正整數)的候選,確保 display-resample 成立,也避免與刷新率互相追逐(§5.4)。候選變化視同參數變更(§6.3)。
  • 節點輸入(型別由 runtime 定義,語意由核心規定):時間窗內的幀(依實際 PTS,VFR 亦正確)、輸出 PTS、輸出時間在前後輸入幀間的相位(0–1)、HDR metadata、使用者參數。

6.6 Graph 編譯與資源管理

FlowGraph(核心):

  1. 編譯:Frame Graph 依使用者順序分段;Display Graph 依 hook 排序、套用單選語意與 fallback → 建立紋理命名空間,略過綁定不到紋理的節點 → 標記需要的色彩轉換 → 呼叫 configure 推導每個節點的輸入/輸出規格 → 切分 pass → 產生 DisplayPlan。兩張圖都只在來源格式、輸出尺寸、節點組合變化時才重新編譯;Display Graph 另含 video.rotate 與 drawable 像素格式。DisplayPlan 與上次相同時不重建 pipeline。
  2. 切分 pass:中間紋理的讀寫是主要成本(4K rgba16Float 每多一個 pass 約多讀寫 133 MB),故只在取樣前切開:
    • 逐像素步驟接到目前 pass 的尾端,中間值留在暫存器。
    • 遇到取樣步驟時,結束目前 pass 並寫出紋理,取樣步驟開新 pass。
    • 硬體 bilinear 取樣(如 bilinear chroma 升取樣)視為逐像素,可放在 pass 開頭。
    • scale 為 1:1 或硬體 bilinear 時不切開,直接以輸出座標取樣來源平面(同 libplacebo);先內插再做非線性步驟,屬可接受的近似,只適用於 render.scale-light 為 gamma(或尚未支援光域的 fallback);linear/sigmoid 須在線性化後縮放,scale 另開 pass。若 chroma 也是 bilinear 且沒有取樣節點,整條管線只有一個 pass。
    • overlay、dither 接到最後一個 pass;是否另寫出 output 由 runtime 逐幀決定(見下方「重用」)。
    • 使用者 shader 每個 //!HOOK 為獨立 pass,不合併(同 mpv)。
  3. 監控:彙整 runtime 回報的耗時供 Governor(§6.7)使用,逾時即 bypass 該節點或段。

DisplayRuntime(FlowMetal4):

  1. 資源:暫存紋理由 MTLHeap 配置並依生命週期(最後一個讀取者)做 aliasing,4K 管線的暫存記憶體可降低 40–60%;Frame Graph 段輸出要進 FrameQueue,改由 CVPixelBufferPool 配置。
  2. 合併 pass:依 DisplayPlan 把同一 pass 的步驟編成單一 kernel;Frame Graph 的 Metal 段同此規則。
    • 只含內建步驟:一個 uber kernel,以 function constant 開關各步驟並特化 pipeline;組合有限,全部預先建立。
    • 含 MTL4PointwiseNode:各節點提供 MSL 函式片段與參數,由 runtime 與內建步驟依序串成 kernel 背景編譯,以整串的雜湊快取;就緒前比照下方「Pipeline cache」先 bypass。
  3. 只用 compute:所有 pass 皆為 compute kernel,不用 render pass。驅動依行程用過的 encoder 種類配置 GPU context(M4 Max:compute 160 MB、render 224 MB、兩者並用約 290 MB),單一種類最省且路徑單純。render 專屬功能的替代:
    • mpv fragment 型 hook 編成 kernel;dFdx 等微分以 quad shuffle 模擬,隱含 LOD 取樣改為 level(0)。
    • 字幕不畫四邊形,改取樣 overlay 紋理(§9)。
    • 不用 MetalFX(內部為 render),縮放器以 kernel 實作。
  4. 提交:一幀一個 command buffer(重複使用 command allocator + argument table),最多 3 幀 in-flight,以 MTLSharedEvent 控制。
  5. Pipeline cache:所有 MTLComputePipelineState 由 MTL4Compiler 在背景預先建立並快取於記憶體(模組啟用時 prewarm 內建組合),避免首次啟用節點時卡頓;跨啟動的快取以 MTL4Archive 持久化(待實作);首幀不等待,pipeline 未就緒的選配節點先 bypass,就緒後再啟用。
  6. 計時:每個 command buffer 以 MTL4CommitFeedback 的 GPU 起訖時間量測;多 pass 時改以 MTL4CounterHeap 逐 pass 量測(待實作)。結果回報給 FlowGraph 與統計 OSD;合併的步驟共用一個量測值。
  7. 收尾:overlay、dither 併入最後一個 pass 的 kernel:取樣 overlay 紋理 blend、dither 後寫入 drawable;中間值留在暫存器,且維持 overlay → dither 順序。
  8. 重用:同一幀可能重繪(跨多個 vsync 或暫停)、有 overlay,且管線不只一個 pass 時,才寫出 output,後續重繪只重做 overlay 之後;其餘情況重繪時從來源重算(單一 pass 時重算比讀 output 省頻寬)。overlay 未變時不提交,CAMetalLayer 維持顯示上一個 drawable。

6.7 Governor

依處理預算自動切換節點品質層級,讓昂貴節點(神經網路超解析、高階縮放器)負載過高時先降品質而非直接 bypass。由 FlowEngine 決策,FlowGraph 經 runtime 套用。

  • 品質層級:節點於 NodeDescriptor 宣告層級數(如 AI 模型大小、縮放器 kernel),未宣告者只有一級。各層級輸出的 NodeSpec 須相同,故切換不重新編譯、不遞增 generation、保留已預讀的幀。
  • 預算:
    • GPU:兩張圖所有 pass 的 GPU 時間佔經過時間的比例 ≤ Property render.gpu-budget(預設 0.8),依 runtime 的 GPU 計時(§6.6)。
    • Frame Graph:預讀水位持續下降視同超出預算,涵蓋 CPU、ANE 等非 GPU runtime。
    • ProcessInfo.thermalState 達 .serious 或啟用低耗電模式時調降預算。
  • 決策:超出預算時,將近期耗時最高且仍可降的節點降一級;持續低於預算一段時間才逐級回升,回升後立即超出則暫時鎖定該層級,避免來回切換。
  • 與降級的關係:為 §5.4 落後降級的第一步;所有節點降至最低層級仍不足才進入後續步驟。
  • 控制:Property render.governor(auto/off);各節點目前層級顯示於統計 OSD。

7. 模組

7.1 模組模型

所有實作(解碼器、runtime、節點、輸出、功能)皆為第一方模組:原始碼位於 Modules/,靜態連結進 App,由 composition root 組裝。不支援外部模組(無 bundle、manifest、下載或安裝)。

// Packages/Composition/Sources/FlowComposition/Modules.swift:唯一知道用了哪些實作的地方
import FlowAVAudio
import FlowFFmpeg
import FlowFile
import FlowMetal4
import FlowVideoToolbox

public enum FlowModules {
    public static let essential: [ModuleEntry] = [
        .essential(FileModule.self),
        .essential(FFmpegModule.self),       // 替換:改這行與對應 import(另加一行 package 依賴)
        .essential(VideoToolboxModule.self),
        .essential(AVAudioModule.self),
        .essential(Metal4Module.self),
        // .optional(EffectsModule.self),    // 選用模組
    ]
}
  • FlowPlayer:FlowComposition 以 FlowPlayer 組裝 ComponentRegistry、PlayerController、ModuleHost、PlayerSession,四者皆不對外公開。App 只用 api(PlayerAPI)、makeVideoView()、attach(_:);影片 view 由 PresenterFactory.makeSurface() 提供,App 與 composition root 都不需知道 presenter 型別。Benchmarks 經同一個 FlowPlayer 組裝,量到的就是發行的。

  • 替換:同一介面的實作可互換,改模組名稱與 import 即可;依賴某 runtime 的模組(如 FlowGLSL 依賴 FlowMetal4)需隨該 runtime 一起替換。

  • 必要性:.essential 不可停用;.optional 可於執行期停用與重新啟用。

  • in-process:模組本身不使用 ExtensionKit/XPC;完整 SwiftUI、直接呼叫框架 API、熱路徑零開銷。

  • Library validation:主程式不載入外部程式碼,Hardened Runtime 下無需 disable-library-validation;需執行外部程式碼的模組(如 VapourSynth)一律交由 helper 行程。

不提供 Script(JavaScript):帶來額外 runtime 與維護負擔。

7.2 Module API

public protocol FlowModule: AnyObject, SendableMetatype {
    /// 穩定識別碼;預設為完整型別名稱
    static var id: String { get }
    init()
    /// 只在這裡註冊;停用時由 FlowModuleHost 撤銷所有註冊
    @MainActor func activate(_ context: any ModuleContext) throws
    @MainActor func deactivate()
}

@MainActor
public protocol ModuleContext {
    var media: any MediaRegistry { get }        // MediaSourceFactory、DemuxerFactory、外部字幕來源
    var decoders: any DecoderRegistry { get }   // VideoDecoderFactory / AudioDecoderFactory / SubtitleDecoderFactory
    var outputs: any OutputRegistry { get }     // AudioOutputFactory、PresenterFactory
    var runtimes: any RuntimeRegistry { get }   // FrameRuntime / DisplayRuntime
    var player: any PlayerAPI { get }           // Command / Property / Event(§8)
    // 待實作:
    var subtitles: any SubtitleRegistry { get } // SubtitleRendererFactory
    var nodes: any NodeRegistry { get }         // 可供使用者加入的節點類型
    var ui: any UIRegistry { get }              // 分頁、設定面板、選單、OSD 元件
    var storage: any ModuleStorage { get }      // 模組專屬設定與檔案
}
  • 能力可任意組合:一個模組可同時註冊解碼器、runtime、節點、UI 分頁並呼叫 API。
  • UI:ui 接受原生 SwiftUI View,與內建畫面無差別。
  • 版本:與 App 一起建置,FlowInterfaces 不需維持 ABI 穩定,可隨需求重構。
  • 停用:撤銷所有註冊即生效;播放中的媒體沿用已建立的元件,下次載入時 FlowEngine 依 §5.3 改選其他實作。重新啟用後恢復原本的偏好順序。
  • 故障:GPU 或 Frame Graph 段逾時時,立即 bypass 該節點或段並通知使用者,播放不中斷。

7.3 範例:解碼器模組(Modules/FFmpeg)

  • 註冊 FFmpegDemuxerFactory、FFmpegVideoDecoderFactory(.software)、FFmpegAudioDecoderFactory、FFmpegSubtitleDecoderFactory、FFmpegBitmapSubtitleRendererFactory。
  • 目前已註冊 FFmpegDemuxerFactory、FFmpegAudioDecoderFactory。
  • 只匯入 libav*/libswresample XCFramework 與 FlowInterfaces、FlowBase;C 型別不越過模組邊界。
  • 新增其他解碼器(如另一個 AV1 實作)=新增模組實作 VideoDecoderFactory,選擇與 fallback 由 FlowEngine 處理。

7.4 範例:Metal 4 Runtime(Modules/Metal4)

  • Runtime:Metal4DisplayRuntime 實作 DisplayRuntime(§6.6 資源、提交、pipeline cache、計時);Frame Graph 上線時一併實作 FrameRuntime。
  • 節點協定:MTL4ShaderNode: ShaderNode,供其他模組實作;MSLShaderNode 為以 MSL 原始碼建立的通用實作。逐像素節點改實作 MTL4PointwiseNode: ShaderNode(提供 MSL 函式片段與參數,不自行 dispatch),以便合併 pass(§6.6)。
  • 內建節點:色彩轉換、chroma 升取樣、線性化、fallback 縮放器/tone mapping/dither、overlay 合成。
  • Presenter:MetalLayerPresenter(MetalVideoView 由 makeSurface() 提供)、SampleBufferPresenter(待實作,§5.6);OffscreenPresenter 以 @_spi(Benchmark) 開放,僅供 Benchmarks 自行註冊(BENCHMARK §7)。
  • Shader ABI:Shaders/Include/Flow/(FlowShader.h:紋理/buffer 索引與 DisplayUniforms,使用者 shader 用的 FlowFrameInfo 待加入;Color.h:轉換函式),供內建 shader 與依賴 FlowMetal4 的模組共用,建置期編譯為 metallib。使用者 .metal 也依賴此 ABI,故需維持相容:破壞性變更遞增 major,shader 以 //!FLOW <major> 宣告,runtime 支援目前與前一個 major。

7.5 範例:GLSL 前端與使用者 Shader(Modules/GLSL、Modules/UserShaders)

FlowGLSL 只做語言前端:GLSL → glslang → SPIR-V → SPIRV-Cross → MSL,產生 GLSLShaderNode: MTL4ShaderNode,與其他 Metal 節點同段執行。新增語言(如 Slang)只需新增對應的前端模組。

FlowUserShaders 讓使用者匯入兩種 shader,共用同一套 //! 指示詞與 hook 語意。

  • 格式:
    • .glsl:mpv/libplacebo GLSL,產生 GLSLShaderNode;常見 shader(FSRCNNX、RAVU、Anime4K、ArtCNN、KrigBilateral、SSimSuperRes)免修改即可使用。
    • .metal:MSL,依 Flow Shader ABI(§7.4),產生 MSLShaderNode;免轉譯,可用 Metal 專屬功能(SIMD-group、half、threadgroup 調校)。
  • 匯入:開檔或拖放 .glsl/.metal,複製到 Application Support;不提供網路下載。啟用清單與順序為 Property render.user-shaders(對應 mpv glsl-shaders,兩種格式可混排),可寫進 Profile。
  • 指示詞://!HOOK、//!BIND、//!SAVE、//!DESC、//!WIDTH///!HEIGHT、//!OFFSET、//!COMPONENTS、//!WHEN、//!COMPUTE、//!TEXTURE,以及 libplacebo 擴充 //!PARAM(顯示為節點參數)、//!BUFFER。.metal 另需 //!FLOW <major>。
  • 編譯:
    • .glsl:前置 mpv 內建定義(HOOKED_tex、NAME_pos/size/pt/mul/rot/off、frame、random、input_size、target_size、tex_offset 等)後交給 FlowGLSL。
    • .metal:每個 pass 實作 hook()(同 mpv);前置 Include/Flow/ 內容(執行期編譯無法 #include)並產生 entry point,依宣告順序綁定 HOOKED///!BIND 紋理、FlowFrameInfo 與參數(按 MSL 對齊打包)。
    • 非 //!COMPUTE 的 pass 包成 kernel,一個 thread 寫一個輸出像素(§6.6「只用 compute」)。
    • 兩者皆以 MTL4Compiler 背景編譯,以原始碼雜湊快取 MSL 與 MTL4Archive;失敗只略過該 shader 並提示。
  • 節點化:每個 pass 在每個 //!HOOK 目標上各成為一個 .display(hook) 節點(hook 對應見 §6.4);//!BIND 依 §6.4 紋理命名空間解析,綁定不到時略過該 pass(同 mpv);//!SAVE 紋理由 runtime 管理並參與 aliasing,與其他節點共用排程、GPU 計時與逾時 bypass。
  • 求值://!WHEN、//!WIDTH///!HEIGHT 的 RPN 運算式在 FlowGraph 編譯時求值,所依賴的尺寸或參數變化時才重新編譯;frame、random 每幀更新。
  • 與 mpv 的差異:只掛在 Display Graph;MAINPRESUB 等同 MAIN(字幕於 overlay 合成);OUTPUT 在 overlay 之前,不作用於字幕與 OSD(同 vo_gpu;libplacebo 在之後)。
  • 未觸發提示:hook 未觸發的 shader(如來源為 RGB 時的 LUMA、render.scale-light=gamma 時的 LINEAR)標示於統計 OSD。
  • 參考實作:除上列差異外,語意以 libplacebo 的 mpv shader 解析為準(只比對行為,不移植程式碼)。

7.6 範例:VapourSynth Runtime(Modules/VapourSynth)

  • Runtime:VSRuntime 只實作 FrameRuntime;連續 VapourSynthFilterNode 經 makeSegment 組成單一 VS 圖,以自訂來源 filter 把上游 FrameSource 注入為 video_in。
  • 幀編號:VS 是隨機存取模型,由 VSRuntime 在段內轉換:n 為本 generation 起算的輸入序號,總幀數回報極大值;來源 filter 收到 getFrame(n) 時向上游 next() 直到第 n 幀,並快取時間窗內的幀,超出快取的倒退請求視為錯誤並 bypass。輸出 PTS 在 .passthrough 沿用第 n 個輸入幀、.retimed 依 §6.5 時間格;reset 時重建 VS 圖。
  • 節點:VapourSynthFilterNode: FilterNode,含通用「VapourSynth Script」(使用者 .vpy)與選配預設節點(QTGMC 等,宣告參數)。
  • UI:新增分頁(腳本編輯、VS/Python 路徑、VS plugin 管理)。
  • 隔離:VS、Python、VS plugin 與 .vpy 一律在 helper 行程執行(IOSurface 經 XPC 傳遞),框架不介入;disable-library-validation 只授予 helper,主程式維持 library validation,Python 崩潰也不影響播放。
  • 成本:VS 只接受 planar 格式且無外部記憶體 import,IOSurface 與 VSFrame 間需轉換與複製;每個跨 runtime 邊界的成本顯示於統計 OSD,連續 VS 節點應排在一起。

8. Command/Property 系統

整個播放器的「公開介面」,UI 與模組使用同一套,確保模組能做到內建 UI 能做的任何事。

  • Property:階層式、型別化、可觀察。例如 playback.position、playback.rate、video.params.colorspace、render.nodes、render.governor、render.gpu-budget、video.rotate、video.aspect、audio.volume、audio.mute、audio.delay、sub.track、video.sync、display.refresh-rate/display.max-refresh-rate(唯讀)。
  • Command:具名、參數型別化、可非同步。例如 load、seek、node.enable、screenshot、sub.add。
  • Event:file-loaded、seek、end-file、track-switched、render-stats 等。
  • 對接:
    • SwiftUI → 以 PlayerViewModel 投影 Property。
    • 按鍵綁定 → input.conf 風格的對應表;可手改檔案或於設定視窗編輯,兩者讀寫同一份檔案。
    • App Intents/捷徑/AppleScript → 包裝常用 Command。
    • AI agent → MCP tools(FlowMCP)包裝 Command 與 Property。
    • 模組 → 經 ModuleContext.player 直接使用。

設定與 Profiles

設定分層合併:內建預設 < 使用者設定 < 條件式 Profile < 單檔覆寫 < 執行期。 條件式 Profile 範例:video.hdr == true && display.edrHeadroom < 2 時自動套用較激進的 tone mapping。


9. 字幕

核心定義 SubtitleDecoder(packet → SubtitleEvent)與 SubtitleRenderer(SubtitleEvent → OverlayFrame:IOSurface + 位置),並負責字幕軌選擇、延遲與時間對齊。

public enum SubtitleEvent: Sendable {
    case ass(ASSEvent)                // 文字字幕一律轉為 ASS
    case bitmap(BitmapSubtitle)
}

public struct ASSEvent: Sendable {
    public let pts: MediaTime
    public let duration: MediaTime?   // nil = 直到下一個事件
    /// MKV 格式事件行(ReadOrder,Layer,Style,Name,MarginL,MarginR,MarginV,Effect,Text):
    /// 即 libavcodec 的輸出與 `ass_process_chunk` 的輸入,兩端免轉換
    public let line: String
}

public struct BitmapSubtitle: Sendable {
    public let pts: MediaTime
    public let duration: MediaTime?   // nil = 直到下一個事件(PGS 一律如此)
    public let canvasSize: PixelSize  // 座標基準(PGS/VobSub 畫面尺寸)
    public let rects: [BitmapRect]    // 位置、尺寸、stride、premultiplied BGRA;空 = 清除
}

public protocol SubtitleDecoder: AnyObject {
    /// 文字字幕軌的 ASS header(styles 等);點陣字幕為 nil
    var assHeader: String? { get }
    func decode(_ packet: Packet) throws -> [SubtitleEvent]
    func flush()
}

public protocol SubtitleRendererFactory: Sendable {
    var id: String { get }
    func support(for kind: SubtitleKind) -> Bool     // .ass / .bitmap
    func make(assHeader: String?, attachments: [Attachment]) throws -> any SubtitleRenderer
}

public protocol SubtitleRenderer: AnyObject {
    func add(_ event: SubtitleEvent)
    func flush()
    /// Subtitle 執行緒;內容未變時回傳前一個結果
    func render(at time: MediaTime, target: OverlayTarget) -> OverlayFrame?
}
職責實作範圍
解碼FlowFFmpeg(libavcodec)文字:SRT、WebVTT、ASS/SSA、mov_text 等 → ass;點陣:PGS、VobSub、DVB、XSUB → bitmap
渲染 ASSFlowLibasslibass 輸出 glyph bitmap → 合成為單一 RGBA IOSurface;完整支援特效與內嵌字型
渲染點陣FlowFFmpegRGBA → IOSurface
  • FlowEngine 依事件種類選用 SubtitleRendererFactory;FlowLibass 只依賴 FlowInterfaces,不依賴 FFmpeg。
  • 執行緒:解碼與渲染皆在 Subtitle 執行緒(§5.1);libass 遇複雜特效單次光柵化可達數十 ms,故 Render 執行緒只依顯示 PTS 取用已備妥的 OverlayFrame。
  • OverlayTarget:輸出像素尺寸、影片於輸出中的位置、Retina 倍率;供 libass 設定畫面尺寸與邊界。尺寸變更時重新光柵化,完成前沿用舊 overlay 縮放顯示。
  • 重新光柵化:只在內容改變時進行(libass 以 ass_render_frame 的變更旗標判斷)。DisplayRuntime 在 overlay hook 取樣 overlay 紋理合成(§6.6「收尾」)。
  • 內嵌字型:Demuxer.attachments 經 make 交給 libass(ass_add_font)。
  • 外部字幕檔:同樣經 Demuxer(libavformat)讀取,開檔時一次讀完並解碼。
  • HDR 內容下字幕亮度以可設定的 nits 值(預設 203 nits,依 BT.2408)合成,避免刺眼。
  • 支援雙字幕(兩個 SubtitleRenderer 實例)、字幕延遲調整、外部字幕自動比對、模組擴充字幕來源。

10. 平台整合

  • 視窗:多視窗、原生全螢幕、浮動視窗、Spaces。
  • Now Playing/媒體鍵:MPNowPlayingInfoCenter、MPRemoteCommandCenter。
  • PiP:SampleBufferPresenter(FlowMetal4)。
  • 檔案存取:一般 bookmark(記住最近檔案;不啟用 App Sandbox)。
  • 網路來源:SMB/WebDAV/HTTP 為 MediaSource(FlowNetwork);HLS 由 demuxer 處理(§5.2)。SMB 以 libsmb2 在行程內實作,讀取可逾時與取消;不掛載(NetFS 的核心讀取無法中斷),不用 libsmbclient(GPLv3)。
  • 自動化:App Intents、AppleScript、URL scheme、MCP。
  • MCP:FlowMCP 於 http://127.0.0.1:17996/mcp 提供 Streamable HTTP(不開 SSE 串流、不用 session),檢查 Host 與 Origin 以防 DNS rebinding;不另做 stdio helper,App 須先開啟。使用方式見 MCP.md。
  • 外接顯示:多顯示器、依顯示器 EDR 能力切換管線。
  • 關於:顯示 LICENSE 與 THIRD_PARTY_NOTICES.md(建置時複製進 App bundle)。

11. 可觀測性與測試

11.1 可觀測性

  • os.Logger:依模組分 subsystem/category。
  • OSSignposter:demux、decode、每個 pass、present 都有 interval,可直接在 Instruments 分析。
  • 統計 OSD(類似 mpv 的 i 鍵):選用的實作、色彩參數、HDR metadata、同步模式、音訊重取樣偏移、降級層級、Governor 品質層級、丟幀/延遲幀、A/V 漂移、各節點 GPU 時間、跨 runtime 邊界成本。

11.2 測試策略

  • 單元測試:時鐘(以虛擬時鐘模擬漂移、換裝置、underrun)、佇列、狀態機、模組註冊與撤銷、Property 系統、Governor(以模擬耗時驗證遲滯)。
  • 核心整合測試:FlowEngine/FlowGraph 以 mock 模組跑完整生命週期(選擇、fallback、seek、bypass),不需 GPU 與 vendored 函式庫。Mock 統一放在 FlowTestSupport,不在各測試 target 複製。
  • 模組測試:只依賴自身與 FlowTestSupport,不依賴其他模組與控制平面(註冊以 TestModuleContext 驗證);需要真實封包時以系統框架讀 fixture(如 VideoToolbox 測試用 AVFoundation passthrough)。跨模組路徑由 IntegrationTests 負責。
  • 邊界檢查:SwiftLint custom rule(core_banned_import、core_banned_link)確認 Packages/Core 未匯入 GPU/解碼 API 與 vendored C 函式庫(§4)。
  • Golden image:每個內建節點與色彩轉換以參考畫面比對(容許 ΔE/PSNR 閾值);常見 mpv shader 與 mpv 輸出比對。
  • 一致性測試語料:涵蓋各容器、編碼、HDR 格式、字幕格式的測試影片集(含邊界案例:VFR、損壞檔、超長 GOP)。
  • 效能測試:見 BENCHMARK.md。
  • 幀時間測試:以 display link 時間戳驗證 pacing:24p@120Hz 每幀恰好 5 個 vsync;23.976p 補幀到 120@120Hz 每幀 1 個 vsync、音訊重取樣約 0.1%;25p@60Hz 降為 audio;負載不足時依序降級且不在模式間來回切換。
  • Fuzzing:Demux 與字幕解析以 libFuzzer 針對畸形輸入。

12. 關鍵架構決策(ADR 摘要)

#決策替代方案理由
1自建播放管線(FFmpeg demux + VT decode + Metal)純 AVFoundationAVFoundation 不支援 MKV/多數字幕/逐幀 GPU 處理管線;自建才能做到完整影像處理與擴充
2YUV→RGB 由 Graph 處理,不交給解碼器VT 輸出 BGRA控制色彩精度、chroma 升取樣品質,並讓 Frame Graph 可存取原生 YUV
3預設 display-resample,條件不符退回音訊時鐘只用音訊時鐘/只用視訊時鐘刷新率為幀率整數倍時以微量重取樣換得無 judder 的 pacing(補幀尤其需要);否則音訊連續優先,因人耳對不連續遠比對丟/重複幀敏感
4Frame Graph(自由排序)+ Display Graph(固定 hook)單一固定 hook 圖/單一自由 DAG與顯示無關的處理可任意排序並混用 runtime;依賴顯示參數的步驟固定、語意可預期
5模組僅第一方、靜態連結外部 bundle/ExtensionKit + XPC/Script完整 SwiftUI、直接呼叫 API、熱路徑零開銷;免除外部程式碼的簽章、信任與崩潰歸因問題
6資料平面不使用 actor全面 Swift Concurrency熱路徑需要可預測延遲,避免 executor 排程與 suspension 的抖動
7LGPL 元件(FFmpeg、FriBidi、libsmb2)動態連結靜態連結/GPL 組態符合 LGPL 重新連結要求,與 Apache-2.0 本體相容
8核心只含介面與控制平面,所有實作皆為模組核心內建預設實作實作可替換、可擴充;核心能以 mock 測試;官方實作與擴充走同一套介面
9使用者 shader 採 mpv 指示詞格式,本體可為 GLSL 或 MSL自訂格式/僅 GLSL/僅 MSLGLSL 沿用 mpv 生態現成 shader,MSL 免轉譯且可用 Metal 專屬功能;兩者共用 hook 語意與解析器
10節點/runtime/語言前端分離每種語言自帶執行環境不同語言的節點同段執行,共用 command buffer、aliasing 與計時;新語言只需前端
11佇列、執行緒、時鐘歸核心由各實作自行管理生命週期與即時規則可由核心保證,實作不必重複造輪子
12核心不抽象 GPU API核心定義跨 API 的紋理/編碼器抽象必然洩漏且破壞 zero-copy;段間只傳 VideoFrame,段內用 runtime 原生型別
13本體採 Apache-2.0GPL/MPL-2.0/閉源與所有依賴相容、含貢獻者專利授權
14僅以 GitHub Release 與 Homebrew Cask 發行Mac App Store免 App Sandbox 與審查限制(使用者 shader、VapourSynth、任意檔案存取);以 Developer ID + notarization 保障 Gatekeeper
15Frame Graph 依 PTS 串流 pull依幀編號隨機存取(VapourSynth 模型)解碼器為 push、VFR/直播總幀數未知、seek 後幀編號無定義;隨機存取只在需要的 runtime 內轉換
16逐像素步驟合併成單一 pass,只在取樣前切開每個步驟或 hook 一個 pass中間紋理讀寫是頻寬瓶頸,運算量不變;與 mpv/libplacebo 相同
17GPU 工作只用 computerender pass/兩者並用驅動每行程 context 只付一種且最小(§6.6);單一路徑;render 專屬功能皆有 compute 替代

13. 主要風險

風險影響緩解
Dolby Vision 正確性色彩錯誤(P5 綠紫色偏)早期建立 DV 測試語料;依規格自行實作,以 libplacebo 輸出比對;以 golden image 驗證
節點或使用者 shader 拖垮效能或 GPU hang卡頓/丟幀GPU 時間監控、command buffer 逾時偵測後 bypass 該節點;每個模組與 shader 的 signpost 與統計
介面邊界劃分不當實作被迫繞過介面、或介面過度抽象先以兩個以上實作驗證介面(如 VT + FFmpeg、Metal + VS);介面無 ABI 承諾,可隨時重構
mpv shader 相容性輸出與 mpv 不同/無法編譯以 libplacebo 語意為準;常見 shader 以 mpv 輸出做 golden image 回歸
藍牙/AirPlay 輸出延遲回報不準A/V 失步延遲隨裝置變更重新查詢;提供 audio.delay 手動補償;評估以 AVSampleBufferAudioRenderer + AVSampleBufferRenderSynchronizer 實作另一個 AudioOutput
FFmpeg 安全性(畸形檔案)崩潰/漏洞Fuzzing、跟進上游安全修正、精簡啟用的 demuxer/decoder
授權違規(依賴、模型、內建 shader)無法散布新增前依 THIRD_PARTY_NOTICES.md 審查;禁止 GPL、AGPL、非商用;內建 shader 與移植程式碼限寬鬆授權