@draftbit/core 54.0.4-6c949a.2 → 54.0.4-8202b6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/lib/commonjs/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.js +1 -1
  2. package/lib/commonjs/components/MediaPlayer/MediaPlaybackWrapper.js +1 -1
  3. package/lib/commonjs/components/MediaPlayer/MediaPlayerCommon.js +1 -1
  4. package/lib/commonjs/components/MediaPlayer/VideoPlayer/VideoPlayer.js +1 -1
  5. package/lib/typescript/src/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.d.ts +3 -1
  6. package/lib/typescript/src/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.js +69 -54
  7. package/lib/typescript/src/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.js.map +1 -1
  8. package/lib/typescript/src/components/MediaPlayer/MediaPlaybackWrapper.d.ts +3 -2
  9. package/lib/typescript/src/components/MediaPlayer/MediaPlaybackWrapper.js +19 -21
  10. package/lib/typescript/src/components/MediaPlayer/MediaPlaybackWrapper.js.map +1 -1
  11. package/lib/typescript/src/components/MediaPlayer/MediaPlayerCommon.d.ts +5 -4
  12. package/lib/typescript/src/components/MediaPlayer/MediaPlayerCommon.js +3 -26
  13. package/lib/typescript/src/components/MediaPlayer/MediaPlayerCommon.js.map +1 -1
  14. package/lib/typescript/src/components/MediaPlayer/VideoPlayer/VideoPlayer.d.ts +14 -4
  15. package/lib/typescript/src/components/MediaPlayer/VideoPlayer/VideoPlayer.js +125 -62
  16. package/lib/typescript/src/components/MediaPlayer/VideoPlayer/VideoPlayer.js.map +1 -1
  17. package/lib/typescript/tsconfig.tsbuildinfo +1 -1
  18. package/package.json +5 -4
  19. package/src/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.tsx +84 -72
  20. package/src/components/MediaPlayer/MediaPlaybackWrapper.tsx +21 -24
  21. package/src/components/MediaPlayer/MediaPlayerCommon.ts +8 -34
  22. package/src/components/MediaPlayer/VideoPlayer/VideoPlayer.tsx +213 -86
@@ -1,136 +1,230 @@
1
1
  import React from "react";
2
- import { ImageResizeMode, Platform } from "react-native";
3
2
  import {
4
- Video as VideoPlayerComponent,
5
- VideoProps as ExpoVideoProps,
6
- ResizeMode as ExpoResizeMode,
7
- AVPlaybackStatus,
8
- VideoFullscreenUpdate,
9
- AVPlaybackSource,
10
- Audio,
11
- } from "expo-av";
3
+ Image,
4
+ ImageProps,
5
+ ImageResizeMode,
6
+ StyleSheet,
7
+ View,
8
+ } from "react-native";
9
+ import {
10
+ VideoView as VideoPlayerComponent,
11
+ VideoViewProps as ExpoVideoProps,
12
+ VideoContentFit,
13
+ useVideoPlayer,
14
+ TimeUpdateEventPayload,
15
+ VideoPlayer as VideoPlayerType,
16
+ VideoSource,
17
+ } from "expo-video";
18
+ import { setAudioModeAsync } from "expo-audio";
12
19
  import { extractSizeStyles } from "../../../utilities";
13
20
  import MediaPlaybackWrapper from "../MediaPlaybackWrapper";
14
- import type { Playback } from "expo-av/src/AV";
15
21
  import {
16
- mapToMediaPlayerStatus,
17
22
  normalizeBase64Source,
18
23
  useSourceDeepCompareEffect,
24
+ useSourceDeepCompareMemoize,
25
+ } from "../MediaPlayerCommon";
26
+ import type {
27
+ MediaPlayerRef,
28
+ MediaPlayerProps,
29
+ MediaPlayerStatus,
19
30
  } from "../MediaPlayerCommon";
20
- import type { MediaPlayerRef, MediaPlayerProps } from "../MediaPlayerCommon";
21
31
 
22
32
  type ResizeMode = "contain" | "cover" | "stretch";
23
- type ExpoVideoPropsOmitted = Omit<
24
- ExpoVideoProps,
25
- "videoStyle" | "resizeMode" | "onPlaybackStatusUpdate" | "source"
26
- >;
33
+ type ExpoVideoPropsOmitted = Omit<ExpoVideoProps, "player" | "nativeControls">;
27
34
 
28
35
  interface VideoPlayerProps extends ExpoVideoPropsOmitted, MediaPlayerProps {
29
36
  resizeMode?: ResizeMode;
30
37
  posterResizeMode?: ImageResizeMode;
38
+ posterSource?: ImageProps["source"];
39
+ usePoster?: boolean;
31
40
  playsInSilentModeIOS?: boolean;
41
+ isMuted?: boolean;
42
+ useNativeControls?: boolean;
43
+ shouldPlay?: boolean;
44
+ isLooping?: boolean;
45
+ positionMillis?: number;
46
+ rate?: number;
47
+ volume?: number;
32
48
  }
33
49
 
34
50
  export interface VideoPlayerRef extends MediaPlayerRef {
35
51
  toggleFullscreen: () => void;
36
52
  }
37
53
 
38
- // Setting playsInSilentModeIOS prop directly on Video component is unreliable,
39
- // so we need to set the audio mode globally before playing.
40
- // See:
41
- // https://github.com/expo/expo/issues/7485
42
- // https://stackoverflow.com/questions/57371543/how-to-fix-video-play-but-dont-have-sound-on-ios-with-expo
43
- const triggerAudio = async (ref: React.RefObject<MediaPlayerRef | null>) => {
44
- if (ref && ref?.current && Platform.OS === "ios") {
45
- await Audio.setAudioModeAsync({ playsInSilentModeIOS: true });
46
- ref.current.play();
47
- }
48
- };
49
-
50
54
  const VideoPlayer = React.forwardRef<VideoPlayerRef, VideoPlayerProps>(
51
55
  (
52
56
  {
53
57
  style,
54
58
  resizeMode = "contain",
55
59
  posterResizeMode = "cover",
60
+ posterSource,
61
+ usePoster = false,
56
62
  onPlaybackStatusUpdate: onPlaybackStatusUpdateProp,
57
63
  onPlaybackFinish,
58
64
  source,
59
65
  playsInSilentModeIOS = false,
66
+ isMuted = false,
67
+ useNativeControls = true,
68
+ shouldPlay = false,
69
+ isLooping = false,
70
+ positionMillis,
71
+ allowsFullscreen = true,
72
+ rate = 1,
73
+ volume = 1,
60
74
  ...rest
61
75
  },
62
76
  ref
63
77
  ) => {
64
- const [videoMediaObject, setVideoMediaObject] =
65
- React.useState<VideoPlayerComponent | null>();
78
+ const stableSource = useSourceDeepCompareMemoize(
79
+ normalizeBase64Source(source, "video")
80
+ );
81
+
82
+ const player = useVideoPlayer(stableSource, (p) => {
83
+ p.loop = isLooping;
84
+ p.muted = isMuted;
85
+ p.volume = volume;
86
+ p.playbackRate = rate;
87
+ });
88
+
89
+ const videoPlayerRef = React.useRef<VideoPlayerComponent>(null);
66
90
  const [isPlaying, setIsPlaying] = React.useState(false);
67
91
  const [isFullscreen, setIsFullscreen] = React.useState(false);
68
- const [currentSource, setCurrentSource] =
69
- React.useState<AVPlaybackSource>();
92
+ const [showPoster, setShowPoster] = React.useState(
93
+ usePoster && !!posterSource
94
+ );
95
+
70
96
  const mediaPlaybackWrapperRef = React.useRef<MediaPlayerRef>(null);
71
97
 
72
98
  const sizeStyles = extractSizeStyles(style);
73
99
 
74
- let mappedResizeMode;
100
+ React.useEffect(() => {
101
+ player.muted = isMuted;
102
+ }, [player, isMuted]);
103
+
104
+ React.useEffect(() => {
105
+ player.loop = isLooping;
106
+ }, [player, isLooping]);
107
+
108
+ React.useEffect(() => {
109
+ player.volume = volume;
110
+ }, [player, volume]);
111
+
112
+ React.useEffect(() => {
113
+ player.playbackRate = rate;
114
+ }, [player, rate]);
115
+
116
+ // Refs so statusChange can read latest shouldPlay/positionMillis
117
+ const shouldPlayRef = React.useRef(shouldPlay);
118
+ const positionMillisRef = React.useRef(positionMillis);
119
+ shouldPlayRef.current = shouldPlay;
120
+ positionMillisRef.current = positionMillis;
121
+
122
+ const hasAppliedInitialState = React.useRef(false);
123
+
124
+ React.useEffect(() => {
125
+ const timeUpdateSub = player.addListener("timeUpdate", (status) => {
126
+ onPlaybackStatusUpdateProp?.(mapToMediaPlayerStatus(status, player));
127
+ });
128
+
129
+ const playingChangeSub = player.addListener(
130
+ "playingChange",
131
+ ({ isPlaying: playing }) => {
132
+ setIsPlaying(playing);
133
+ onPlaybackStatusUpdateProp?.(mapPlayerToMediaPlayerStatus(player));
134
+ }
135
+ );
136
+
137
+ const playToEndSub = player.addListener("playToEnd", () => {
138
+ onPlaybackFinish?.();
139
+ });
140
+
141
+ const statusChangeSub = player.addListener(
142
+ "statusChange",
143
+ ({ status, error }) => {
144
+ if (status === "readyToPlay") {
145
+ setShowPoster(false);
146
+ if (!hasAppliedInitialState.current) {
147
+ hasAppliedInitialState.current = true;
148
+ if (positionMillisRef.current) {
149
+ player.currentTime = positionMillisRef.current / 1000;
150
+ }
151
+ if (shouldPlayRef.current) {
152
+ player.play();
153
+ }
154
+ }
155
+ }
156
+ const mappedStatus = mapPlayerToMediaPlayerStatus(player);
157
+ onPlaybackStatusUpdateProp?.(
158
+ status === "error" && error
159
+ ? { ...mappedStatus, isError: true, error: error.message }
160
+ : mappedStatus
161
+ );
162
+ }
163
+ );
164
+
165
+ return () => {
166
+ timeUpdateSub.remove();
167
+ playingChangeSub.remove();
168
+ playToEndSub.remove();
169
+ statusChangeSub.remove();
170
+ };
171
+ }, []);
172
+
173
+ // Replace video source when it changes (deep comparison on URI to avoid unnecessary reloads)
174
+ const isFirstSourceRender = React.useRef(true);
175
+ useSourceDeepCompareEffect(() => {
176
+ if (isFirstSourceRender.current) {
177
+ isFirstSourceRender.current = false;
178
+ return;
179
+ }
180
+ hasAppliedInitialState.current = false;
181
+ player.replace(normalizeBase64Source(source, "video") as VideoSource);
182
+ }, [source]);
183
+
184
+ let mappedVideoContentFit: VideoContentFit;
75
185
  switch (resizeMode) {
76
186
  case "contain":
77
- mappedResizeMode = ExpoResizeMode.CONTAIN;
187
+ mappedVideoContentFit = "contain";
78
188
  break;
79
189
  case "cover":
80
- mappedResizeMode = ExpoResizeMode.COVER;
190
+ mappedVideoContentFit = "cover";
81
191
  break;
82
192
  case "stretch":
83
- mappedResizeMode = ExpoResizeMode.STRETCH;
193
+ mappedVideoContentFit = "fill";
84
194
  break;
85
195
  }
86
196
 
87
- const onPlaybackStatusUpdate = (status: AVPlaybackStatus) => {
88
- const mappedStatus = mapToMediaPlayerStatus(status);
89
- onPlaybackStatusUpdateProp?.(mappedStatus);
90
-
91
- if (status.isLoaded) {
92
- if (status.didJustFinish) {
93
- onPlaybackFinish?.();
94
- }
95
- setIsPlaying(status.isPlaying);
96
- }
97
- };
98
-
99
- const onFullscreenUpdate = (fullscreenUpdate: VideoFullscreenUpdate) => {
100
- switch (fullscreenUpdate) {
101
- case VideoFullscreenUpdate.PLAYER_DID_PRESENT:
102
- case VideoFullscreenUpdate.PLAYER_WILL_PRESENT:
197
+ const onFullscreenUpdate = (type: "entered" | "exited") => {
198
+ switch (type) {
199
+ case "entered":
103
200
  setIsFullscreen(true);
104
201
  break;
105
- case VideoFullscreenUpdate.PLAYER_DID_DISMISS:
106
- case VideoFullscreenUpdate.PLAYER_WILL_DISMISS:
202
+ case "exited":
107
203
  setIsFullscreen(false);
108
204
  break;
109
205
  }
110
206
  };
111
207
 
112
208
  const toggleFullscreen = React.useCallback(async () => {
113
- if (isFullscreen) {
114
- await videoMediaObject?.dismissFullscreenPlayer();
115
- } else {
116
- await videoMediaObject?.presentFullscreenPlayer();
209
+ if (videoPlayerRef) {
210
+ if (isFullscreen) {
211
+ await videoPlayerRef.current?.exitFullscreen();
212
+ } else {
213
+ await videoPlayerRef.current?.enterFullscreen();
214
+ }
117
215
  }
118
- }, [isFullscreen, videoMediaObject]);
216
+ }, [isFullscreen]);
119
217
 
120
218
  const updateAudioMode = React.useCallback(async () => {
121
219
  try {
122
- await Audio.setAudioModeAsync({
123
- playsInSilentModeIOS,
220
+ await setAudioModeAsync({
221
+ playsInSilentMode: playsInSilentModeIOS,
124
222
  });
125
223
  } catch (e) {
126
224
  console.error("Failed to set audio mode. Error details:", e);
127
225
  }
128
226
  }, [playsInSilentModeIOS]);
129
227
 
130
- React.useEffect(() => {
131
- if (isPlaying) triggerAudio(mediaPlaybackWrapperRef);
132
- }, [mediaPlaybackWrapperRef, isPlaying]);
133
-
134
228
  React.useImperativeHandle(
135
229
  ref,
136
230
  () => ({
@@ -147,36 +241,69 @@ const VideoPlayer = React.forwardRef<VideoPlayerRef, VideoPlayerProps>(
147
241
  [toggleFullscreen, isPlaying]
148
242
  );
149
243
 
150
- useSourceDeepCompareEffect(() => {
151
- const updateSource = async () => {
152
- const finalSource = await normalizeBase64Source(source, "video");
153
- setCurrentSource(finalSource);
154
- };
155
- updateSource();
156
- }, [source]);
157
-
158
244
  return (
159
245
  <MediaPlaybackWrapper
160
- media={videoMediaObject as Playback | undefined}
246
+ player={player}
161
247
  isPlaying={isPlaying}
162
248
  ref={mediaPlaybackWrapperRef}
163
249
  onTogglePlayback={updateAudioMode}
164
250
  >
165
- <VideoPlayerComponent
166
- // https://docs.expo.dev/versions/latest/sdk/av/#example-video to see why ref is handled this way
167
- ref={(component) => setVideoMediaObject(component)}
168
- style={style}
169
- videoStyle={sizeStyles}
170
- resizeMode={mappedResizeMode}
171
- posterStyle={[sizeStyles, { resizeMode: posterResizeMode }]}
172
- onPlaybackStatusUpdate={onPlaybackStatusUpdate}
173
- onFullscreenUpdate={(e) => onFullscreenUpdate(e.fullscreenUpdate)}
174
- source={currentSource}
175
- {...rest}
176
- />
251
+ <View style={[style, styles.container]}>
252
+ <VideoPlayerComponent
253
+ ref={videoPlayerRef}
254
+ player={player}
255
+ nativeControls={useNativeControls}
256
+ style={sizeStyles}
257
+ contentFit={mappedVideoContentFit}
258
+ onFullscreenEnter={() => onFullscreenUpdate("entered")}
259
+ onFullscreenExit={() => onFullscreenUpdate("exited")}
260
+ allowsFullscreen={allowsFullscreen}
261
+ {...rest}
262
+ />
263
+ {showPoster && posterSource && (
264
+ <View style={StyleSheet.absoluteFill} pointerEvents="none">
265
+ <Image
266
+ source={posterSource}
267
+ resizeMode={posterResizeMode}
268
+ style={[StyleSheet.absoluteFill, sizeStyles]}
269
+ />
270
+ </View>
271
+ )}
272
+ </View>
177
273
  </MediaPlaybackWrapper>
178
274
  );
179
275
  }
180
276
  );
181
277
 
278
+ const styles = StyleSheet.create({
279
+ container: {
280
+ overflow: "hidden",
281
+ },
282
+ });
283
+
284
+ function mapPlayerToMediaPlayerStatus(
285
+ player: VideoPlayerType
286
+ ): MediaPlayerStatus {
287
+ return {
288
+ isPlaying: player.playing,
289
+ isLoading: player.status === "loading",
290
+ isBuffering: player.status === "loading",
291
+ currentPositionMillis: player.currentTime * 1000,
292
+ durationMillis: player.duration * 1000,
293
+ bufferedDurationMillis: player.bufferedPosition * 1000,
294
+ isError: player.status === "error",
295
+ };
296
+ }
297
+
298
+ export function mapToMediaPlayerStatus(
299
+ status: TimeUpdateEventPayload,
300
+ player: VideoPlayerType
301
+ ): MediaPlayerStatus {
302
+ return {
303
+ ...mapPlayerToMediaPlayerStatus(player),
304
+ currentPositionMillis: status.currentTime * 1000,
305
+ bufferedDurationMillis: status.bufferedPosition * 1000,
306
+ };
307
+ }
308
+
182
309
  export default VideoPlayer;