# Player API

URL: https://flow.lanstar.dev/docs/architecture/player-api

> 播放器唯一的公開介面 `PlayerAPI`，以及經由它的平台整合。總覽見 [`architecture.md`](/docs/architecture)。

## Command／Property 系統

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

```swift
@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`](/docs/properties)。
    - **目錄**：每個 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](/docs/architecture/engine#狀態機與-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`](/docs/getting-started#快捷鍵)。同一個鍵後出現的行取代前面的，`#` 起為註解。
- **按鍵**（`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 介面](/docs/architecture/rendering#runtime-介面)）；attach 前為 `nil`，重新配對時從 0 開始。
- **`planPassCount`**：目前 `DisplayPlan` 的 pass 數，須等於 `display.lastPassCount`（見 [Pass 數](/docs/benchmark#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`](/docs/mcp) |
| 模組 | 經 `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/`。
