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 的setschema 與status由目錄產生。 - 唯讀:只有引擎經
PlayerController.publish寫入;前端與模組只拿到PlayerAPI,碰不到 Kernel。 - 分層:每個 Property 有預設層與執行期層,
set、publish寫入執行期層,設過即優先;PlayerController.setDefault(property:to:)替換預設層。
- 目錄:每個 Property 以
-
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 數)。
平台整合
| 對象 | 實作 |
|---|---|
| SwiftUI | FlowUI 的 PlayerViewModel 投影 Property(是否開啟媒體看 playback.path),把 UI 操作轉成 Command 與 Property;錯誤顯示 PlayerError 的 localizedDescription;seek bar 拖動時 .keyframe、放開時一次 .exact |
| 快捷鍵 | FlowUI 的 PlayerCommands 把 PlayerAPI.bindings 轉成 KeyboardShortcut 與選單項目,執行綁定的指令(見按鍵綁定):只改 window.* 的列在 View 選單、隨時可用,其餘列在 Playback 選單、開啟媒體才可用,相鄰項目的 Property 類別或 Command 不同時以分隔線隔開 |
| AI agent | FlowMCP 以 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/。