@@ -40,12 +40,13 @@ import {
4040} from "lucide-react" ;
4141import {
4242 MediaActionTypes ,
43+ MediaContext ,
4344 MediaProvider ,
45+ type MediaState ,
4446 timeUtils ,
4547 useMediaDispatch ,
4648 useMediaFullscreenRef ,
4749 useMediaRef ,
48- useMediaSelector ,
4950} from "media-chrome/react/media-store" ;
5051import * as React from "react" ;
5152import { forwardRef , useEffect } from "react" ;
@@ -77,6 +78,73 @@ const SEEK_TOOLTIP_Y = "--seek-tooltip-y";
7778const SPRITE_CONTAINER_WIDTH = 224 ;
7879const SPRITE_CONTAINER_HEIGHT = 128 ;
7980
81+ const DEFAULT_SEEKABLE : [ number , number ] = [ 0 , 0 ] ;
82+ const DEFAULT_BUFFERED : ( readonly [ number , number ] ) [ ] = [ ] ;
83+ const DEFAULT_CUES : readonly unknown [ ] = [ ] ;
84+ const DEFAULT_SUBTITLES_LIST : readonly unknown [ ] = [ ] ;
85+ const DEFAULT_SUBTITLES_SHOWING : readonly unknown [ ] = [ ] ;
86+ const DEFAULT_RENDITION_LIST : readonly unknown [ ] = [ ] ;
87+
88+ type S = Partial < MediaState > ;
89+ const selectPaused = ( s : S ) => s . mediaPaused ?? true ;
90+ const selectFullscreen = ( s : S ) => s . mediaIsFullscreen ?? false ;
91+ const selectLoading = ( s : S ) => s . mediaLoading ?? false ;
92+ const selectHasPlayed = ( s : S ) => s . mediaHasPlayed ?? false ;
93+ const selectError = ( s : S ) => s . mediaError ;
94+ const selectVolume = ( s : S ) => s . mediaVolume ?? 1 ;
95+ const selectMuted = ( s : S ) => s . mediaMuted ?? false ;
96+ const selectVolumeLevel = ( s : S ) => s . mediaVolumeLevel ?? "high" ;
97+ const selectCurrentTime = ( s : S ) => s . mediaCurrentTime ?? 0 ;
98+ const selectDuration = ( s : S ) => s . mediaDuration ?? 0 ;
99+ const selectSeekable = ( s : S ) => s . mediaSeekable ?? DEFAULT_SEEKABLE ;
100+ const selectBuffered = ( s : S ) => s . mediaBuffered ?? DEFAULT_BUFFERED ;
101+ const selectEnded = ( s : S ) => s . mediaEnded ?? false ;
102+ const selectChaptersCues = ( s : S ) => s . mediaChaptersCues ?? DEFAULT_CUES ;
103+ const selectPreviewTime = ( s : S ) => s . mediaPreviewTime ;
104+ const selectPreviewImage = ( s : S ) => s . mediaPreviewImage ;
105+ const selectPreviewCoords = ( s : S ) => s . mediaPreviewCoords ;
106+ const selectPlaybackRate = ( s : S ) => s . mediaPlaybackRate ?? 1 ;
107+ const selectPip = ( s : S ) => s . mediaIsPip ?? false ;
108+ const selectSubtitlesList = ( s : S ) =>
109+ s . mediaSubtitlesList ?? DEFAULT_SUBTITLES_LIST ;
110+ const selectSubtitlesShowing = ( s : S ) =>
111+ s . mediaSubtitlesShowing ?? DEFAULT_SUBTITLES_SHOWING ;
112+ const selectRenditionList = ( s : S ) =>
113+ s . mediaRenditionList ?? DEFAULT_RENDITION_LIST ;
114+ const selectRenditionSelected = ( s : S ) => s . mediaRenditionSelected ;
115+
116+ const EMPTY_MEDIA_STATE = Object . freeze ( { } ) as Partial < MediaState > ;
117+
118+ const serverSnapshotCache = new Map <
119+ ( state : Partial < MediaState > ) => unknown ,
120+ unknown
121+ > ( ) ;
122+
123+ function useMediaSelector < T > ( selector : ( state : Partial < MediaState > ) => T ) : T {
124+ const store = React . useContext ( MediaContext ) ;
125+
126+ const subscribe = React . useCallback (
127+ ( cb : ( ) => void ) => {
128+ if ( ! store ?. subscribe ) return ( ) => { } ;
129+ return store . subscribe ( cb ) ;
130+ } ,
131+ [ store ] ,
132+ ) ;
133+
134+ const getSnapshot = React . useCallback (
135+ ( ) => selector ( store ?. getState ?.( ) ?? EMPTY_MEDIA_STATE ) ,
136+ [ store , selector ] ,
137+ ) ;
138+
139+ if ( ! serverSnapshotCache . has ( selector ) ) {
140+ serverSnapshotCache . set ( selector , selector ( EMPTY_MEDIA_STATE ) ) ;
141+ }
142+ const cached = serverSnapshotCache . get ( selector ) as T ;
143+ const getServerSnapshot = React . useCallback ( ( ) => cached , [ cached ] ) ;
144+
145+ return React . useSyncExternalStore ( subscribe , getSnapshot , getServerSnapshot ) ;
146+ }
147+
80148type Direction = "ltr" | "rtl" ;
81149
82150const DirectionContext = React . createContext < Direction | undefined > ( undefined ) ;
@@ -287,10 +355,8 @@ function MediaPlayerRootImpl(props: MediaPlayerRootProps) {
287355 const lastMouseMoveRef = React . useRef < number > ( Date . now ( ) ) ;
288356 const volumeIndicatorTimeoutRef = React . useRef < NodeJS . Timeout | null > ( null ) ;
289357
290- const mediaPaused = useMediaSelector ( ( state ) => state . mediaPaused ?? true ) ;
291- const isFullscreen = useMediaSelector (
292- ( state ) => state . mediaIsFullscreen ?? false ,
293- ) ;
358+ const mediaPaused = useMediaSelector ( selectPaused ) ;
359+ const isFullscreen = useMediaSelector ( selectFullscreen ) ;
294360
295361 const [ mounted , setMounted ] = React . useState ( false ) ;
296362 React . useLayoutEffect ( ( ) => setMounted ( true ) , [ ] ) ;
@@ -901,9 +967,7 @@ function MediaPlayerControls(props: MediaPlayerControlsProps) {
901967 } = props ;
902968
903969 const context = useMediaPlayerContext ( "MediaPlayerControls" ) ;
904- const isFullscreen = useMediaSelector (
905- ( state ) => state . mediaIsFullscreen ?? false ,
906- ) ;
970+ const isFullscreen = useMediaSelector ( selectFullscreen ) ;
907971 const controlsVisible = useStoreSelector ( ( state ) => state . controlsVisible ) ;
908972 // Call the callback whenever controlsVisible changes
909973 useEffect ( ( ) => {
@@ -946,9 +1010,9 @@ function MediaPlayerLoading(props: MediaPlayerLoadingProps) {
9461010 ...loadingProps
9471011 } = props ;
9481012
949- const isLoading = useMediaSelector ( ( state ) => state . mediaLoading ?? false ) ;
950- const isPaused = useMediaSelector ( ( state ) => state . mediaPaused ?? true ) ;
951- const hasPlayed = useMediaSelector ( ( state ) => state . mediaHasPlayed ?? false ) ;
1013+ const isLoading = useMediaSelector ( selectLoading ) ;
1014+ const isPaused = useMediaSelector ( selectPaused ) ;
1015+ const hasPlayed = useMediaSelector ( selectHasPlayed ) ;
9521016
9531017 const shouldShowLoading = isLoading && ! isPaused ;
9541018 const shouldUseDelay = hasPlayed && shouldShowLoading ;
@@ -1027,10 +1091,8 @@ function MediaPlayerError(props: MediaPlayerErrorProps) {
10271091 } = props ;
10281092
10291093 const context = useMediaPlayerContext ( "MediaPlayerError" ) ;
1030- const isFullscreen = useMediaSelector (
1031- ( state ) => state . mediaIsFullscreen ?? false ,
1032- ) ;
1033- const mediaError = useMediaSelector ( ( state ) => state . mediaError ) ;
1094+ const isFullscreen = useMediaSelector ( selectFullscreen ) ;
1095+ const mediaError = useMediaSelector ( selectError ) ;
10341096
10351097 const error = errorProp ?? mediaError ;
10361098
@@ -1183,11 +1245,9 @@ interface MediaPlayerVolumeIndicatorProps extends React.ComponentProps<"div"> {
11831245function MediaPlayerVolumeIndicator ( props : MediaPlayerVolumeIndicatorProps ) {
11841246 const { asChild, className, ...indicatorProps } = props ;
11851247
1186- const mediaVolume = useMediaSelector ( ( state ) => state . mediaVolume ?? 1 ) ;
1187- const mediaMuted = useMediaSelector ( ( state ) => state . mediaMuted ?? false ) ;
1188- const mediaVolumeLevel = useMediaSelector (
1189- ( state ) => state . mediaVolumeLevel ?? "high" ,
1190- ) ;
1248+ const mediaVolume = useMediaSelector ( selectVolume ) ;
1249+ const mediaMuted = useMediaSelector ( selectMuted ) ;
1250+ const mediaVolumeLevel = useMediaSelector ( selectVolumeLevel ) ;
11911251 const volumeIndicatorVisible = useStoreSelector (
11921252 ( state ) => state . volumeIndicatorVisible ,
11931253 ) ;
@@ -1255,9 +1315,7 @@ interface MediaPlayerControlsOverlayProps extends React.ComponentProps<"div"> {
12551315function MediaPlayerControlsOverlay ( props : MediaPlayerControlsOverlayProps ) {
12561316 const { asChild, className, ...overlayProps } = props ;
12571317
1258- const isFullscreen = useMediaSelector (
1259- ( state ) => state . mediaIsFullscreen ?? false ,
1260- ) ;
1318+ const isFullscreen = useMediaSelector ( selectFullscreen ) ;
12611319 const controlsVisible = useStoreSelector ( ( state ) => state . controlsVisible ) ;
12621320
12631321 const OverlayPrimitive = asChild ? Slot : "div" ;
@@ -1283,7 +1341,7 @@ function MediaPlayerPlay(props: MediaPlayerPlayProps) {
12831341
12841342 const context = useMediaPlayerContext ( "MediaPlayerPlay" ) ;
12851343 const dispatch = useMediaDispatch ( ) ;
1286- const mediaPaused = useMediaSelector ( ( state ) => state . mediaPaused ?? true ) ;
1344+ const mediaPaused = useMediaSelector ( selectPaused ) ;
12871345
12881346 const isDisabled = disabled || context . disabled ;
12891347
@@ -1348,9 +1406,7 @@ function MediaPlayerSeekBackward(props: MediaPlayerSeekBackwardProps) {
13481406
13491407 const context = useMediaPlayerContext ( "MediaPlayerSeekBackward" ) ;
13501408 const dispatch = useMediaDispatch ( ) ;
1351- const mediaCurrentTime = useMediaSelector (
1352- ( state ) => state . mediaCurrentTime ?? 0 ,
1353- ) ;
1409+ const mediaCurrentTime = useMediaSelector ( selectCurrentTime ) ;
13541410
13551411 const isDisabled = disabled || context . disabled ;
13561412
@@ -1409,12 +1465,8 @@ function MediaPlayerSeekForward(props: MediaPlayerSeekForwardProps) {
14091465
14101466 const context = useMediaPlayerContext ( "MediaPlayerSeekForward" ) ;
14111467 const dispatch = useMediaDispatch ( ) ;
1412- const mediaCurrentTime = useMediaSelector (
1413- ( state ) => state . mediaCurrentTime ?? 0 ,
1414- ) ;
1415- const [ , seekableEnd ] = useMediaSelector (
1416- ( state ) => state . mediaSeekable ?? [ 0 , 0 ] ,
1417- ) ;
1468+ const mediaCurrentTime = useMediaSelector ( selectCurrentTime ) ;
1469+ const [ , seekableEnd ] = useMediaSelector ( selectSeekable ) ;
14181470 const isDisabled = disabled || context . disabled ;
14191471
14201472 const onSeekForward = React . useCallback (
@@ -1498,26 +1550,16 @@ function MediaPlayerSeek(props: MediaPlayerSeekProps) {
14981550 const context = useMediaPlayerContext ( SEEK_NAME ) ;
14991551 const store = useStoreContext ( SEEK_NAME ) ;
15001552 const dispatch = useMediaDispatch ( ) ;
1501- const mediaCurrentTime = useMediaSelector (
1502- ( state ) => state . mediaCurrentTime ?? 0 ,
1503- ) ;
1504- const mediaDuration = useMediaSelector ( ( state ) => state . mediaDuration ?? 0 ) ;
1505- const [ seekableStart = 0 , seekableEnd = 0 ] = useMediaSelector (
1506- ( state ) => state . mediaSeekable ?? [ 0 , 0 ] ,
1507- ) ;
1508- const mediaBuffered = useMediaSelector ( ( state ) => state . mediaBuffered ?? [ ] ) ;
1509- const mediaEnded = useMediaSelector ( ( state ) => state . mediaEnded ?? false ) ;
1553+ const mediaCurrentTime = useMediaSelector ( selectCurrentTime ) ;
1554+ const mediaDuration = useMediaSelector ( selectDuration ) ;
1555+ const [ seekableStart = 0 , seekableEnd = 0 ] = useMediaSelector ( selectSeekable ) ;
1556+ const mediaBuffered = useMediaSelector ( selectBuffered ) ;
1557+ const mediaEnded = useMediaSelector ( selectEnded ) ;
15101558
1511- const chapterCues = useMediaSelector (
1512- ( state ) => state . mediaChaptersCues ?? [ ] ,
1513- ) ;
1514- const mediaPreviewTime = useMediaSelector ( ( state ) => state . mediaPreviewTime ) ;
1515- const mediaPreviewImage = useMediaSelector (
1516- ( state ) => state . mediaPreviewImage ,
1517- ) ;
1518- const mediaPreviewCoords = useMediaSelector (
1519- ( state ) => state . mediaPreviewCoords ,
1520- ) ;
1559+ const chapterCues = useMediaSelector ( selectChaptersCues ) ;
1560+ const mediaPreviewTime = useMediaSelector ( selectPreviewTime ) ;
1561+ const mediaPreviewImage = useMediaSelector ( selectPreviewImage ) ;
1562+ const mediaPreviewCoords = useMediaSelector ( selectPreviewCoords ) ;
15211563
15221564 const seekRef = React . useRef < HTMLDivElement > ( null ) ;
15231565 const tooltipRef = React . useRef < HTMLDivElement > ( null ) ;
@@ -1551,13 +1593,17 @@ function MediaPlayerSeek(props: MediaPlayerSeekProps) {
15511593
15521594 const timeCache = React . useRef < Map < number , string > > ( new Map ( ) ) ;
15531595 const resolvedDuration = React . useMemo ( ( ) => {
1554- const candidates = [
1555- mediaDuration ,
1556- seekableEnd ,
1557- fallbackDuration ?? 0 ,
1558- ] . filter ( ( duration ) => Number . isFinite ( duration ) && duration > 0 ) ;
1559-
1560- return candidates . length > 0 ? Math . max ( ...candidates ) : 0 ;
1596+ const mediaValues = [ mediaDuration , seekableEnd ] . filter (
1597+ ( d ) => Number . isFinite ( d ) && d > 0 ,
1598+ ) ;
1599+ if ( mediaValues . length > 0 ) return Math . max ( ...mediaValues ) ;
1600+ if (
1601+ fallbackDuration != null &&
1602+ Number . isFinite ( fallbackDuration ) &&
1603+ fallbackDuration > 0
1604+ )
1605+ return fallbackDuration ;
1606+ return 0 ;
15611607 } , [ fallbackDuration , mediaDuration , seekableEnd ] ) ;
15621608 const lastKnownDurationRef = React . useRef ( resolvedDuration ) ;
15631609
@@ -2286,11 +2332,9 @@ function MediaPlayerVolume(props: MediaPlayerVolumeProps) {
22862332 const context = useMediaPlayerContext ( VOLUME_NAME ) ;
22872333 const store = useStoreContext ( VOLUME_NAME ) ;
22882334 const dispatch = useMediaDispatch ( ) ;
2289- const mediaVolume = useMediaSelector ( ( state ) => state . mediaVolume ?? 1 ) ;
2290- const mediaMuted = useMediaSelector ( ( state ) => state . mediaMuted ?? false ) ;
2291- const mediaVolumeLevel = useMediaSelector (
2292- ( state ) => state . mediaVolumeLevel ?? "high" ,
2293- ) ;
2335+ const mediaVolume = useMediaSelector ( selectVolume ) ;
2336+ const mediaMuted = useMediaSelector ( selectMuted ) ;
2337+ const mediaVolumeLevel = useMediaSelector ( selectVolumeLevel ) ;
22942338
22952339 const sliderId = React . useId ( ) ;
22962340 const volumeTriggerId = React . useId ( ) ;
@@ -2445,21 +2489,21 @@ function MediaPlayerTime(props: MediaPlayerTimeProps) {
24452489 } = props ;
24462490
24472491 const context = useMediaPlayerContext ( "MediaPlayerTime" ) ;
2448- const mediaCurrentTime = useMediaSelector (
2449- ( state ) => state . mediaCurrentTime ?? 0 ,
2450- ) ;
2451- const mediaDuration = useMediaSelector ( ( state ) => state . mediaDuration ?? 0 ) ;
2452- const [ , seekableEnd = 0 ] = useMediaSelector (
2453- ( state ) => state . mediaSeekable ?? [ 0 , 0 ] ,
2454- ) ;
2492+ const mediaCurrentTime = useMediaSelector ( selectCurrentTime ) ;
2493+ const mediaDuration = useMediaSelector ( selectDuration ) ;
2494+ const [ , seekableEnd = 0 ] = useMediaSelector ( selectSeekable ) ;
24552495 const resolvedDuration = React . useMemo ( ( ) => {
2456- const candidates = [
2457- mediaDuration ,
2458- seekableEnd ,
2459- fallbackDuration ?? 0 ,
2460- ] . filter ( ( duration ) => Number . isFinite ( duration ) && duration > 0 ) ;
2461-
2462- return candidates . length > 0 ? Math . max ( ...candidates ) : 0 ;
2496+ const mediaValues = [ mediaDuration , seekableEnd ] . filter (
2497+ ( d ) => Number . isFinite ( d ) && d > 0 ,
2498+ ) ;
2499+ if ( mediaValues . length > 0 ) return Math . max ( ...mediaValues ) ;
2500+ if (
2501+ fallbackDuration != null &&
2502+ Number . isFinite ( fallbackDuration ) &&
2503+ fallbackDuration > 0
2504+ )
2505+ return fallbackDuration ;
2506+ return 0 ;
24632507 } , [ fallbackDuration , mediaDuration , seekableEnd ] ) ;
24642508 const lastKnownDurationRef = React . useRef ( resolvedDuration ) ;
24652509
@@ -2564,9 +2608,7 @@ function MediaPlayerPlaybackSpeed(props: MediaPlayerPlaybackSpeedProps) {
25642608 const context = useMediaPlayerContext ( PLAYBACK_SPEED_NAME ) ;
25652609 const store = useStoreContext ( PLAYBACK_SPEED_NAME ) ;
25662610 const dispatch = useMediaDispatch ( ) ;
2567- const mediaPlaybackRate = useMediaSelector (
2568- ( state ) => state . mediaPlaybackRate ?? 1 ,
2569- ) ;
2611+ const mediaPlaybackRate = useMediaSelector ( selectPlaybackRate ) ;
25702612
25712613 const isDisabled = disabled || context . disabled ;
25722614
@@ -2716,9 +2758,7 @@ function MediaPlayerFullscreen(props: MediaPlayerFullscreenProps) {
27162758
27172759 const context = useMediaPlayerContext ( "MediaPlayerFullscreen" ) ;
27182760 const dispatch = useMediaDispatch ( ) ;
2719- const isFullscreen = useMediaSelector (
2720- ( state ) => state . mediaIsFullscreen ?? false ,
2721- ) ;
2761+ const isFullscreen = useMediaSelector ( selectFullscreen ) ;
27222762
27232763 const isDisabled = disabled || context . disabled ;
27242764
@@ -2768,9 +2808,7 @@ function MediaPlayerPiP(props: MediaPlayerPiPProps) {
27682808
27692809 const context = useMediaPlayerContext ( "MediaPlayerPiP" ) ;
27702810 const dispatch = useMediaDispatch ( ) ;
2771- const isPictureInPicture = useMediaSelector (
2772- ( state ) => state . mediaIsPip ?? false ,
2773- ) ;
2811+ const isPictureInPicture = useMediaSelector ( selectPip ) ;
27742812
27752813 const isDisabled = disabled || context . disabled ;
27762814
@@ -2971,7 +3009,7 @@ function EnhancedAudioSync({
29713009 enhancedAudioEnabled,
29723010 enhancedAudioMuted,
29733011} : EnhancedAudioSyncProps ) {
2974- const mediaVolume = useMediaSelector ( ( state ) => state . mediaVolume ?? 1 ) ;
3012+ const mediaVolume = useMediaSelector ( selectVolume ) ;
29753013 const wasEnhancedRef = React . useRef ( false ) ;
29763014
29773015 const syncEnhancedAudio = React . useCallback ( ( ) => {
@@ -3166,21 +3204,11 @@ function MediaPlayerSettings(props: MediaPlayerSettingsProps) {
31663204 const store = useStoreContext ( SETTINGS_NAME ) ;
31673205 const dispatch = useMediaDispatch ( ) ;
31683206
3169- const mediaPlaybackRate = useMediaSelector (
3170- ( state ) => state . mediaPlaybackRate ?? 1 ,
3171- ) ;
3172- const mediaSubtitlesList = useMediaSelector (
3173- ( state ) => state . mediaSubtitlesList ?? [ ] ,
3174- ) ;
3175- const mediaSubtitlesShowing = useMediaSelector (
3176- ( state ) => state . mediaSubtitlesShowing ?? [ ] ,
3177- ) ;
3178- const mediaRenditionList = useMediaSelector (
3179- ( state ) => state . mediaRenditionList ?? [ ] ,
3180- ) ;
3181- const selectedRenditionId = useMediaSelector (
3182- ( state ) => state . mediaRenditionSelected ,
3183- ) ;
3207+ const mediaPlaybackRate = useMediaSelector ( selectPlaybackRate ) ;
3208+ const mediaSubtitlesList = useMediaSelector ( selectSubtitlesList ) ;
3209+ const mediaSubtitlesShowing = useMediaSelector ( selectSubtitlesShowing ) ;
3210+ const mediaRenditionList = useMediaSelector ( selectRenditionList ) ;
3211+ const selectedRenditionId = useMediaSelector ( selectRenditionSelected ) ;
31843212
31853213 const isDisabled = disabled || context . disabled ;
31863214 const isSubtitlesActive = mediaSubtitlesShowing . length > 0 ;
0 commit comments