AstralView/packages/sdk/src/core/widgets/video.ts

132 lines
3.8 KiB
TypeScript

import type { VideoWidgetNode } from '../schema';
import { pickUrlLike } from './urlLike';
/**
* goView video option shape varies across versions.
* Keep it permissive and normalize the common fields.
*/
export interface GoViewVideoOption {
dataset?: unknown;
src?: unknown;
url?: unknown;
loop?: boolean;
muted?: boolean;
autoplay?: boolean;
autoPlay?: boolean;
fit?: unknown;
objectFit?: unknown;
borderRadius?: number;
}
/**
* Back-compat alias (older code used "LegacyVideoOption").
*/
export type LegacyVideoOption = GoViewVideoOption;
function pickSrc(option: GoViewVideoOption): string {
// Prefer the whole option first (covers videoUrl/mp4/m3u8/flv/etc directly on the object).
return pickUrlLike(option) || pickUrlLike(option.dataset) || pickUrlLike(option.src) || pickUrlLike(option.url);
}
function asString(v: unknown): string {
if (typeof v === 'string') return v;
if (!v) return '';
return '';
}
function toMaybeBoolean(v: unknown): boolean | undefined {
if (typeof v === 'boolean') return v;
if (typeof v === 'number') return v !== 0;
if (typeof v === 'string') {
const s = v.trim().toLowerCase();
if (s === 'true' || s === '1') return true;
if (s === 'false' || s === '0') return false;
}
return undefined;
}
function toMaybeNumber(v: unknown): number | undefined {
if (typeof v === 'number' && Number.isFinite(v)) return v;
if (typeof v === 'string') {
const n = Number(v);
if (Number.isFinite(n)) return n;
}
return undefined;
}
function pickFromNested<T>(input: unknown, picker: (obj: Record<string, unknown>) => T | undefined, depth: number): T | undefined {
if (!input || typeof input !== 'object') return undefined;
const obj = input as Record<string, unknown>;
const direct = picker(obj);
if (direct !== undefined) return direct;
if (depth <= 0) return undefined;
for (const key of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) {
const nested = pickFromNested(obj[key], picker, depth - 1);
if (nested !== undefined) return nested;
}
return undefined;
}
function pickBorderRadius(option: GoViewVideoOption): number | undefined {
const direct = toMaybeNumber(option.borderRadius);
if (direct !== undefined) return direct;
return pickFromNested(option, (obj) => toMaybeNumber(obj.borderRadius ?? obj.radius ?? obj.r), 2);
}
function pickBooleanLike(option: GoViewVideoOption, keys: string[]): boolean | undefined {
return pickFromNested(
option,
(obj) => {
for (const key of keys) {
const v = toMaybeBoolean(obj[key]);
if (v !== undefined) return v;
}
return undefined;
},
2,
);
}
function pickFitFromNested(option: GoViewVideoOption): string {
const direct = asString(option.fit) || asString(option.objectFit);
if (direct) return direct;
const nested = pickFromNested(option, (obj) => asString(obj.fit) || asString(obj.objectFit), 2);
return nested ?? '';
}
function pickFit(option: GoViewVideoOption): VideoWidgetNode['props']['fit'] | undefined {
const raw = pickFitFromNested(option);
if (!raw) return undefined;
// normalize common variants
const v = raw.toLowerCase();
if (v === 'contain' || v === 'cover' || v === 'fill' || v === 'none' || v === 'scale-down') {
return v as VideoWidgetNode['props']['fit'];
}
return undefined;
}
export function convertGoViewVideoOptionToNodeProps(option: GoViewVideoOption): VideoWidgetNode['props'] {
return {
src: pickSrc(option),
loop: pickBooleanLike(option, ['loop', 'isLoop']),
muted: pickBooleanLike(option, ['muted', 'isMuted', 'mute']),
fit: pickFit(option),
borderRadius: pickBorderRadius(option),
};
}
/**
* Back-compat export.
*/
export const convertLegacyVideoOptionToNodeProps = convertGoViewVideoOptionToNodeProps;