跳到主要內容
Flow

Player API

播放器唯一的公開介面 PlayerAPI,以及經由它的平台整合。總覽見 architecture.md。

Command/Property 系統

整個播放器的「公開介面」,UI、自動化與模組使用同一套(借鏡 mpv),確保模組能做到內建 UI 能做的任何事。PlayerAPI 定義於 FlowPlayerAPI,由 FlowController 的 PlayerController 實作;FlowEngine 註冊 Command handler 並觀察 Property。

@MainActor
public protocol PlayerAPI: AnyObject {
    var properties: [PropertyDescriptor] { get }    // 目錄
    func value<Value>(of key: PropertyKey<Value>) -> Value
    /// 未登記、唯讀與無效值拋錯;正規化後(如 clamp)的值才存入
    func set<Value>(_ key: PropertyKey<Value>, to value: Value) throws(PropertyError)
    func observe<Value>(_ key: PropertyKey<Value>, _ handler: @escaping @MainActor (Value) -> Void) -> ObserverToken
    func value(ofProperty name: String) -> PropertyValue?
    func set(property name: String, to value: PropertyValue) throws(PropertyError)
    func execute<Arguments>(_ command: CommandKey<Arguments>, _ arguments: Arguments) async throws
    func execute(_ command: InputCommand) async throws   // 文字形式,見按鍵綁定
    var bindings: [InputBinding] { get }                 // 按鍵綁定
    func onEvent(_ handler: @escaping @MainActor (PlayerEvent) -> Void) -> ObserverToken
    var statistics: PlayerStatistics { get }             // 統計,每次讀取時拉取
}
  • Property:具名、型別化、可觀察;value(of:) 參與 Observation tracking,SwiftUI 直接更新。清單與行為見 properties.md。

    • 目錄:每個 Property 以 PropertyDescriptor 登記名稱、值型別、預設值、唯讀、cycle 的步階(steps)、正規化與給自動化的說明;內建目錄為 PlayerVocabulary.swift 的 PropertyDescriptor.builtins,PlayerController 建立時登記。PropertyKey 只是型別化的名稱,規則一律取自登記的描述,不取自呼叫端。
    • 依名稱存取:value(ofProperty:)、set(property:to:) 以 PropertyValue 讀寫,解碼後與型別化 API 走同一套規則;供 MCP 等只有名稱的呼叫端,MCP 的 set schema 與 status 由目錄產生。
    • 唯讀:只有引擎經 PlayerController.publish 寫入;前端與模組只拿到 PlayerAPI,碰不到 Kernel。
    • 分層:每個 Property 有預設層與執行期層,set、publish 寫入執行期層,設過即優先;PlayerController.setDefault(property:to:) 替換預設層。
  • Command:具名、參數型別化、可非同步:load(LoadRequest:URL 與 paused)、seek(SeekRequest:絕對或相對目標與 SeekMode;相對目標以進行中 seek 的目標為基準,見狀態機與 Seek)、play、pause、stop、add、cycle。

    • add、cycle:泛用的 Property 操作(同 mpv),由 PlayerController 處理、不需引擎。add(AddRequest)對整數或小數 Property 加減;cycle(CycleRequest:up 或 down)切換 Bool,或移到描述的 steps(如 playback.rate 的速度分級)中的下一階,到端點停住。結果與 set 走同一套正規化(如 clamp)。
  • load:UI、MCP 與模組呼叫時行為相同。

    • 已開啟媒體時,先開啟新的來源與容器(MediaSourceFactory、demuxer);開不了時拋 PlayerError,目前的媒體繼續播放,Property 不變也不發 Event。
    • 開啟後才卸載目前的媒體,再把 LoadRequest.paused(預設 false)寫入 playback.paused 並進入 loading:新檔案一定播放,除非要求暫停;載入期間設為暫停則停在首幀。
    • 未開啟媒體時直接進入 loading;此後的失敗(含開不了、沒有可播放的軌)進入 error。
  • Event:fileLoaded、seek、playbackRestart(load 或 seek 後首幀顯示)、endFile(EOF、停止或錯誤)。

  • 順序:Property 先更新,再發出 Event;handler 收到 Event 時相關 Property 已是新值(如 fileLoaded 時的 playback.state、playback.duration)。playback.error 在 playback.state 變為 error 之前寫入。Property 的 observer 在寫入時同步呼叫,因此也早於 Event。

按鍵綁定

按鍵綁定是資料而非 UI 程式碼:每行一個按鍵與一行指令,格式同 mpv 的 input.conf(如 SPACE cycle playback.paused、RIGHT seek 5)。

  • 預設表:FlowController 的 InputConf.defaults,PlayerController 建立時以 InputBinding.list(parsing:) 解析,經 PlayerAPI.bindings 提供給前端;各鍵見 getting-started.md。同一個鍵後出現的行取代前面的,# 起為註解。
  • 按鍵(KeyChord):平台中立的名稱,修飾鍵 Ctrl+、Alt+、Shift+、Meta+(macOS 的 ⌘)在前;特殊鍵 SPACE、ENTER、ESC、TAB、BS、DEL、LEFT、RIGHT、UP、DOWN、HOME、END、PGUP、PGDWN,名稱與修飾鍵不分大小寫;其他為單一字元,分大小寫(M 即 Shift+M)。
  • 指令(InputCommand):名稱與以空白分隔的參數(不支援引號),由 PlayerAPI.execute(_:) 依 CommandDescriptor 解析成型別化參數再執行;名稱不明拋 CommandError.unknownCommand,參數不合拋 invalidArguments。有文字形式的 Command:
    • seek <秒> [relative|absolute] [exact|keyframe]:預設 relative、exact。
    • add <property> [<數值>]:預設 1。
    • cycle <property> [up|down]:預設 up。
    • play、pause、stop。
  • 前端:只負責把綁定轉成平台的快捷鍵與選單項目,動作一律執行綁定的指令,不另寫行為。

錯誤

載入與播放失敗以 PlayerError 表達:穩定的 code(如 cannotOpen、unsupportedFormat、decodeFailed)加上英文的 detail(失敗的元件與原因,供 log 與回報)。PlayerError 是 LocalizedError,localizedDescription 依 code 產生給使用者的訊息(UI-06 前只有英文)。

  • Command 失敗時拋 PlayerError;被較新的 load 或 stop 取代的 load 拋 CancellationError。play、pause、seek 在未開啟媒體時拋 notLoaded。
  • 播放失敗時依序寫入 playback.error、playback.state(error),再發出 endFile(.error);下次 load 或 stop 清除。

統計

PlayerAPI.statistics 回傳 PlayerStatistics(FlowPlayerAPI),前端、模組、MCP 與 Benchmarks 都讀這一份;累計值只增不減,兩次讀取相減即區間值。

  • 按需拉取:每次讀取時 PlayerController 呼叫 PlayerSession 登記的 provider,讀出原子計數器;沒人讀時不做任何事。不做成 Property 或 Event:計數每幀都變,發布須每幀或定時在 main actor 寫入與通知觀察者,即使沒人看也要付;讀取端(OSD、MCP)自己決定頻率。
  • playback:核心的 PlaybackStatistics:顯示、丟棄、溢出、延遲幀、underrun 與呈現延遲。
  • display:與 presenter 配對的 runtime 經 DisplayConnection.statistics 回報:提交、略過、pass 數、GPU 時間與解碼 surface(見 Runtime 介面);attach 前為 nil,重新配對時從 0 開始。
  • planPassCount:目前 DisplayPlan 的 pass 數,須等於 display.lastPassCount(見 Pass 數)。

平台整合

對象實作
SwiftUIFlowUI 的 PlayerViewModel 投影 Property(是否開啟媒體看 playback.path),把 UI 操作轉成 Command 與 Property;錯誤顯示 PlayerError 的 localizedDescription;seek bar 拖動時 .keyframe、放開時一次 .exact
快捷鍵FlowUI 的 PlayerCommands 把 PlayerAPI.bindings 轉成 KeyboardShortcut 與選單項目,執行綁定的指令(見按鍵綁定):只改 window.* 的列在 View 選單、隨時可用,其餘列在 Playback 選單、開啟媒體才可用,相鄰項目的 Property 類別或 Command 不同時以分隔線隔開
AI agentFlowMCP 以 MCP tools 包裝 Command 與 Property,使用方式見 mcp.md
模組經 ModuleContext.player 直接使用
  • 視窗:單一視窗,依影片尺寸調整並鎖定長寬比;原生全螢幕。
  • 開檔:⌘O、拖放、Finder「打開檔案的應用程式」;不啟用 App Sandbox。
  • MCP:FlowMCP 於 http://127.0.0.1:17996/mcp 提供 Streamable HTTP(不開 SSE 串流、不用 session),檢查 Host 與 Origin 以防 DNS rebinding;不另做 stdio helper,App 須先開啟。
  • 授權:LICENSE 與 THIRD_PARTY_NOTICES.md 等授權檔於建置時複製進 App bundle 的 Resources/Licenses/。