diff options
Diffstat (limited to 'apps/web/src/services/thumbnail-cache/tiling.ts')
| -rw-r--r-- | apps/web/src/services/thumbnail-cache/tiling.ts | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/apps/web/src/services/thumbnail-cache/tiling.ts b/apps/web/src/services/thumbnail-cache/tiling.ts new file mode 100644 index 00000000..bc8d31e5 --- /dev/null +++ b/apps/web/src/services/thumbnail-cache/tiling.ts @@ -0,0 +1,121 @@ +export const MEDIA_TILE_ASPECT_RATIO = 16 / 9; +export const FILMSTRIP_MIN_INTERVAL_SECONDS = 0.1; +export const FILMSTRIP_MAX_INTERVAL_SECONDS = 64; +export const FILMSTRIP_OVERSCAN_SLOTS = 2; + +const SOURCE_TIME_END_PADDING_SECONDS = 0.05; +const TILE_TIME_QUANTUM_PER_SECOND = 4096; + +export function getFilmstripInterval({ + pixelsPerSecond, + minTileWidthPx, +}: { + pixelsPerSecond: number; + minTileWidthPx: number; +}): number { + if ( + !Number.isFinite(pixelsPerSecond) || + pixelsPerSecond <= 0 || + !Number.isFinite(minTileWidthPx) || + minTileWidthPx <= 0 + ) { + return FILMSTRIP_MAX_INTERVAL_SECONDS; + } + + const ideal = minTileWidthPx / pixelsPerSecond; + const clamped = Math.min( + FILMSTRIP_MAX_INTERVAL_SECONDS, + Math.max(FILMSTRIP_MIN_INTERVAL_SECONDS, ideal), + ); + return 2 ** Math.round(Math.log2(clamped)); +} + +export function getFilmstripSlotCount({ + clipDurationSec, + interval, +}: { + clipDurationSec: number; + interval: number; +}): number { + if ( + !Number.isFinite(clipDurationSec) || + clipDurationSec <= 0 || + interval <= 0 + ) { + return 0; + } + return Math.max(1, Math.ceil(clipDurationSec / interval - 1e-6)); +} + +export function getVisibleSlotRange({ + slotCount, + tileWidthPx, + visibleLeftPx, + visibleRightPx, + overscanSlots = FILMSTRIP_OVERSCAN_SLOTS, +}: { + slotCount: number; + tileWidthPx: number; + visibleLeftPx: number; + visibleRightPx: number; + overscanSlots?: number; +}): { firstSlot: number; lastSlot: number } { + if (slotCount <= 0 || tileWidthPx <= 0 || visibleRightPx <= visibleLeftPx) { + return { firstSlot: 0, lastSlot: -1 }; + } + + const firstSlot = Math.max( + 0, + Math.floor(visibleLeftPx / tileWidthPx) - overscanSlots, + ); + const lastSlot = Math.min( + slotCount - 1, + Math.ceil(visibleRightPx / tileWidthPx) - 1 + overscanSlots, + ); + return { firstSlot, lastSlot }; +} + +export function getFilmstripSlotSourceTime({ + slot, + interval, + trimStartSec, + rate, + assetDurationSec, +}: { + slot: number; + interval: number; + trimStartSec: number; + rate: number; + assetDurationSec?: number; +}): number { + const safeRate = Number.isFinite(rate) && rate > 0 ? rate : 1; + const raw = Math.max(0, trimStartSec + slot * interval * safeRate); + if ( + assetDurationSec === undefined || + !Number.isFinite(assetDurationSec) || + assetDurationSec <= 0 + ) { + return raw; + } + return Math.min( + raw, + Math.max(0, assetDurationSec - SOURCE_TIME_END_PADDING_SECONDS), + ); +} + +export function buildThumbnailTileKey({ + mediaId, + time, + width, + height, +}: { + mediaId: string; + time: number; + width: number; + height: number; +}): string { + const quantizedTime = Math.round( + Math.max(0, time) * TILE_TIME_QUANTUM_PER_SECOND, + ); + return `${mediaId}:${width}x${height}:${quantizedTime}`; +} |
