this repo has no description atmosphereconf-vods.wisp.place/
4
fork

Configure Feed

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

hls

+216 -4
+1
package.json
··· 33 33 "@tanstack/react-start": "latest", 34 34 "@tanstack/router-plugin": "^1.132.0", 35 35 "@window-splitter/react": "^1.1.3", 36 + "hls-video-element": "^1.5.11", 36 37 "lucide-react": "^0.548.0", 37 38 "media-chrome": "^4.18.3", 38 39 "react": "^19.2.0",
+27
pnpm-lock.yaml
··· 56 56 '@window-splitter/react': 57 57 specifier: ^1.1.3 58 58 version: 1.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 59 + hls-video-element: 60 + specifier: ^1.5.11 61 + version: 1.5.11 59 62 lucide-react: 60 63 specifier: ^0.548.0 61 64 version: 0.548.0(react@19.2.4) ··· 2395 2398 csstype@3.2.3: 2396 2399 resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} 2397 2400 2401 + custom-media-element@1.4.6: 2402 + resolution: {integrity: sha512-/HRYqJOa1ob5ik4q7FIJVYxTJCFs/FL3+cQPAJjUf2uiqrDEzbTgB315gQ2rG8oK3w094W9m5tcB8S5Qah+caA==} 2403 + 2398 2404 data-urls@7.0.0: 2399 2405 resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} 2400 2406 engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} ··· 2749 2755 resolution: {integrity: sha512-aJQG0t3vAnQtDb5Ny9a/ZpHl+nF4Dm62iF/Zfpk7DWDlvK8vYP6xVMP6xKQZBlbrPB9akalqmaUVM1s5POfvcg==} 2750 2756 hasBin: true 2751 2757 2758 + hls-video-element@1.5.11: 2759 + resolution: {integrity: sha512-tJJ65/52CDxj8XFyIve6zT9nVVdUIc6mqvKR25X0ycPKHk07rpjp4xxVteeCefDUBSf/tFLhlICFmn3KWj37xA==} 2760 + 2761 + hls.js@1.6.15: 2762 + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} 2763 + 2752 2764 html-encoding-sniffer@6.0.0: 2753 2765 resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} 2754 2766 engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} ··· 3086 3098 media-chrome@4.18.3: 3087 3099 resolution: {integrity: sha512-YuS2wY0Fn+2nXGijJYn4+IE0n9wFe3v6SvOZHGNkoxh32T/cCcrXHUWskA+9tyYTONa6JKwKAOJJeO6QOlJLKw==} 3088 3100 3101 + media-tracks@0.3.5: 3102 + resolution: {integrity: sha512-l54rkKXlLBt3ob3zOLWHcnjvwUmX5bNEZ70igyapOZZC9imzqBmq1oz8p2roiV04KhjblFIi2hetLPF1oYVLRA==} 3103 + 3089 3104 micromark-core-commonmark@2.0.3: 3090 3105 resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} 3091 3106 ··· 6611 6626 6612 6627 csstype@3.2.3: {} 6613 6628 6629 + custom-media-element@1.4.6: {} 6630 + 6614 6631 data-urls@7.0.0: 6615 6632 dependencies: 6616 6633 whatwg-mimetype: 5.0.0 ··· 6997 7014 - react-devtools-core 6998 7015 - typescript 6999 7016 - utf-8-validate 7017 + 7018 + hls-video-element@1.5.11: 7019 + dependencies: 7020 + custom-media-element: 1.4.6 7021 + hls.js: 1.6.15 7022 + media-tracks: 0.3.5 7023 + 7024 + hls.js@1.6.15: {} 7000 7025 7001 7026 html-encoding-sniffer@6.0.0: 7002 7027 dependencies: ··· 7433 7458 ce-la-react: 0.3.2(react@19.2.4) 7434 7459 transitivePeerDependencies: 7435 7460 - react 7461 + 7462 + media-tracks@0.3.5: {} 7436 7463 7437 7464 micromark-core-commonmark@2.0.3: 7438 7465 dependencies:
+187 -3
src/components/video/index.tsx
··· 7 7 MediaFullscreenButton, 8 8 MediaMuteButton, 9 9 MediaPlayButton, 10 + MediaPlaybackRateButton, 10 11 MediaSeekBackwardButton, 11 12 MediaSeekForwardButton, 12 13 MediaTimeDisplay, 13 14 MediaTimeRange, 14 15 MediaVolumeRange, 15 16 } from "media-chrome/react"; 17 + import { 18 + MediaCaptionsMenu, 19 + MediaCaptionsMenuButton, 20 + } from "media-chrome/react/menu"; 16 21 17 22 import type { StyleXComponentProps } from "../theme/types"; 18 23 ··· 26 31 } from "../theme/semantic-spacing.stylex"; 27 32 28 33 const DEFAULT_SEEK_OFFSET = 10; 34 + type VideoTrackKind = 35 + | "captions" 36 + | "chapters" 37 + | "descriptions" 38 + | "metadata" 39 + | "subtitles"; 40 + type VideoMediaTag = 41 + | "cloudflare-video" 42 + | "dash-video" 43 + | "hls-video" 44 + | "jwplayer-video" 45 + | "mux-video" 46 + | "shaka-video" 47 + | "video" 48 + | "video-js-video" 49 + | "vimeo-video" 50 + | "wistia-video" 51 + | "youtube-video"; 29 52 30 53 const styles = stylex.create({ 31 54 root: { ··· 80 103 }), 81 104 }); 82 105 106 + function getMediaTag(src?: string, mediaType?: VideoMediaTag): VideoMediaTag { 107 + if (mediaType) { 108 + return mediaType; 109 + } 110 + 111 + if (!src) { 112 + return "video"; 113 + } 114 + 115 + const normalizedSrc = src.toLowerCase(); 116 + 117 + if ( 118 + normalizedSrc.includes("iframe.videodelivery.net") || 119 + (normalizedSrc.includes("customer-") && 120 + normalizedSrc.includes(".cloudflarestream.com")) 121 + ) { 122 + return "cloudflare-video"; 123 + } 124 + 125 + if (normalizedSrc.includes(".mpd")) { 126 + return "dash-video"; 127 + } 128 + 129 + if ( 130 + normalizedSrc.includes(".m3u8") || 131 + normalizedSrc.includes("getvideoplaylist") 132 + ) { 133 + return "hls-video"; 134 + } 135 + 136 + if ( 137 + normalizedSrc.includes("jwplayer.com") || 138 + normalizedSrc.includes("jwplatform.com") 139 + ) { 140 + return "jwplayer-video"; 141 + } 142 + 143 + if (normalizedSrc.includes("stream.mux.com")) { 144 + return "mux-video"; 145 + } 146 + 147 + if ( 148 + normalizedSrc.includes("player.vimeo.com") || 149 + normalizedSrc.includes("vimeo.com") 150 + ) { 151 + return "vimeo-video"; 152 + } 153 + 154 + if ( 155 + normalizedSrc.includes("fast.wistia.com") || 156 + normalizedSrc.includes("wistia.com") || 157 + normalizedSrc.includes("wistia.net") 158 + ) { 159 + return "wistia-video"; 160 + } 161 + 162 + if ( 163 + normalizedSrc.includes("youtube.com") || 164 + normalizedSrc.includes("youtube-nocookie.com") || 165 + normalizedSrc.includes("youtu.be") 166 + ) { 167 + return "youtube-video"; 168 + } 169 + 170 + return "video"; 171 + } 172 + 173 + /** 174 + * A subtitle or caption track rendered as a native `<track>` element. 175 + */ 176 + export interface VideoSubtitleTrack { 177 + /** 178 + * Whether this track should be enabled by default. 179 + */ 180 + default?: boolean; 181 + /** 182 + * Optional stable identifier for React keys. 183 + */ 184 + id?: string; 185 + /** 186 + * The text track kind. 187 + * @default "subtitles" 188 + */ 189 + kind?: VideoTrackKind; 190 + /** 191 + * The user-facing label shown in the captions menu. 192 + */ 193 + label: string; 194 + /** 195 + * The WebVTT file URL for this track. 196 + */ 197 + src: string; 198 + /** 199 + * The BCP-47 language tag for this track. 200 + */ 201 + srcLang: string; 202 + } 203 + 204 + /** 205 + * An audio track shown in the audio track menu. 206 + */ 207 + export interface VideoAudioTrack { 208 + /** 209 + * Whether this track should be selected by default. 210 + */ 211 + enabled?: boolean; 212 + /** 213 + * Optional stable identifier for the track. 214 + */ 215 + id?: string; 216 + /** 217 + * The media track kind. 218 + * @default "metadata" 219 + */ 220 + kind?: VideoTrackKind; 221 + /** 222 + * The user-facing label shown in the audio track menu. 223 + */ 224 + label: string; 225 + /** 226 + * The language code associated with this track. 227 + */ 228 + language?: string; 229 + /** 230 + * An optional source URL to swap in when this track is selected. 231 + */ 232 + src?: string; 233 + } 234 + 83 235 /** 84 236 * Props for the Video component. 85 237 */ ··· 107 259 * When omitted, the component renders a default control bar. 108 260 */ 109 261 children?: React.ReactNode; 262 + /** 263 + * Subtitle and caption tracks rendered inside the media element. 264 + */ 265 + subtitleTracks?: Array<VideoSubtitleTrack>; 266 + /** 267 + * Override the Media Chrome media element used by the player. 268 + * When omitted, the component will infer common providers from `src` 269 + * and otherwise fall back to the native `video` element. 270 + */ 271 + mediaType?: VideoMediaTag; 110 272 } 111 273 112 274 export function Video({ ··· 116 278 seekOffset = DEFAULT_SEEK_OFFSET, 117 279 style, 118 280 aspectRatio = 16 / 9, 281 + mediaType, 282 + subtitleTracks = [], 119 283 ...props 120 284 }: VideoProps) { 285 + const { src, ...videoProps } = props; 286 + const hasSubtitleTracks = subtitleTracks.length > 0; 287 + const mediaTag = getMediaTag(src, mediaType); 288 + const MediaTag = mediaTag as unknown as React.ElementType; 289 + 121 290 return ( 122 291 <div 123 292 {...stylex.props(styles.root, rounded && styles.rounded, ui.bgDim, style)} ··· 127 296 > 128 297 {/* Caption tracks are app-specific, so the wrapper forwards native video props instead of forcing a track API. */} 129 298 {/* oxlint-disable-next-line jsx_a11y/media-has-caption */} 130 - <video 131 - {...props} 299 + <MediaTag 300 + {...videoProps} 132 301 preload={preload} 133 302 slot="media" 303 + src={src} 134 304 {...stylex.props(styles.media, styles.aspectRatio(aspectRatio))} 135 - /> 305 + > 306 + {subtitleTracks.map((track) => ( 307 + <track 308 + key={track.id ?? track.src} 309 + default={track.default} 310 + kind={track.kind ?? "subtitles"} 311 + label={track.label} 312 + src={track.src} 313 + srcLang={track.srcLang} 314 + /> 315 + ))} 316 + </MediaTag> 317 + {hasSubtitleTracks ? <MediaCaptionsMenu anchor="auto" hidden /> : null} 136 318 {children ?? ( 137 319 <MediaControlBar> 138 320 <MediaPlayButton /> ··· 142 324 <MediaTimeDisplay showDuration /> 143 325 <MediaMuteButton /> 144 326 <MediaVolumeRange /> 327 + {hasSubtitleTracks ? <MediaCaptionsMenuButton /> : null} 328 + <MediaPlaybackRateButton /> 145 329 <MediaFullscreenButton /> 146 330 </MediaControlBar> 147 331 )}
+1 -1
src/routes/videos.$videoSlug.tsx
··· 1 + import "hls-video-element"; 1 2 import type { LinkProps } from "@tanstack/react-router"; 2 3 import { createFileRoute, createLink, useRouter } from "@tanstack/react-router"; 3 4 import * as stylex from "@stylexjs/stylex"; ··· 53 54 props: CardProps & LinkProps & { children: React.ReactNode }, 54 55 ) { 55 56 const { isHovered, hoverProps } = useHover({}); 56 - console.log({ isHovered }); 57 57 return ( 58 58 <CardLink 59 59 {...mergeProps(