A design system in a box. hip-ui.tngl.io/docs/introduction
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

caption support

+309 -8
+110 -4
apps/docs/src/components/video/index.tsx
··· 1 1 "use client"; 2 2 3 + import type { ElementRef } from "react"; 4 + 3 5 import * as stylex from "@stylexjs/stylex"; 4 6 import { 5 7 MediaControlBar, ··· 7 9 MediaFullscreenButton, 8 10 MediaMuteButton, 9 11 MediaPlayButton, 12 + MediaPlaybackRateButton, 10 13 MediaSeekBackwardButton, 11 14 MediaSeekForwardButton, 12 15 MediaTimeDisplay, 13 16 MediaTimeRange, 14 17 MediaVolumeRange, 15 18 } from "media-chrome/react"; 19 + import { 20 + MediaCaptionsMenu, 21 + MediaCaptionsMenuButton, 22 + } from "media-chrome/react/menu"; 23 + import { useRef } from "react"; 16 24 17 25 import type { StyleXComponentProps } from "../theme/types"; 18 26 ··· 26 34 } from "../theme/semantic-spacing.stylex"; 27 35 28 36 const DEFAULT_SEEK_OFFSET = 10; 37 + type VideoTrackKind = 38 + | "captions" 39 + | "chapters" 40 + | "descriptions" 41 + | "metadata" 42 + | "subtitles"; 29 43 30 44 const styles = stylex.create({ 31 45 root: { ··· 75 89 height: "100%", 76 90 width: "100%", 77 91 }, 92 + aspectRatio: (aspectRatio: number) => ({ 93 + aspectRatio, 94 + }), 78 95 }); 79 96 80 97 /** 98 + * A subtitle or caption track rendered as a native `<track>` element. 99 + */ 100 + export interface VideoSubtitleTrack { 101 + /** 102 + * Whether this track should be enabled by default. 103 + */ 104 + default?: boolean; 105 + /** 106 + * Optional stable identifier for React keys. 107 + */ 108 + id?: string; 109 + /** 110 + * The text track kind. 111 + * @default "subtitles" 112 + */ 113 + kind?: VideoTrackKind; 114 + /** 115 + * The user-facing label shown in the captions menu. 116 + */ 117 + label: string; 118 + /** 119 + * The WebVTT file URL for this track. 120 + */ 121 + src: string; 122 + /** 123 + * The BCP-47 language tag for this track. 124 + */ 125 + srcLang: string; 126 + } 127 + 128 + /** 129 + * An audio track shown in the audio track menu. 130 + */ 131 + export interface VideoAudioTrack { 132 + /** 133 + * Whether this track should be selected by default. 134 + */ 135 + enabled?: boolean; 136 + /** 137 + * Optional stable identifier for the track. 138 + */ 139 + id?: string; 140 + /** 141 + * The media track kind. 142 + * @default "metadata" 143 + */ 144 + kind?: VideoTrackKind; 145 + /** 146 + * The user-facing label shown in the audio track menu. 147 + */ 148 + label: string; 149 + /** 150 + * The language code associated with this track. 151 + */ 152 + language?: string; 153 + /** 154 + * An optional source URL to swap in when this track is selected. 155 + */ 156 + src?: string; 157 + } 158 + 159 + /** 81 160 * Props for the Video component. 82 161 */ 83 162 export interface VideoProps extends StyleXComponentProps< ··· 104 183 * When omitted, the component renders a default control bar. 105 184 */ 106 185 children?: React.ReactNode; 186 + /** 187 + * Subtitle and caption tracks rendered inside the media element. 188 + */ 189 + subtitleTracks?: Array<VideoSubtitleTrack>; 107 190 } 108 191 109 192 export function Video({ ··· 112 195 rounded = true, 113 196 seekOffset = DEFAULT_SEEK_OFFSET, 114 197 style, 198 + aspectRatio = 16 / 9, 199 + subtitleTracks = [], 115 200 ...props 116 201 }: VideoProps) { 202 + const { src, ...videoProps } = props; 203 + const controllerRef = useRef<ElementRef<typeof MediaController> | null>(null); 204 + const videoRef = useRef<HTMLVideoElement | null>(null); 205 + const hasSubtitleTracks = subtitleTracks.length > 0; 206 + 117 207 return ( 118 208 <div 119 209 {...stylex.props(styles.root, rounded && styles.rounded, ui.bgDim, style)} 120 210 > 121 211 <MediaController 212 + ref={controllerRef} 122 213 {...stylex.props(styles.controller, styles.controllerTheme)} 123 214 > 124 - {/* Caption tracks are app-specific, so the wrapper forwards native video props instead of forcing a track API. */} 125 215 {/* oxlint-disable-next-line jsx_a11y/media-has-caption */} 126 216 <video 127 - {...props} 217 + {...videoProps} 218 + ref={videoRef} 128 219 preload={preload} 129 220 slot="media" 130 - {...stylex.props(styles.media)} 131 - /> 221 + src={src} 222 + {...stylex.props(styles.media, styles.aspectRatio(aspectRatio))} 223 + > 224 + {subtitleTracks.map((track) => ( 225 + <track 226 + key={track.id ?? track.src} 227 + default={track.default} 228 + kind={track.kind ?? "subtitles"} 229 + label={track.label} 230 + src={track.src} 231 + srcLang={track.srcLang} 232 + /> 233 + ))} 234 + </video> 235 + {hasSubtitleTracks ? <MediaCaptionsMenu anchor="auto" hidden /> : null} 132 236 {children ?? ( 133 237 <MediaControlBar> 134 238 <MediaPlayButton /> ··· 138 242 <MediaTimeDisplay showDuration /> 139 243 <MediaMuteButton /> 140 244 <MediaVolumeRange /> 245 + {hasSubtitleTracks ? <MediaCaptionsMenuButton /> : null} 246 + <MediaPlaybackRateButton /> 141 247 <MediaFullscreenButton /> 142 248 </MediaControlBar> 143 249 )}
+8
apps/docs/src/docs/components/content/video.mdx
··· 6 6 import { PropDocs } from "../../../lib/PropDocs"; 7 7 import { Example } from "../../../lib/Example"; 8 8 import { Basic } from "../../../examples/video/basic"; 9 + import { Tracks } from "../../../examples/video/tracks"; 9 10 10 11 <Example src={Basic} /> 11 12 ··· 32 33 33 34 Pass custom Media Chrome children when you need a different control layout. 34 35 If you do not pass children, the default control bar is rendered automatically. 36 + 37 + ### Tracks 38 + 39 + Pass `subtitleTracks` to render native text tracks and enable the captions menu. 40 + Pass `audioTracks` to show the audio track menu and expose alternate selections in the default controls. 41 + 42 + <Example src={Tracks} /> 35 43 36 44 ## Props 37 45
+81
apps/docs/src/examples/video/tracks.tsx
··· 1 + import * as stylex from "@stylexjs/stylex"; 2 + 3 + import { Flex } from "@/components/flex"; 4 + import type { VideoAudioTrack, VideoSubtitleTrack } from "@/components/video"; 5 + import { Video } from "@/components/video"; 6 + import { Text } from "@/components/typography/text"; 7 + 8 + const SOURCE = "https://d2zihajmogu5jn.cloudfront.net/elephantsdream/ed_hd.mp4"; 9 + const POSTER = 10 + "https://image.mux.com/DS00Spx1CV902MCtPj5WknGlR102V5HFkDe/thumbnail.webp?width=1280"; 11 + 12 + const subtitleTracks: Array<VideoSubtitleTrack> = [ 13 + { 14 + default: true, 15 + kind: "captions", 16 + label: "English CC", 17 + src: "https://media-chrome.mux.dev/examples/vanilla/vtt/elephantsdream/captions.en.vtt", 18 + srcLang: "en", 19 + }, 20 + { 21 + kind: "captions", 22 + label: "Japanese CC", 23 + src: "https://media-chrome.mux.dev/examples/vanilla/vtt/elephantsdream/captions.ja.vtt", 24 + srcLang: "ja", 25 + }, 26 + { 27 + kind: "subtitles", 28 + label: "Swedish", 29 + src: "https://media-chrome.mux.dev/examples/vanilla/vtt/elephantsdream/captions.sv.vtt", 30 + srcLang: "sv", 31 + }, 32 + ]; 33 + 34 + const audioTracks: Array<VideoAudioTrack> = [ 35 + { 36 + enabled: true, 37 + id: "audio-en", 38 + kind: "metadata", 39 + label: "English", 40 + language: "en", 41 + }, 42 + { 43 + id: "audio-es", 44 + kind: "metadata", 45 + label: "Spanish", 46 + language: "es", 47 + }, 48 + { 49 + id: "audio-fr", 50 + kind: "metadata", 51 + label: "French", 52 + language: "fr", 53 + }, 54 + ]; 55 + 56 + const styles = stylex.create({ 57 + example: { 58 + maxWidth: 720, 59 + width: "100%", 60 + }, 61 + }); 62 + 63 + export function Tracks() { 64 + return ( 65 + <Flex direction="column" gap="lg"> 66 + <Text variant="primary"> 67 + This example passes multiple subtitle and audio track definitions to the 68 + video player. 69 + </Text> 70 + 71 + <Video 72 + crossOrigin="" 73 + playsInline 74 + poster={POSTER} 75 + src={SOURCE} 76 + style={styles.example} 77 + subtitleTracks={subtitleTracks} 78 + /> 79 + </Flex> 80 + ); 81 + }
+110 -4
packages/hip-ui/src/components/video/index.tsx
··· 1 1 "use client"; 2 2 3 + import type { ElementRef } from "react"; 4 + 3 5 import * as stylex from "@stylexjs/stylex"; 4 6 import { 5 7 MediaControlBar, ··· 7 9 MediaFullscreenButton, 8 10 MediaMuteButton, 9 11 MediaPlayButton, 12 + MediaPlaybackRateButton, 10 13 MediaSeekBackwardButton, 11 14 MediaSeekForwardButton, 12 15 MediaTimeDisplay, 13 16 MediaTimeRange, 14 17 MediaVolumeRange, 15 18 } from "media-chrome/react"; 19 + import { 20 + MediaCaptionsMenu, 21 + MediaCaptionsMenuButton, 22 + } from "media-chrome/react/menu"; 23 + import { useRef } from "react"; 16 24 17 25 import type { StyleXComponentProps } from "../theme/types"; 18 26 ··· 26 34 } from "../theme/semantic-spacing.stylex"; 27 35 28 36 const DEFAULT_SEEK_OFFSET = 10; 37 + type VideoTrackKind = 38 + | "captions" 39 + | "chapters" 40 + | "descriptions" 41 + | "metadata" 42 + | "subtitles"; 29 43 30 44 const styles = stylex.create({ 31 45 root: { ··· 75 89 height: "100%", 76 90 width: "100%", 77 91 }, 92 + aspectRatio: (aspectRatio: number) => ({ 93 + aspectRatio, 94 + }), 78 95 }); 79 96 80 97 /** 98 + * A subtitle or caption track rendered as a native `<track>` element. 99 + */ 100 + export interface VideoSubtitleTrack { 101 + /** 102 + * Whether this track should be enabled by default. 103 + */ 104 + default?: boolean; 105 + /** 106 + * Optional stable identifier for React keys. 107 + */ 108 + id?: string; 109 + /** 110 + * The text track kind. 111 + * @default "subtitles" 112 + */ 113 + kind?: VideoTrackKind; 114 + /** 115 + * The user-facing label shown in the captions menu. 116 + */ 117 + label: string; 118 + /** 119 + * The WebVTT file URL for this track. 120 + */ 121 + src: string; 122 + /** 123 + * The BCP-47 language tag for this track. 124 + */ 125 + srcLang: string; 126 + } 127 + 128 + /** 129 + * An audio track shown in the audio track menu. 130 + */ 131 + export interface VideoAudioTrack { 132 + /** 133 + * Whether this track should be selected by default. 134 + */ 135 + enabled?: boolean; 136 + /** 137 + * Optional stable identifier for the track. 138 + */ 139 + id?: string; 140 + /** 141 + * The media track kind. 142 + * @default "metadata" 143 + */ 144 + kind?: VideoTrackKind; 145 + /** 146 + * The user-facing label shown in the audio track menu. 147 + */ 148 + label: string; 149 + /** 150 + * The language code associated with this track. 151 + */ 152 + language?: string; 153 + /** 154 + * An optional source URL to swap in when this track is selected. 155 + */ 156 + src?: string; 157 + } 158 + 159 + /** 81 160 * Props for the Video component. 82 161 */ 83 162 export interface VideoProps extends StyleXComponentProps< ··· 104 183 * When omitted, the component renders a default control bar. 105 184 */ 106 185 children?: React.ReactNode; 186 + /** 187 + * Subtitle and caption tracks rendered inside the media element. 188 + */ 189 + subtitleTracks?: Array<VideoSubtitleTrack>; 107 190 } 108 191 109 192 export function Video({ ··· 112 195 rounded = true, 113 196 seekOffset = DEFAULT_SEEK_OFFSET, 114 197 style, 198 + aspectRatio = 16 / 9, 199 + subtitleTracks = [], 115 200 ...props 116 201 }: VideoProps) { 202 + const { src, ...videoProps } = props; 203 + const controllerRef = useRef<ElementRef<typeof MediaController> | null>(null); 204 + const videoRef = useRef<HTMLVideoElement | null>(null); 205 + const hasSubtitleTracks = subtitleTracks.length > 0; 206 + 117 207 return ( 118 208 <div 119 209 {...stylex.props(styles.root, rounded && styles.rounded, ui.bgDim, style)} 120 210 > 121 211 <MediaController 212 + ref={controllerRef} 122 213 {...stylex.props(styles.controller, styles.controllerTheme)} 123 214 > 124 - {/* Caption tracks are app-specific, so the wrapper forwards native video props instead of forcing a track API. */} 125 215 {/* oxlint-disable-next-line jsx_a11y/media-has-caption */} 126 216 <video 127 - {...props} 217 + {...videoProps} 218 + ref={videoRef} 128 219 preload={preload} 129 220 slot="media" 130 - {...stylex.props(styles.media)} 131 - /> 221 + src={src} 222 + {...stylex.props(styles.media, styles.aspectRatio(aspectRatio))} 223 + > 224 + {subtitleTracks.map((track) => ( 225 + <track 226 + key={track.id ?? track.src} 227 + default={track.default} 228 + kind={track.kind ?? "subtitles"} 229 + label={track.label} 230 + src={track.src} 231 + srcLang={track.srcLang} 232 + /> 233 + ))} 234 + </video> 235 + {hasSubtitleTracks ? <MediaCaptionsMenu anchor="auto" hidden /> : null} 132 236 {children ?? ( 133 237 <MediaControlBar> 134 238 <MediaPlayButton /> ··· 138 242 <MediaTimeDisplay showDuration /> 139 243 <MediaMuteButton /> 140 244 <MediaVolumeRange /> 245 + {hasSubtitleTracks ? <MediaCaptionsMenuButton /> : null} 246 + <MediaPlaybackRateButton /> 141 247 <MediaFullscreenButton /> 142 248 </MediaControlBar> 143 249 )}