# Initial architecture gate

本节记录最初引入 native Widget 播放链时的 8 张 gate 图。任意显示比例改造的当前/目标图及 Interface 决策见 [`arbitrary-aspect-design.md`](arbitrary-aspect-design.md)。

## 架构/调用关系图：当前

```mermaid
flowchart LR
    user["用户"] --> shell["Ghostty shell"]
    shell --> cli["mpv --vo=tct"]
    cli --> audio["系统音频设备"]
    cli --> terminal["全局终端 stdout"]
    pi["Pi TUI"] -. "不能同时安全拥有 stdout" .-> terminal
```

## 架构/调用关系图：目标

```mermaid
flowchart LR
    video["视频文件"] --> mpv["libmpv 播放实例"]
    mpv --> audio["系统音频设备"]
    mpv --> sw["Software Render API rgb0"]
    sw --> sidecar["Rust render thread"]
    sidecar --> slot["单槽 latest frame"]
    slot --> output["Rust output thread"]
    output -->|"PIV1 二进制帧"| client["TypeScript NativeClient"]
    client --> latest["Latest-frame buffer"]
    latest --> renderer["Half-block renderer"]
    renderer --> widget["Pi aboveEditor Widget"]
    widget --> tui["Pi TUI renderer"]
    client -->|"resize / stop"| sidecar
    mpv --> events["Rust event thread"]
```

## 时序图：当前

```mermaid
sequenceDiagram
    actor User as 用户
    participant Shell as Ghostty shell
    participant Mpv as mpv tct
    participant Terminal as 终端
    User->>Shell: 运行 mpv 命令
    Shell->>Mpv: 启动播放器
    Mpv->>Terminal: 接管屏幕、光标和 ANSI 输出
    Mpv-->>User: 播放画面与声音
    Mpv->>Terminal: 恢复终端
```

## 时序图：目标

```mermaid
sequenceDiagram
    actor User as 用户
    participant Pi as Pi runtime
    participant Editor as Pi editor
    participant Ext as VideoTuiExtension
    participant UI as aboveEditor Widget
    participant Client as NativeClient
    participant Native as Rust render thread
    participant Output as Latest-frame output
    participant Events as mpv event thread
    participant Mpv as libmpv
    participant Audio as 系统音频
    Pi->>Ext: session_start reason=startup
    Ext->>UI: setWidget placement aboveEditor
    Ext-->>Pi: session_start handler 完成
    UI->>Client: first render width and height
    Client->>Native: spawn video size fps
    Native->>Mpv: create initialize render context
    Native->>Events: start event loop
    Mpv->>Audio: 播放音频
    User->>Pi: 普通字符或 Enter
    Pi->>Editor: 正常输入和提交
    loop 最多 15 FPS
        Mpv-->>Native: render update
        Native->>Output: replace latest rgb0 frame
        Output-->>Client: PIV1 frame
        Client->>Client: 替换 latest frame
        Client-->>UI: requestRender
        UI->>UI: half-block render
    end
    alt 视频自然结束
        Mpv-->>Native: end-file
        Native-->>Client: end
    else 用户跳过
        User->>Ext: Escape 或 Ctrl+C
        Ext->>Client: stop
    end
    Ext->>UI: remove Widget by key
    Ext->>Client: dispose
```

## 状态图：当前

```mermaid
stateDiagram-v2
    [*] --> PiIdle: Pi 已启动
    PiIdle --> ExternalPlayback: 用户离开 Pi 执行 mpv
    ExternalPlayback --> TerminalOwnedByMpv: tct 接管全局终端
    TerminalOwnedByMpv --> PiIdle: mpv 退出并恢复终端
    PiIdle --> [*]
```

## 状态图：目标

```mermaid
stateDiagram-v2
    [*] --> Dormant
    Dormant --> Starting: session_start startup 且 TUI 可用
    Dormant --> Skipped: 非 startup、禁用或非 TUI
    Starting --> Playing: sidecar ready
    Starting --> Failed: 配置、spawn 或 libmpv 错误
    Playing --> Playing: 普通输入继续进入 Pi editor
    Playing --> Resizing: 终端宽高变化
    Resizing --> Playing: 新 framebuffer 生效
    Playing --> Stopping: EOF、跳过或 session shutdown
    Stopping --> Finished: render context、mpv、pipe 已释放
    Failed --> Finished
    Skipped --> [*]
    Finished --> [*]
```

## 类图：当前

```mermaid
classDiagram
    class UserCommand {
        +runMpvTct()
    }
    class MpvCli {
        +decodeVideo()
        +playAudio()
        +writeAnsi()
    }
    class GhosttyTerminal {
        +stdout
        +alternateScreen
    }
    UserCommand --> MpvCli
    MpvCli --> GhosttyTerminal
```

## 类图：目标

```mermaid
classDiagram
    class VideoTuiExtension {
        +onSessionStart(event, context)
        +onSessionShutdown()
    }
    class VideoTuiConfig {
        +videoPath: string | undefined
        +fps: number
        +maxWidth: number or null
        +maxRows: number or null
        +maxHeightPercent: number
        +volume: number
    }
    class VideoWidget {
        +render(width) string[]
        +handleInput(data)
        +dispose()
    }
    class NativeClient {
        +start()
        +resize(width, height)
        +stop()
        +latestFrame()
    }
    class FrameParser {
        +push(chunk)
    }
    class HalfBlockRenderer {
        +render(frame, cols, rows) string[]
    }
    class NativeSidecar {
        +run(config)
        +handleCommand(command)
    }
    class FrameRateGate {
        +new(sourceFps, targetFps)
        +shouldEmit() bool
    }
    class LatestFrameWriter {
        +sendFrame(frame)
        +finishEnd(reason)
    }
    class MpvEventThread {
        +waitEvent()
        +readProperties()
    }
    class MpvPlayer {
        +initialize()
        +renderRgb0(surface)
        +shutdown()
    }
    VideoTuiExtension --> VideoTuiConfig
    VideoTuiExtension --> VideoWidget
    VideoWidget --> NativeClient
    VideoWidget --> HalfBlockRenderer
    NativeClient --> FrameParser
    NativeClient --> NativeSidecar: stdin/stdout IPC
    NativeSidecar --> FrameRateGate
    NativeSidecar --> LatestFrameWriter
    NativeSidecar --> MpvEventThread
    NativeSidecar --> MpvPlayer
```

## mpv 尺寸依据

mpv 的原生窗口先取视频 display size，乘以 `window-scale`，再应用 `autofit` 限制（[`win_state.c`](https://github.com/mpv-player/mpv/blob/94335ab87ab225ca3e36e0faeac831639d3e1d4e/video/out/win_state.c#L88-L128)）。`autofit` 在目标 box 与视频宽高比不一致时缩小一边，不改变宽高比（[`win_state.c`](https://github.com/mpv-player/mpv/blob/94335ab87ab225ca3e36e0faeac831639d3e1d4e/video/out/win_state.c#L38-L65)）。在给定 fullscreen/window surface 后，`keepaspect` 根据 surface 和源视频计算目标矩形及边距（[`aspect.c`](https://github.com/mpv-player/mpv/blob/94335ab87ab225ca3e36e0faeac831639d3e1d4e/video/out/aspect.c#L130-L186)）。

libmpv Software Render API 不创建 OS 窗口。Pi Widget 从终端高度中扣除固定 UI 和 estimated editor rows，得到 layout-available rows，再应用 `maxHeightPercent` 响应式 cap；native Module 使用稳定后的 `video-out-params/dw/dh` 在 box 内最大内接并创建输出 surface。这对应 mpv 的 autofit 规则；`maxWidth`/`maxRows` 是独立 box 上限。

## 固定设计决策

1. mpv 是唯一的播放时钟和音频输出者；TypeScript 的刷新节流不控制媒体时间。
2. sidecar 按 libmpv 源 FPS 和配置目标 FPS 维护帧预算；每个更新执行 emit 或 skip，不排队补发。
3. IPC 的 stdout 只承载带长度前缀的 `PIV1` 二进制消息；日志只写 stderr。
4. framebuffer 使用 `rgb0`，每行 stride 向 64 字节对齐；surface 高度为偶数，以便一行终端 cell 表示两个像素行。
5. Widget 只计算由 `maxHeightPercent`、`maxRows` 和 `maxWidth` 限制的最大 render box；native Module 解释 libmpv display size、执行 autofit，并通过 READY/FRAME geometry 返回结果。
6. libmpv 初始 paused；event thread 在同一个 handle 内实体化可观察到的 container rotation filter，并以 rotation `COMMAND_REPLY` 作为完成 barrier；authoritative geometry 建立前不消费 render update；surface 就绪后通过 render-thread-safe `mpv_command_async` unpause，第一个 `PRESENT` 发布一次，后续 redraw/repeat 不作为媒体帧发布。render loop 不等待普通 libmpv 调用；`keepaspect=yes` 继续作为整数取整保护。
7. 只有 `session_start.reason === "startup"` 才自动播放；挂载后 handler 立即返回，`reload/new/resume/fork` 不重复播放。
8. Widget 使用 `placement: "aboveEditor"`，顺序保持为 `conversation -> video -> editor -> footer`；配置较低 `maxHeightPercent` 时未分配给视频的区域供 conversation 使用。普通输入留给 Pi，只有 Escape/Ctrl+C 被 listener 消费。
9. Ghostty 播放期间移除 DEC 2026 sync 标记和全宽视频行前的冗余 `CSI 2K`，结束后恢复原 writer。
10. 播放结束后先停止 event thread，再释放 render context、销毁 mpv handle；output thread 最后刷出 END 并退出。
