@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.
- package/lib/commonjs/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.js +1 -1
- package/lib/commonjs/components/MediaPlayer/MediaPlaybackWrapper.js +1 -1
- package/lib/commonjs/components/MediaPlayer/MediaPlayerCommon.js +1 -1
- package/lib/commonjs/components/MediaPlayer/VideoPlayer/VideoPlayer.js +1 -1
- package/lib/typescript/src/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.d.ts +3 -1
- package/lib/typescript/src/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.js +69 -54
- package/lib/typescript/src/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.js.map +1 -1
- package/lib/typescript/src/components/MediaPlayer/MediaPlaybackWrapper.d.ts +3 -2
- package/lib/typescript/src/components/MediaPlayer/MediaPlaybackWrapper.js +19 -21
- package/lib/typescript/src/components/MediaPlayer/MediaPlaybackWrapper.js.map +1 -1
- package/lib/typescript/src/components/MediaPlayer/MediaPlayerCommon.d.ts +5 -4
- package/lib/typescript/src/components/MediaPlayer/MediaPlayerCommon.js +3 -26
- package/lib/typescript/src/components/MediaPlayer/MediaPlayerCommon.js.map +1 -1
- package/lib/typescript/src/components/MediaPlayer/VideoPlayer/VideoPlayer.d.ts +14 -4
- package/lib/typescript/src/components/MediaPlayer/VideoPlayer/VideoPlayer.js +125 -62
- package/lib/typescript/src/components/MediaPlayer/VideoPlayer/VideoPlayer.js.map +1 -1
- package/lib/typescript/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.tsx +84 -72
- package/src/components/MediaPlayer/MediaPlaybackWrapper.tsx +21 -24
- package/src/components/MediaPlayer/MediaPlayerCommon.ts +8 -34
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
65
|
-
|
|
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 [
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
mappedVideoContentFit = "contain";
|
|
78
188
|
break;
|
|
79
189
|
case "cover":
|
|
80
|
-
|
|
190
|
+
mappedVideoContentFit = "cover";
|
|
81
191
|
break;
|
|
82
192
|
case "stretch":
|
|
83
|
-
|
|
193
|
+
mappedVideoContentFit = "fill";
|
|
84
194
|
break;
|
|
85
195
|
}
|
|
86
196
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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 (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
209
|
+
if (videoPlayerRef) {
|
|
210
|
+
if (isFullscreen) {
|
|
211
|
+
await videoPlayerRef.current?.exitFullscreen();
|
|
212
|
+
} else {
|
|
213
|
+
await videoPlayerRef.current?.enterFullscreen();
|
|
214
|
+
}
|
|
117
215
|
}
|
|
118
|
-
}, [isFullscreen
|
|
216
|
+
}, [isFullscreen]);
|
|
119
217
|
|
|
120
218
|
const updateAudioMode = React.useCallback(async () => {
|
|
121
219
|
try {
|
|
122
|
-
await
|
|
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
|
-
|
|
246
|
+
player={player}
|
|
161
247
|
isPlaying={isPlaying}
|
|
162
248
|
ref={mediaPlaybackWrapperRef}
|
|
163
249
|
onTogglePlayback={updateAudioMode}
|
|
164
250
|
>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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;
|