···11"use client";
2233+import type { ElementRef } from "react";
44+35import * as stylex from "@stylexjs/stylex";
46import {
57 MediaControlBar,
···79 MediaFullscreenButton,
810 MediaMuteButton,
911 MediaPlayButton,
1212+ MediaPlaybackRateButton,
1013 MediaSeekBackwardButton,
1114 MediaSeekForwardButton,
1215 MediaTimeDisplay,
1316 MediaTimeRange,
1417 MediaVolumeRange,
1518} from "media-chrome/react";
1919+import {
2020+ MediaCaptionsMenu,
2121+ MediaCaptionsMenuButton,
2222+} from "media-chrome/react/menu";
2323+import { useRef } from "react";
16241725import type { StyleXComponentProps } from "../theme/types";
1826···2634} from "../theme/semantic-spacing.stylex";
27352836const DEFAULT_SEEK_OFFSET = 10;
3737+type VideoTrackKind =
3838+ | "captions"
3939+ | "chapters"
4040+ | "descriptions"
4141+ | "metadata"
4242+ | "subtitles";
29433044const styles = stylex.create({
3145 root: {
···7589 height: "100%",
7690 width: "100%",
7791 },
9292+ aspectRatio: (aspectRatio: number) => ({
9393+ aspectRatio,
9494+ }),
7895});
79968097/**
9898+ * A subtitle or caption track rendered as a native `<track>` element.
9999+ */
100100+export interface VideoSubtitleTrack {
101101+ /**
102102+ * Whether this track should be enabled by default.
103103+ */
104104+ default?: boolean;
105105+ /**
106106+ * Optional stable identifier for React keys.
107107+ */
108108+ id?: string;
109109+ /**
110110+ * The text track kind.
111111+ * @default "subtitles"
112112+ */
113113+ kind?: VideoTrackKind;
114114+ /**
115115+ * The user-facing label shown in the captions menu.
116116+ */
117117+ label: string;
118118+ /**
119119+ * The WebVTT file URL for this track.
120120+ */
121121+ src: string;
122122+ /**
123123+ * The BCP-47 language tag for this track.
124124+ */
125125+ srcLang: string;
126126+}
127127+128128+/**
129129+ * An audio track shown in the audio track menu.
130130+ */
131131+export interface VideoAudioTrack {
132132+ /**
133133+ * Whether this track should be selected by default.
134134+ */
135135+ enabled?: boolean;
136136+ /**
137137+ * Optional stable identifier for the track.
138138+ */
139139+ id?: string;
140140+ /**
141141+ * The media track kind.
142142+ * @default "metadata"
143143+ */
144144+ kind?: VideoTrackKind;
145145+ /**
146146+ * The user-facing label shown in the audio track menu.
147147+ */
148148+ label: string;
149149+ /**
150150+ * The language code associated with this track.
151151+ */
152152+ language?: string;
153153+ /**
154154+ * An optional source URL to swap in when this track is selected.
155155+ */
156156+ src?: string;
157157+}
158158+159159+/**
81160 * Props for the Video component.
82161 */
83162export interface VideoProps extends StyleXComponentProps<
···104183 * When omitted, the component renders a default control bar.
105184 */
106185 children?: React.ReactNode;
186186+ /**
187187+ * Subtitle and caption tracks rendered inside the media element.
188188+ */
189189+ subtitleTracks?: Array<VideoSubtitleTrack>;
107190}
108191109192export function Video({
···112195 rounded = true,
113196 seekOffset = DEFAULT_SEEK_OFFSET,
114197 style,
198198+ aspectRatio = 16 / 9,
199199+ subtitleTracks = [],
115200 ...props
116201}: VideoProps) {
202202+ const { src, ...videoProps } = props;
203203+ const controllerRef = useRef<ElementRef<typeof MediaController> | null>(null);
204204+ const videoRef = useRef<HTMLVideoElement | null>(null);
205205+ const hasSubtitleTracks = subtitleTracks.length > 0;
206206+117207 return (
118208 <div
119209 {...stylex.props(styles.root, rounded && styles.rounded, ui.bgDim, style)}
120210 >
121211 <MediaController
212212+ ref={controllerRef}
122213 {...stylex.props(styles.controller, styles.controllerTheme)}
123214 >
124124- {/* Caption tracks are app-specific, so the wrapper forwards native video props instead of forcing a track API. */}
125215 {/* oxlint-disable-next-line jsx_a11y/media-has-caption */}
126216 <video
127127- {...props}
217217+ {...videoProps}
218218+ ref={videoRef}
128219 preload={preload}
129220 slot="media"
130130- {...stylex.props(styles.media)}
131131- />
221221+ src={src}
222222+ {...stylex.props(styles.media, styles.aspectRatio(aspectRatio))}
223223+ >
224224+ {subtitleTracks.map((track) => (
225225+ <track
226226+ key={track.id ?? track.src}
227227+ default={track.default}
228228+ kind={track.kind ?? "subtitles"}
229229+ label={track.label}
230230+ src={track.src}
231231+ srcLang={track.srcLang}
232232+ />
233233+ ))}
234234+ </video>
235235+ {hasSubtitleTracks ? <MediaCaptionsMenu anchor="auto" hidden /> : null}
132236 {children ?? (
133237 <MediaControlBar>
134238 <MediaPlayButton />
···138242 <MediaTimeDisplay showDuration />
139243 <MediaMuteButton />
140244 <MediaVolumeRange />
245245+ {hasSubtitleTracks ? <MediaCaptionsMenuButton /> : null}
246246+ <MediaPlaybackRateButton />
141247 <MediaFullscreenButton />
142248 </MediaControlBar>
143249 )}
+8
apps/docs/src/docs/components/content/video.mdx
···66import { PropDocs } from "../../../lib/PropDocs";
77import { Example } from "../../../lib/Example";
88import { Basic } from "../../../examples/video/basic";
99+import { Tracks } from "../../../examples/video/tracks";
9101011<Example src={Basic} />
1112···32333334Pass custom Media Chrome children when you need a different control layout.
3435If you do not pass children, the default control bar is rendered automatically.
3636+3737+### Tracks
3838+3939+Pass `subtitleTracks` to render native text tracks and enable the captions menu.
4040+Pass `audioTracks` to show the audio track menu and expose alternate selections in the default controls.
4141+4242+<Example src={Tracks} />
35433644## Props
3745