# Arbitrary aspect ratio design

This change moves source-aspect ownership from the Pi Widget to the native playback Module. The Widget Interface supplies only a bounded terminal render box. The sidecar Adapter stabilizes observable container rotation through the same libmpv filter chain, reads libmpv's aspect-corrected post-filter display size, and returns fitted framebuffer geometry through the existing READY and FRAME messages.

## Architecture and calls: current

```mermaid
flowchart LR
    widget["VideoWidget"] --> layout["16:9 layout calculation"]
    layout --> client["NativeClient"]
    client -->|"exact width and height"| sidecar["Native sidecar"]
    sidecar --> surface["Rgb0Surface"]
    mpv["libmpv"] -->|"keepaspect render"| surface
    surface -->|"PIV1 FRAME"| client
```

The media-derived ratio is guessed before the libmpv Adapter is consulted.

## Architecture and calls: target

```mermaid
flowchart LR
    widget["VideoWidget"] --> bounds["Terminal render box"]
    bounds --> client["NativeClient"]
    client -->|"maximum width and height"| sidecar["Native playback Module"]
    mpv["libmpv Adapter"] -->|"dwidth and dheight"| fit["Native autofit"]
    sidecar --> fit
    bounds --> fit
    fit --> surface["Exact Rgb0Surface"]
    mpv -->|"Software Render API"| surface
    surface -->|"PIV1 READY and FRAME geometry"| client
    client --> widget
```

The existing process Seam stays in place. Media interpretation and autofit gain Locality inside the native playback Module.

## Sequence: current

```mermaid
sequenceDiagram
    participant Widget as VideoWidget
    participant Client as NativeClient
    participant Native as Native sidecar
    participant Mpv as libmpv
    Widget->>Widget: calculate 16:9 surface
    Widget->>Client: start exact surface
    Client->>Native: spawn width and height
    Native->>Native: allocate surface
    Native-->>Client: READY surface
    Native->>Mpv: load media
    Mpv-->>Native: FILE_LOADED
    loop Frames
        Mpv-->>Native: render update
        Native-->>Client: FRAME
        Client-->>Widget: request render
    end
```

## Sequence: target

```mermaid
sequenceDiagram
    participant Widget as VideoWidget
    participant Client as NativeClient
    participant Native as Native playback Module
    participant Events as mpv event thread
    participant Mpv as libmpv Adapter
    Widget->>Widget: calculate maximum render box
    Widget->>Client: start render box
    Client->>Native: spawn maximum width and height
    Native->>Mpv: load media paused
    Mpv-->>Events: FILE_LOADED and VIDEO_RECONFIG
    opt Observable container rotation
        Events->>Mpv: add rotation filter to same handle
        Mpv-->>Events: rotation COMMAND_REPLY barrier
        Mpv-->>Events: VIDEO_RECONFIG with post-filter geometry
    end
    Events-->>Native: stable dwidth and dheight
    Native->>Native: fit exact even-height surface
    Native->>Mpv: render-safe async unpause
    Mpv-->>Events: COMMAND_REPLY
    Mpv-->>Native: first render update
    Native->>Mpv: query NEXT_FRAME_INFO
    Native->>Native: render first PRESENT
    Native-->>Client: READY and first FRAME
    loop Subsequent updates
        Mpv-->>Native: render update
        Native->>Mpv: query NEXT_FRAME_INFO
        alt New media frame
            Native->>Native: apply frame-rate gate and render
            Native-->>Client: FRAME actual geometry
            Client-->>Widget: request render
        else Redraw or repeat
            Native->>Mpv: SKIP_RENDERING
        end
    end
    opt Terminal or editor resize
        Widget->>Client: resize render box
        Client->>Native: resize maximum width and height
        Native->>Native: refit exact surface
    end
```

No guessed surface is rendered before authoritative media geometry exists.

## State: current

```mermaid
stateDiagram-v2
    [*] --> BoundsKnown
    BoundsKnown --> SurfaceAllocated: calculate fixed 16:9
    SurfaceAllocated --> Loading: start libmpv
    Loading --> Playing: file loaded
    Playing --> SurfaceAllocated: terminal resize recalculates 16:9
    Playing --> Finished: EOF or stop
    Finished --> [*]
```

## State: target

```mermaid
stateDiagram-v2
    [*] --> BoundsKnown
    BoundsKnown --> Loading: start one libmpv instance
    Loading --> MediaKnown: paused FILE_LOADED
    MediaKnown --> GeometryStable: event thread applies rotation filter
    GeometryStable --> SurfaceFitted: native autofit and allocation
    SurfaceFitted --> Playing: async unpause then publish first PRESENT
    Playing --> SurfaceFitted: render box or display size changes
    Playing --> Finished: EOF or stop
    Finished --> [*]
```

`Rgb0Surface` does not exist until both the render box and display size are known.

## Local classes: current

```mermaid
classDiagram
    class VideoWidget {
        -lastRequestedSize
        +render(width)
        +computeVideoLayout(width, config, rows)
    }
    class VideoFrameSource {
        +start(width, height)
        +resize(width, height)
        +latestFrame()
        +stop()
    }
    class NativeClient {
        -requestedSize
        +start(width, height)
        +resize(width, height)
        +latestFrame()
    }
    class NativeSidecar {
        +allocateSurface(width, height)
        +renderFrame()
    }
    VideoWidget --> VideoFrameSource
    NativeClient ..|> VideoFrameSource
    NativeClient --> NativeSidecar
```

The Interface names exact surface dimensions even though the Widget does not own media geometry.

## Local classes: target

```mermaid
classDiagram
    class VideoWidget {
        -lastRequestedBox
        +render(width)
        +computeRenderBox(width, config, rows)
    }
    class VideoFrameSource {
        +start(maxWidth, maxHeight)
        +resize(maxWidth, maxHeight)
        +latestFrame()
        +stop()
    }
    class NativeClient {
        -requestedBox
        +start(maxWidth, maxHeight)
        +resize(maxWidth, maxHeight)
        +latestFrame()
    }
    class MpvPlayer {
        +startPlayback()
        +renderFrame(surface)
    }
    class MpvEventThread {
        +stabilizeRotation()
        +readDisplaySize()
        +setPaused(paused)
    }
    class SurfaceSize {
        +fit(maxWidth, maxHeight, displayWidth, displayHeight)
    }
    class NativeSidecar {
        -renderBox
        -displaySize
        -surface
        +refitSurface()
        +renderFrame()
    }
    VideoWidget --> VideoFrameSource
    NativeClient ..|> VideoFrameSource
    NativeClient --> NativeSidecar
    NativeSidecar --> MpvPlayer
    MpvPlayer --> MpvEventThread
    NativeSidecar --> SurfaceSize
```

The Widget-facing Interface keeps the same four operations; only their dimension invariant becomes truthful: inputs are upper bounds, while READY and FRAME carry the selected surface.

## Decision

- `video-out-params/dw` and `video-out-params/dh` are authoritative only after `VIDEO_RECONFIG` has stabilized aspect correction and video filters. libmpv therefore owns SAR and crop semantics.
- mpv 0.41 Software Render cannot safely consume rotation metadata directly. The event-thread Adapter sets `video-rotate=no` and materializes observable container-level 90-degree rotations with a filter on the same libmpv handle. Rotation `COMMAND_REPLY` is the completion barrier before geometry can be published; 90/270-degree paths additionally require swapped post-filter dimensions.
- Frame-only H.264 display-orientation SEI can disappear before mpv 0.41 exposes stable event-thread properties. The Module deliberately has no ffprobe fallback or second parser; this is a documented backend compatibility boundary, not another media-clock path.
- The native playback Module fits that ratio into `maxWidth x maxHeight` and enforces an even framebuffer height for half-block rendering.
- READY is queued after the first successful render and before its FRAME, so its geometry cannot describe an earlier reconfiguration.
- FRAME remains self-describing. No `VIDEO_INFO`, aspect option, ffprobe process, pixel inference, or second media handle is introduced.
- TypeScript applies terminal/editor availability plus the configured percentage and absolute caps only. It never interprets media metadata.
- libmpv remains paused until stable geometry and the first native surface exist. Render updates stay queued until authoritative geometry exists; the render loop then unpauses and publishes the first `PRESENT` once. After READY, `NEXT_FRAME_INFO` excludes `REDRAW` and `REPEAT` from the media-frame budget. Load and unpause use render-thread-safe `mpv_command_async`; event-thread `COMMAND_REPLY` handling turns delayed failures into playback errors.
- Before the first frame the Widget occupies zero rows. On resize, NativeClient retains its cached frame and the Widget reuses it only while it still fits the current box.
- `keepaspect=yes` remains a final rounding guard.

## Dependency strategy

libmpv is a truly external dependency behind the existing `MpvPlayer` Adapter. PIV1 is an owned out-of-process Seam whose two sides ship together. The Widget Seam already has two Adapters: production `NativeClient` and the in-memory frame source used by tests. Surface fitting is in-process pure calculation and does not need another Adapter.
