| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | | import { RotateCwIcon, TriangleAlertIcon } from "lucide-react"; |
| 1 | import { PlayIcon, RotateCwIcon, TriangleAlertIcon } from "lucide-react"; |
| 2 | 2 | import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react"; |
| 3 | 3 | |
| 4 | 4 | import { cn } from "../../lib/utils"; |
| ... | ... | @@ -15,6 +15,8 @@ interface MediaVideoPlayerProps { |
| 15 | 15 | readonly revision?: string | null | undefined; |
| 16 | 16 | readonly preload?: "visible" | "metadata" | undefined; |
| 17 | 17 | readonly autoPlay?: boolean | undefined; |
| 18 | /** Presents a still thumbnail whose full surface opens the video in a viewer. */ |
| 19 | readonly onOpen?: (() => void) | undefined; |
| 18 | 20 | readonly className?: string | undefined; |
| 19 | 21 | readonly videoClassName?: string | undefined; |
| 20 | 22 | /** Styles the loading and failure panels, which otherwise assume an inline light surface. */ |
| ... | ... | @@ -34,6 +36,7 @@ export function MediaVideoPlayer({ |
| 34 | 36 | revision = null, |
| 35 | 37 | preload = "visible", |
| 36 | 38 | autoPlay = false, |
| 39 | onOpen, |
| 37 | 40 | className, |
| 38 | 41 | videoClassName, |
| 39 | 42 | stateClassName, |
| ... | ... | @@ -131,7 +134,7 @@ export function MediaVideoPlayer({ |
| 131 | 134 | style={style} |
| 132 | 135 | data-markdown-copy={copyMarkdown} |
| 133 | 136 | > |
| 134 | | {failed ? ( |
| 137 | {failed && !onOpen ? ( |
| 135 | 138 | <span |
| 136 | 139 | role="alert" |
| 137 | 140 | className={cn( |
| ... | ... | @@ -160,17 +163,23 @@ export function MediaVideoPlayer({ |
| 160 | 163 | <OpenMediaLink originalUrl={originalUrl} src={latestSrc ?? src} fileName={label} /> |
| 161 | 164 | </span> |
| 162 | 165 | </span> |
| 163 | | ) : src !== null ? ( |
| 166 | ) : src !== null && !failed ? ( |
| 164 | 167 | <video |
| 165 | 168 | key={loadAttempt} |
| 166 | 169 | ref={videoRef} |
| 167 | 170 | src={src} |
| 168 | 171 | aria-label={label || "Video preview"} |
| 169 | | autoPlay={autoPlay} |
| 170 | | controls |
| 172 | aria-hidden={onOpen ? true : undefined} |
| 173 | autoPlay={onOpen ? false : autoPlay} |
| 174 | controls={!onOpen} |
| 175 | muted={onOpen ? true : undefined} |
| 171 | 176 | playsInline |
| 172 | 177 | preload={preload === "metadata" || preloadedSrc === src ? "metadata" : "none"} |
| 173 | | className={cn("aspect-video max-h-full w-full bg-black object-contain", videoClassName)} |
| 178 | className={cn( |
| 179 | "aspect-video max-h-full w-full bg-black object-contain", |
| 180 | onOpen && "pointer-events-none", |
| 181 | videoClassName, |
| 182 | )} |
| 174 | 183 | style={style} |
| 175 | 184 | onLoadedMetadata={(event) => prepareVideoFirstFrame(event.currentTarget)} |
| 176 | 185 | onPlay={() => setPlaybackSource({ src, revision: sourceRevision })} |
| ... | ... | @@ -184,11 +193,23 @@ export function MediaVideoPlayer({ |
| 184 | 193 | ) : ( |
| 185 | 194 | <span |
| 186 | 195 | role="status" |
| 187 | | aria-label="Loading video" |
| 196 | aria-label={failed ? "Video preview unavailable" : "Loading video"} |
| 188 | 197 | className={cn("block aspect-video w-full rounded-lg bg-muted/60", stateClassName)} |
| 189 | 198 | style={style} |
| 190 | 199 | /> |
| 191 | 200 | )} |
| 201 | {onOpen ? ( |
| 202 | <button |
| 203 | type="button" |
| 204 | aria-label={label ? `Play ${label}` : "Play video"} |
| 205 | onClick={onOpen} |
| 206 | className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" |
| 207 | > |
| 208 | <span className="flex size-8 items-center justify-center rounded-full bg-black/50 text-white"> |
| 209 | <PlayIcon aria-hidden className="size-4 fill-current" /> |
| 210 | </span> |
| 211 | </button> |
| 212 | ) : null} |
| 192 | 213 | </span> |
| 193 | 214 | ); |
| 194 | 215 | return actionsSource ? <MediaActions source={actionsSource}>{player}</MediaActions> : player; |