'use client'; import { useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react'; import { useAnimate } from 'motion/react'; import { SlideCanvas, type SlideEffects } from '@openmaic/renderer'; import type { PPTImageElement, PPTVideoElement } from '@openmaic/dsl'; import { Film, ImageOff, Paintbrush, RotateCcw, ShieldAlert, VideoOff } from 'lucide-react'; import { useCanvasStore } from '@/lib/store'; import { useSceneData, useSceneSelector } from '@/lib/contexts/scene-context'; import type { SlideContent } from '@/lib/types/stage'; import { useResolvedSlideMedia, type ResolvedSlideMediaEntry } from '../use-resolved-slide'; import { retryMediaTask } from '@/lib/media/media-orchestrator'; import { useI18n } from '@/lib/hooks/use-i18n'; import { createLogger } from '@/lib/logger'; import { mediaResolutionCanRetry, type MediaResolution } from '@/lib/media/resolve-media-ref'; const log = createLogger('RendererScreenCanvas'); function PlaybackVideoContent({ element, media, sceneId, slideId, }: { readonly element: PPTVideoElement; readonly media: ResolvedSlideMediaEntry | undefined; readonly sceneId: string; readonly slideId: string; }) { const { t } = useI18n(); const videoRef = useRef(null); const playingVideoElementId = useCanvasStore.use.playingVideoElementId(); const prevPlayingRef = useRef(''); const [scope, animate] = useAnimate(); const task = media?.task; const mediaRef = media?.ref; const resolvedSrc = element.src || undefined; const resolvedPoster = element.poster || undefined; const showSkeleton = media?.resolution.kind === 'pending' || media?.resolution.kind === 'placeholder'; const showDisabled = media?.resolution.kind === 'disabled'; const showError = media?.resolution.kind === 'failed'; const canRetry = mediaResolutionCanRetry(media?.resolution); useEffect(() => { videoRef.current?.pause(); }, []); useEffect(() => { const video = videoRef.current; if (!video) return; const isMe = playingVideoElementId === element.id; const wasMe = prevPlayingRef.current === element.id; prevPlayingRef.current = playingVideoElementId; if (isMe && !wasMe) { animate( scope.current, { scale: [1, 1.035, 1] }, { duration: 0.6, ease: [0.25, 0.1, 0.25, 1], times: [0, 0.35, 1] }, ); video.play().catch((err) => { log.warn('[PlaybackVideoContent] play() failed:', err); }); } else if (!isMe && wasMe) { video.pause(); } }, [playingVideoElementId, element.id, animate, scope]); const handleEnded = () => { if (useCanvasStore.getState().playingVideoElementId === element.id) { useCanvasStore.getState().pauseVideo(); } }; if (showSkeleton) { return (
); } if (showDisabled) { return (
{t('settings.mediaGenerationDisabled')}
); } if (showError && media?.resolution.kind === 'failed') { return (
{task?.errorCode === 'CONTENT_SENSITIVE' ? (
{t('settings.mediaContentSensitive')}
) : canRetry ? ( ) : null}
); } if (resolvedSrc) { return (
); } return (
); } export type PlaybackImageState = 'ready' | 'pending' | 'failed' | 'disabled'; export function getPlaybackImageState(resolution: MediaResolution): PlaybackImageState { if (resolution.kind === 'failed') return 'failed'; if (resolution.kind === 'disabled') return 'disabled'; if (resolution.kind === 'pending' || resolution.kind === 'placeholder') return 'pending'; return 'ready'; } export function PlaybackImageContent({ element, defaultContent, media, sceneId, slideId, }: { readonly element: PPTImageElement; readonly defaultContent: ReactNode; readonly media: ResolvedSlideMediaEntry | undefined; readonly sceneId: string; readonly slideId: string; }) { const { t } = useI18n(); const task = media?.task; const state = getPlaybackImageState(media?.resolution ?? { kind: 'placeholder' }); const canRetry = mediaResolutionCanRetry(media?.resolution); if (state === 'pending') { return (
); } if (state === 'disabled') { return (
{t('settings.mediaGenerationDisabled')}
); } if (state === 'failed' && media?.resolution.kind === 'failed') { return (
{task?.errorCode === 'CONTENT_SENSITIVE' ? (
{t('settings.mediaContentSensitive')}
) : canRetry ? ( ) : null}
); } return ( <> {defaultContent} {canRetry ? ( ) : null} ); } export function RendererScreenCanvas() { const { sceneId } = useSceneData(); const slide = useSceneSelector((content) => content.canvas); const resolved = useResolvedSlideMedia(slide); const canvasPercentage = useCanvasStore.use.canvasPercentage(); const setCanvasScale = useCanvasStore.use.setCanvasScale(); const highlightedElementIds = useCanvasStore.use.highlightedElementIds(); const highlightOptions = useCanvasStore.use.highlightOptions(); const spotlightElementId = useCanvasStore.use.spotlightElementId(); const spotlightOptions = useCanvasStore.use.spotlightOptions(); const laserElementId = useCanvasStore.use.laserElementId(); const laserOptions = useCanvasStore.use.laserOptions(); const zoomTarget = useCanvasStore.use.zoomTarget(); const handleScaleChange = useCallback( (scale: number) => { setCanvasScale(scale); }, [setCanvasScale], ); const effects = useMemo(() => { const next: SlideEffects = {}; if (highlightOptions && highlightedElementIds.length) { next.highlights = highlightedElementIds.map((elementId) => ({ elementId, ...highlightOptions, })); } if (spotlightElementId && spotlightOptions) { next.spotlight = { elementId: spotlightElementId, dimness: spotlightOptions.dimness, }; } if (laserElementId && laserOptions) { next.laser = { elementId: laserElementId, color: laserOptions.color, duration: laserOptions.duration, }; } if (zoomTarget) { next.zoom = zoomTarget; } return next; }, [ highlightOptions, highlightedElementIds, laserElementId, laserOptions, spotlightElementId, spotlightOptions, zoomTarget, ]); return ( ( )} renderVideo={(element) => ( )} videoInteractive chrome /> ); }