{"version":3,"file":"use-video-player.cjs","names":[],"sources":["../src/use-video-player.ts"],"sourcesContent":["/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */\n\n\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type RefObject,\n} from \"react\";\nimport type { VideoMedia } from \"@langchain/langgraph-sdk/stream\";\nimport type { PlayerStatus } from \"./use-audio-player.js\";\n\n/**\n * Options for {@link useVideoPlayer}.\n */\nexport interface UseVideoPlayerOptions {\n  /**\n   * Start playback as soon as the blob URL resolves. Subject to\n   * browser autoplay policies — pair with `muted={true}` on the\n   * `<video>` element to bypass the user-gesture requirement.\n   */\n  autoPlay?: boolean;\n}\n\n/**\n * Controls + live state returned by {@link useVideoPlayer}. Mirrors\n * {@link AudioPlayerHandle} on the shared subset so callers only ever\n * learn one shape.\n */\nexport interface VideoPlayerHandle {\n  status: PlayerStatus;\n  play(): void;\n  pause(): void;\n  stop(): void;\n  toggle(): void;\n  reset(): void;\n  /**\n   * Resolve on the next terminal transition (`finished` / `paused` /\n   * `idle`). Reject on transitions to `\"error\"`. Triggers `play()`\n   * when called.\n   */\n  playToEnd(): Promise<void>;\n\n  currentTime: number;\n  /** Total duration (seconds) once the element has parsed the blob. */\n  duration?: number;\n  /** Seek to an absolute timestamp in seconds. */\n  seek(seconds: number): void;\n\n  error: Error | undefined;\n}\n\n/**\n * Bind a {@link VideoMedia} handle to a caller-owned `<video>` element.\n *\n * ### Contract\n *\n * - The caller renders `<video ref={videoRef}>` and styles it however\n *   they like; the hook never injects DOM nor overrides layout.\n * - On `message-finish`, the underlying blob URL is minted and assigned\n *   as `video.src`. Progressive playback of streamed container video\n *   (fragmented mp4 / webm via MSE) is out of scope for this version;\n *   `status` stays in `\"buffering\"` until the blob resolves.\n * - Element events (`play` / `pause` / `ended` / `timeupdate` /\n *   `loadedmetadata` / `error`) are translated into the shared\n *   {@link PlayerStatus} enum.\n * - On unmount, `media.revoke()` is called to free the object URL.\n *\n * @param videoRef - Ref to the `<video>` element the caller renders.\n * @param media    - Video handle from {@link useVideo}.\n * @param options  - Auto-play toggle.\n */\nexport function useVideoPlayer(\n  videoRef: RefObject<HTMLVideoElement | null>,\n  media: VideoMedia | undefined,\n  options?: UseVideoPlayerOptions\n): VideoPlayerHandle {\n  const autoPlay = options?.autoPlay ?? false;\n\n  const [status, setStatus] = useState<PlayerStatus>(\"idle\");\n  const [error, setError] = useState<Error | undefined>(undefined);\n  const [currentTime, setCurrentTime] = useState(0);\n  const [duration, setDuration] = useState<number | undefined>(undefined);\n\n  const statusRef = useRef<PlayerStatus>(\"idle\");\n  useEffect(() => {\n    statusRef.current = status;\n  }, [status]);\n\n  const shouldPlayRef = useRef(false);\n  const pendingResolveRef = useRef<(() => void) | null>(null);\n  const pendingRejectRef = useRef<((err: Error) => void) | null>(null);\n\n  const resolvePending = useCallback(() => {\n    const resolve = pendingResolveRef.current;\n    pendingResolveRef.current = null;\n    pendingRejectRef.current = null;\n    resolve?.();\n  }, []);\n\n  const rejectPending = useCallback((err: Error) => {\n    const reject = pendingRejectRef.current;\n    pendingResolveRef.current = null;\n    pendingRejectRef.current = null;\n    reject?.(err);\n  }, []);\n\n  useEffect(() => {\n    if (status === \"finished\" || status === \"paused\" || status === \"idle\") {\n      resolvePending();\n    } else if (status === \"error\") {\n      rejectPending(error ?? new Error(\"playback error\"));\n    }\n  }, [status, error, resolvePending, rejectPending]);\n\n  const play = useCallback(() => {\n    if (media == null) return;\n    if (statusRef.current === \"error\") return;\n    shouldPlayRef.current = true;\n    const video = videoRef.current;\n    if (video == null) {\n      setStatus(\"buffering\");\n      return;\n    }\n    video.play().catch((err) => {\n      setError(err as Error);\n      setStatus(\"error\");\n    });\n  }, [media, videoRef]);\n\n  const pause = useCallback(() => {\n    shouldPlayRef.current = false;\n    videoRef.current?.pause();\n    if (statusRef.current === \"playing\" || statusRef.current === \"buffering\") {\n      setStatus(\"paused\");\n    }\n  }, [videoRef]);\n\n  const stop = useCallback(() => {\n    shouldPlayRef.current = false;\n    const video = videoRef.current;\n    if (video != null) {\n      video.pause();\n      video.currentTime = 0;\n    }\n    setCurrentTime(0);\n    setStatus(media == null ? \"idle\" : \"paused\");\n  }, [videoRef, media]);\n\n  const reset = useCallback(() => {\n    stop();\n    setError(undefined);\n    setDuration(undefined);\n    setStatus(\"idle\");\n  }, [stop]);\n\n  const toggle = useCallback(() => {\n    if (statusRef.current === \"playing\") pause();\n    else play();\n  }, [play, pause]);\n\n  const playToEnd = useCallback((): Promise<void> => {\n    pendingResolveRef.current?.();\n    pendingResolveRef.current = null;\n    pendingRejectRef.current = null;\n\n    return new Promise<void>((resolve, reject) => {\n      pendingResolveRef.current = resolve;\n      pendingRejectRef.current = reject;\n      play();\n    });\n  }, [play]);\n\n  const seek = useCallback(\n    (seconds: number) => {\n      const video = videoRef.current;\n      if (video == null) return;\n      video.currentTime = seconds;\n      setCurrentTime(seconds);\n    },\n    [videoRef]\n  );\n\n  // Surface a media-level error immediately.\n  useEffect(() => {\n    if (media?.error == null) return;\n    setError(new Error(media.error.message));\n    setStatus(\"error\");\n  }, [media]);\n\n  // Bind the element to the blob URL once it resolves.\n  useEffect(() => {\n    if (media == null) {\n      setStatus(\"idle\");\n      setError(undefined);\n      setCurrentTime(0);\n      setDuration(undefined);\n      return undefined;\n    }\n\n    setError(undefined);\n    setStatus(\"buffering\");\n    setCurrentTime(0);\n    setDuration(undefined);\n\n    let cancelled = false;\n    const video = videoRef.current;\n\n    media.objectURL.then(\n      (resolved) => {\n        if (cancelled) return;\n        if (video == null) return;\n        video.src = resolved;\n\n        if (shouldPlayRef.current || autoPlay) {\n          video.play().catch((err) => {\n            setError(err as Error);\n            setStatus(\"error\");\n          });\n        } else {\n          setStatus(\"paused\");\n        }\n      },\n      () => {\n        if (!cancelled) {\n          setError(new Error(\"media failed to materialise\"));\n          setStatus(\"error\");\n        }\n      }\n    );\n\n    if (video == null) {\n      return () => {\n        cancelled = true;\n        try {\n          media.revoke();\n        } catch {\n          // best-effort\n        }\n      };\n    }\n\n    const onPlay = () => {\n      if (statusRef.current !== \"error\") setStatus(\"playing\");\n    };\n    const onPause = () => {\n      if (video.ended) return;\n      if (statusRef.current === \"playing\") setStatus(\"paused\");\n    };\n    const onEnded = () => {\n      setStatus(\"finished\");\n    };\n    const onTimeUpdate = () => {\n      setCurrentTime(video.currentTime);\n    };\n    const onLoadedMetadata = () => {\n      if (Number.isFinite(video.duration)) setDuration(video.duration);\n    };\n    const onError = () => {\n      setError(new Error(\"HTMLVideoElement error\"));\n      setStatus(\"error\");\n    };\n\n    video.addEventListener(\"play\", onPlay);\n    video.addEventListener(\"pause\", onPause);\n    video.addEventListener(\"ended\", onEnded);\n    video.addEventListener(\"timeupdate\", onTimeUpdate);\n    video.addEventListener(\"loadedmetadata\", onLoadedMetadata);\n    video.addEventListener(\"error\", onError);\n\n    return () => {\n      cancelled = true;\n      video.removeEventListener(\"play\", onPlay);\n      video.removeEventListener(\"pause\", onPause);\n      video.removeEventListener(\"ended\", onEnded);\n      video.removeEventListener(\"timeupdate\", onTimeUpdate);\n      video.removeEventListener(\"loadedmetadata\", onLoadedMetadata);\n      video.removeEventListener(\"error\", onError);\n      try {\n        video.pause();\n        video.removeAttribute(\"src\");\n        video.load();\n      } catch {\n        // best-effort\n      }\n      try {\n        media.revoke();\n      } catch {\n        // best-effort\n      }\n    };\n  }, [media, videoRef, autoPlay]);\n\n  return {\n    status,\n    play,\n    pause,\n    stop,\n    toggle,\n    reset,\n    playToEnd,\n    currentTime,\n    duration,\n    seek,\n    error,\n  };\n}\n"],"mappings":"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"}