import type { Component } from "@earendil-works/pi-tui"; import type { VideoTuiConfig } from "./config.ts"; import type { VideoFrame } from "./frame-buffer.ts"; import { renderHalfBlock } from "./half-block-renderer.ts"; import { shouldDismissVideo } from "./input-controller.ts"; const MAX_FRAMEBUFFER_WIDTH = 1024; const MAX_FRAMEBUFFER_ROWS = 512; export interface VideoFrameSource { latestFrame(): VideoFrame | undefined; start(maxWidth: number, maxHeight: number): void; resize(maxWidth: number, maxHeight: number): void; stop(): void; } export interface VideoRenderBox { columns: number; pixelHeight: number; } export class VideoWidget implements Component { private readonly source: VideoFrameSource; private readonly config: VideoTuiConfig; private readonly onClose: () => void; private readonly availableRows: (width: number) => number; private lastRequestedBox: string | undefined; private closed = false; constructor( source: VideoFrameSource, config: VideoTuiConfig, onClose: () => void, availableRows: (width: number) => number, ) { this.source = source; this.config = config; this.onClose = onClose; this.availableRows = availableRows; } render(width: number): string[] { if (this.closed) return []; const box = computeVideoRenderBox(width, this.config, this.availableRows(width)); const boxKey = `${box.columns}x${box.pixelHeight}`; if (this.lastRequestedBox === undefined) { this.lastRequestedBox = boxKey; this.source.start(box.columns, box.pixelHeight); } else if (boxKey !== this.lastRequestedBox) { this.lastRequestedBox = boxKey; this.source.resize(box.columns, box.pixelHeight); } const frame = this.source.latestFrame(); if (frame && frame.width <= box.columns && frame.height <= box.pixelHeight) { const rows = Math.max(1, Math.ceil(frame.height / 2)); return centerLines( renderHalfBlock(frame, frame.width, rows), width, frame.width, ); } return []; } handleInput(data: string): void { if (!this.closed && shouldDismissVideo(data)) { this.source.stop(); this.dispose(); this.onClose(); } } invalidate(): void {} dispose(): void { this.closed = true; } } export function computeVideoRenderBox( width: number, config: VideoTuiConfig, availableRows: number, ): VideoRenderBox { const availableColumns = Math.max(1, Math.floor(width)); const availableTerminalRows = Math.max(1, Math.floor(availableRows)); const maxColumns = Math.min( availableColumns, config.maxWidth ?? availableColumns, MAX_FRAMEBUFFER_WIDTH, ); const maxRows = Math.min( availableTerminalRows, config.maxRows ?? availableTerminalRows, MAX_FRAMEBUFFER_ROWS, ); return { columns: maxColumns, pixelHeight: maxRows * 2 }; } function centerLines(lines: string[], width: number, contentWidth: number): string[] { const targetWidth = Math.max(contentWidth, Math.floor(width)); const left = Math.floor((targetWidth - contentWidth) / 2); const right = targetWidth - contentWidth - left; const leftPadding = " ".repeat(left); const rightPadding = " ".repeat(right); return lines.map((line) => leftPadding + line + rightPadding); }