react-native-expo-video-player 0.1.0

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.
@@ -0,0 +1,654 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Dimensions,
4
+ StyleSheet,
5
+ Text,
6
+ TouchableOpacity,
7
+ View,
8
+ Animated,
9
+ Platform,
10
+ type GestureResponderEvent,
11
+ } from 'react-native';
12
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
13
+ import { useEvent } from 'expo';
14
+ import Slider from '@react-native-community/slider';
15
+ import type { VideoControlsProps } from './types';
16
+ import { PlayerIcon } from './PlayerIcon';
17
+
18
+ const defaultThumbImage = Platform.OS === 'ios' ? require('./assets/slider-thumb.png') : undefined;
19
+
20
+ /** 格式化秒数为 m:ss */
21
+ const formatTime = (seconds: number) => {
22
+ if (!seconds || seconds < 0) return '0:00';
23
+ const m = Math.floor(seconds / 60);
24
+ const s = Math.floor(seconds % 60);
25
+ return `${m}:${s.toString().padStart(2, '0')}`;
26
+ };
27
+
28
+ export const VideoControls: React.FC<VideoControlsProps> = ({
29
+ player,
30
+ theme,
31
+ isFullscreen,
32
+ onToggleFullscreen,
33
+ onBack,
34
+ speedOptions,
35
+ speedLabel,
36
+ autoHideTimeout,
37
+ enableGestures,
38
+ showSpeedPicker: showSpeedPickerButton,
39
+ showFullscreenButton,
40
+ showQualityPicker: showQualityPickerButton,
41
+ qualityLabel,
42
+ qualityOptions,
43
+ onQualityChange,
44
+ thumbImage,
45
+ }) => {
46
+ const insets = useSafeAreaInsets();
47
+ const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });
48
+ const { status } = useEvent(player, 'statusChange', { status: player.status });
49
+
50
+ const [currentTime, setCurrentTime] = useState(0);
51
+ const [duration, setDuration] = useState(0);
52
+ const [visible, setVisible] = useState(true);
53
+ const [isSeeking, setIsSeeking] = useState(false);
54
+ const [seekValue, setSeekValue] = useState(0);
55
+ const [playbackRate, setPlaybackRate] = useState(1);
56
+ const [showSpeedPicker, setShowSpeedPicker] = useState(false);
57
+ const [showQualityPicker, setShowQualityPicker] = useState(false);
58
+ const [selectedQualityLabel, setSelectedQualityLabel] = useState('自动');
59
+ const [brightness, setBrightness] = useState(0.5);
60
+ const [gestureHint, setGestureHint] = useState<{
61
+ type: 'volume' | 'brightness' | 'seek';
62
+ value: number;
63
+ } | null>(null);
64
+
65
+ const lastTapRef = useRef(0);
66
+ const gestureStartValue = useRef(0);
67
+ const gestureType = useRef<'volume' | 'brightness' | 'seek' | null>(null);
68
+ const touchStartY = useRef(0);
69
+ const touchStartX = useRef(0);
70
+ const isGesturing = useRef(false);
71
+ const seekTarget = useRef(0);
72
+
73
+ const fadeAnim = useRef(new Animated.Value(1)).current;
74
+ const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
75
+
76
+ const resolvedThumbImage = thumbImage ?? defaultThumbImage;
77
+
78
+ // 时间更新
79
+ useEffect(() => {
80
+ player.timeUpdateEventInterval = 0.5;
81
+ const sub = player.addListener('timeUpdate', (payload) => {
82
+ if (!isSeeking) setCurrentTime(payload.currentTime);
83
+ });
84
+ return () => sub.remove();
85
+ }, [player, isSeeking]);
86
+
87
+ // 从资源加载获取时长
88
+ useEffect(() => {
89
+ const sub = player.addListener('sourceLoad', (payload) => {
90
+ setDuration(payload.duration);
91
+ });
92
+ if (player.duration > 0) setDuration(player.duration);
93
+ return () => sub.remove();
94
+ }, [player]);
95
+
96
+ // 全屏时初始化亮度
97
+ useEffect(() => {
98
+ if (isFullscreen && enableGestures) {
99
+ let Brightness: any;
100
+ try {
101
+ Brightness = require('expo-brightness');
102
+ Brightness.getBrightnessAsync().then((b: number) => setBrightness(b)).catch(() => {});
103
+ } catch {}
104
+ }
105
+ }, [isFullscreen, enableGestures]);
106
+
107
+ // 全屏手势处理
108
+ const onGestureTouchStart = useCallback(
109
+ (e: GestureResponderEvent) => {
110
+ if (!isFullscreen || !enableGestures) return;
111
+ touchStartX.current = e.nativeEvent.pageX;
112
+ touchStartY.current = e.nativeEvent.pageY;
113
+ isGesturing.current = false;
114
+ gestureType.current = null;
115
+ },
116
+ [isFullscreen, enableGestures],
117
+ );
118
+
119
+ const onGestureTouchMove = useCallback(
120
+ (e: GestureResponderEvent) => {
121
+ if (!isFullscreen || !enableGestures) return;
122
+ const dx = e.nativeEvent.pageX - touchStartX.current;
123
+ const dy = e.nativeEvent.pageY - touchStartY.current;
124
+
125
+ if (!isGesturing.current && !gestureType.current) {
126
+ if (Math.abs(dy) > 15 && Math.abs(dy) > Math.abs(dx) * 1.5) {
127
+ const windowWidth = Dimensions.get('window').width;
128
+ const isLeft = touchStartX.current < windowWidth / 2;
129
+ gestureType.current = isLeft ? 'volume' : 'brightness';
130
+ gestureStartValue.current = isLeft ? player.volume : brightness;
131
+ isGesturing.current = true;
132
+ } else if (Math.abs(dx) > 15 && Math.abs(dx) > Math.abs(dy) * 1.5) {
133
+ gestureType.current = 'seek';
134
+ gestureStartValue.current = player.currentTime;
135
+ seekTarget.current = player.currentTime;
136
+ isGesturing.current = true;
137
+ }
138
+ return;
139
+ }
140
+
141
+ if (!isGesturing.current) return;
142
+
143
+ const delta = -dy / 200;
144
+ if (gestureType.current === 'volume') {
145
+ const newVal = Math.max(0, Math.min(1, gestureStartValue.current + delta));
146
+ player.volume = newVal;
147
+ setGestureHint({ type: 'volume', value: newVal });
148
+ } else if (gestureType.current === 'brightness') {
149
+ const newVal = Math.max(0, Math.min(1, gestureStartValue.current + delta));
150
+ try {
151
+ const Brightness = require('expo-brightness');
152
+ Brightness.setBrightnessAsync(newVal).catch(() => {});
153
+ } catch {}
154
+ setBrightness(newVal);
155
+ setGestureHint({ type: 'brightness', value: newVal });
156
+ } else if (gestureType.current === 'seek') {
157
+ const seekDelta = Math.max(-120, Math.min(120, dx * 0.5));
158
+ const newTime = Math.max(0, Math.min(duration, gestureStartValue.current + seekDelta));
159
+ seekTarget.current = newTime;
160
+ setGestureHint({ type: 'seek', value: seekDelta });
161
+ }
162
+ },
163
+ [isFullscreen, enableGestures, player, brightness, duration],
164
+ );
165
+
166
+ const onGestureTouchEnd = useCallback(() => {
167
+ if (isGesturing.current) {
168
+ if (gestureType.current === 'seek') {
169
+ player.currentTime = seekTarget.current;
170
+ }
171
+ isGesturing.current = false;
172
+ gestureType.current = null;
173
+ setGestureHint(null);
174
+ }
175
+ }, [player]);
176
+
177
+ // 控制栏自动隐藏
178
+ const resetHideTimer = useCallback(() => {
179
+ if (hideTimer.current) clearTimeout(hideTimer.current);
180
+ if (isPlaying) {
181
+ hideTimer.current = setTimeout(() => {
182
+ Animated.timing(fadeAnim, {
183
+ toValue: 0,
184
+ duration: 300,
185
+ useNativeDriver: true,
186
+ }).start(() => setVisible(false));
187
+ }, autoHideTimeout);
188
+ }
189
+ }, [isPlaying, fadeAnim, autoHideTimeout]);
190
+
191
+ useEffect(() => {
192
+ if (isPlaying) {
193
+ resetHideTimer();
194
+ } else {
195
+ if (hideTimer.current) clearTimeout(hideTimer.current);
196
+ setVisible(true);
197
+ fadeAnim.setValue(1);
198
+ }
199
+ return () => {
200
+ if (hideTimer.current) clearTimeout(hideTimer.current);
201
+ };
202
+ }, [isPlaying, resetHideTimer, fadeAnim]);
203
+
204
+ const toggleVisibility = () => {
205
+ if (visible) {
206
+ Animated.timing(fadeAnim, {
207
+ toValue: 0,
208
+ duration: 200,
209
+ useNativeDriver: true,
210
+ }).start(() => setVisible(false));
211
+ } else {
212
+ setVisible(true);
213
+ fadeAnim.setValue(1);
214
+ resetHideTimer();
215
+ }
216
+ };
217
+
218
+ const togglePlay = () => {
219
+ if (isPlaying) player.pause();
220
+ else player.play();
221
+ resetHideTimer();
222
+ };
223
+
224
+ const handleSeekStart = () => setIsSeeking(true);
225
+ const handleSeekChange = (value: number) => {
226
+ setSeekValue(value);
227
+ setCurrentTime(value);
228
+ };
229
+ const handleSeekComplete = (value: number) => {
230
+ player.currentTime = value;
231
+ setIsSeeking(false);
232
+ resetHideTimer();
233
+ };
234
+
235
+ const handleTap = () => {
236
+ const now = Date.now();
237
+ if (now - lastTapRef.current < 300) {
238
+ togglePlay();
239
+ lastTapRef.current = 0;
240
+ } else {
241
+ lastTapRef.current = now;
242
+ setTimeout(() => {
243
+ if (lastTapRef.current !== 0 && Date.now() - lastTapRef.current >= 280) {
244
+ toggleVisibility();
245
+ lastTapRef.current = 0;
246
+ }
247
+ }, 300);
248
+ }
249
+ };
250
+
251
+ const handleSpeedChange = (speed: number) => {
252
+ player.playbackRate = speed;
253
+ setPlaybackRate(speed);
254
+ setShowSpeedPicker(false);
255
+ setShowQualityPicker(false);
256
+ resetHideTimer();
257
+ };
258
+
259
+ /** 切换画质:通过 replaceAsync 切换视频 URL */
260
+ const handleQualityChange = async (option: { label: string; url: string } | null) => {
261
+ const prevTime = player.currentTime;
262
+ const wasPlaying = player.playing;
263
+ if (option) {
264
+ await player.replaceAsync(option.url);
265
+ } else if (qualityOptions.length > 0) {
266
+ // "自动"选项:使用第一个 URL
267
+ await player.replaceAsync(qualityOptions[0].url);
268
+ }
269
+ // 恢复播放进度和状态
270
+ player.currentTime = prevTime;
271
+ if (wasPlaying) player.play();
272
+ setSelectedQualityLabel(option?.label ?? '自动');
273
+ setShowQualityPicker(false);
274
+ onQualityChange(option);
275
+ resetHideTimer();
276
+ };
277
+
278
+ const handleFullscreen = () => {
279
+ onToggleFullscreen();
280
+ resetHideTimer();
281
+ };
282
+
283
+ const displayTime = isSeeking ? seekValue : currentTime;
284
+
285
+ return (
286
+ <View
287
+ style={StyleSheet.absoluteFill}
288
+ onTouchStart={onGestureTouchStart}
289
+ onTouchMove={onGestureTouchMove}
290
+ onTouchEnd={onGestureTouchEnd}
291
+ >
292
+ <TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={handleTap}>
293
+ {(visible || !isPlaying) && (
294
+ <Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
295
+ {/* 返回按钮 */}
296
+ <TouchableOpacity
297
+ onPress={onBack}
298
+ style={[styles.backBtn, { top: 6, left: isFullscreen ? insets.left + 8 : 8 }]}
299
+ activeOpacity={0.7}
300
+ >
301
+ <PlayerIcon name="arrow-left" size={24} color="#fff" />
302
+ </TouchableOpacity>
303
+
304
+ {/* 底部控制栏 */}
305
+ <View
306
+ style={[
307
+ styles.bottomBar,
308
+ {
309
+ paddingBottom: isFullscreen
310
+ ? Math.max(insets.bottom, Platform.OS === 'ios' ? 8 : 4)
311
+ : 2,
312
+ paddingLeft: isFullscreen ? insets.left + 12 : 12,
313
+ paddingRight: isFullscreen ? insets.right + 12 : 12,
314
+ },
315
+ ]}
316
+ >
317
+ <TouchableOpacity onPress={togglePlay} style={styles.playBtn} activeOpacity={0.7}>
318
+ <PlayerIcon name={isPlaying ? 'pause' : 'play'} size={22} color="#fff" />
319
+ </TouchableOpacity>
320
+
321
+ <Text style={styles.timeText}>{formatTime(displayTime)}</Text>
322
+
323
+ <View style={styles.sliderContainer}>
324
+ <Slider
325
+ style={styles.slider}
326
+ minimumValue={0}
327
+ maximumValue={duration > 0 ? duration : 1}
328
+ value={displayTime}
329
+ onSlidingStart={handleSeekStart}
330
+ onValueChange={handleSeekChange}
331
+ onSlidingComplete={handleSeekComplete}
332
+ minimumTrackTintColor={theme.primary}
333
+ maximumTrackTintColor="rgba(255,255,255,0.3)"
334
+ thumbTintColor={theme.primary}
335
+ thumbImage={resolvedThumbImage}
336
+ {...(Platform.OS === 'ios' ? { tapToSeek: true } : {})}
337
+ />
338
+ </View>
339
+
340
+ <Text style={styles.timeText}>{formatTime(duration)}</Text>
341
+
342
+ {showSpeedPickerButton && (
343
+ <TouchableOpacity
344
+ onPress={() => {
345
+ setShowSpeedPicker(!showSpeedPicker);
346
+ setShowQualityPicker(false);
347
+ resetHideTimer();
348
+ }}
349
+ style={styles.bottomBtn}
350
+ activeOpacity={0.7}
351
+ >
352
+ <Text style={styles.speedText}>
353
+ {playbackRate === 1 ? speedLabel : `${playbackRate}x`}
354
+ </Text>
355
+ </TouchableOpacity>
356
+ )}
357
+
358
+ {showQualityPickerButton && (
359
+ <TouchableOpacity
360
+ onPress={() => {
361
+ setShowQualityPicker(!showQualityPicker);
362
+ setShowSpeedPicker(false);
363
+ resetHideTimer();
364
+ }}
365
+ style={styles.bottomBtn}
366
+ activeOpacity={0.7}
367
+ >
368
+ <Text style={styles.speedText}>
369
+ {selectedQualityLabel === '自动' ? qualityLabel : selectedQualityLabel}
370
+ </Text>
371
+ </TouchableOpacity>
372
+ )}
373
+
374
+ {showFullscreenButton && (
375
+ <TouchableOpacity
376
+ onPress={handleFullscreen}
377
+ style={styles.bottomBtn}
378
+ activeOpacity={0.7}
379
+ >
380
+ <PlayerIcon
381
+ name={isFullscreen ? 'fullscreen-exit' : 'fullscreen'}
382
+ size={22}
383
+ color="#fff"
384
+ />
385
+ </TouchableOpacity>
386
+ )}
387
+ </View>
388
+
389
+ {/* 倍速选择器 */}
390
+ {showSpeedPicker && (
391
+ <View
392
+ style={[
393
+ styles.speedPicker,
394
+ {
395
+ right: isFullscreen ? insets.right + 12 : 12,
396
+ bottom: isFullscreen
397
+ ? 44 + Math.max(insets.bottom, Platform.OS === 'ios' ? 8 : 4)
398
+ : 44,
399
+ },
400
+ ]}
401
+ >
402
+ {speedOptions.map((speed) => (
403
+ <TouchableOpacity
404
+ key={speed}
405
+ style={[
406
+ styles.speedOption,
407
+ playbackRate === speed && { backgroundColor: theme.primary },
408
+ ]}
409
+ onPress={() => handleSpeedChange(speed)}
410
+ activeOpacity={0.7}
411
+ >
412
+ <Text
413
+ style={[
414
+ styles.speedOptionText,
415
+ playbackRate === speed && { color: '#fff' },
416
+ ]}
417
+ >
418
+ {speed === 1 ? '1.0x' : `${speed}x`}
419
+ </Text>
420
+ </TouchableOpacity>
421
+ ))}
422
+ </View>
423
+ )}
424
+
425
+ {/* 画质选择器 */}
426
+ {showQualityPicker && qualityOptions.length > 0 && (
427
+ <View
428
+ style={[
429
+ styles.speedPicker,
430
+ {
431
+ right: isFullscreen ? insets.right + 12 : 12,
432
+ bottom: isFullscreen
433
+ ? 44 + Math.max(insets.bottom, Platform.OS === 'ios' ? 8 : 4)
434
+ : 44,
435
+ },
436
+ ]}
437
+ >
438
+ <TouchableOpacity
439
+ style={[
440
+ styles.speedOption,
441
+ selectedQualityLabel === '自动' && { backgroundColor: theme.primary },
442
+ ]}
443
+ onPress={() => handleQualityChange(null)}
444
+ activeOpacity={0.7}
445
+ >
446
+ <Text
447
+ style={[
448
+ styles.speedOptionText,
449
+ selectedQualityLabel === '自动' && { color: '#fff' },
450
+ ]}
451
+ >
452
+ 自动
453
+ </Text>
454
+ </TouchableOpacity>
455
+ {qualityOptions.map((option) => (
456
+ <TouchableOpacity
457
+ key={option.label}
458
+ style={[
459
+ styles.speedOption,
460
+ selectedQualityLabel === option.label && { backgroundColor: theme.primary },
461
+ ]}
462
+ onPress={() => handleQualityChange(option)}
463
+ activeOpacity={0.7}
464
+ >
465
+ <Text
466
+ style={[
467
+ styles.speedOptionText,
468
+ selectedQualityLabel === option.label && { color: '#fff' },
469
+ ]}
470
+ >
471
+ {option.label}
472
+ </Text>
473
+ </TouchableOpacity>
474
+ ))}
475
+ </View>
476
+ )}
477
+ </Animated.View>
478
+ )}
479
+ </TouchableOpacity>
480
+
481
+ {/* 手势提示指示器 */}
482
+ {gestureHint && (
483
+ <View style={styles.gestureIndicator} pointerEvents="none">
484
+ {gestureHint.type !== 'seek' ? (
485
+ <>
486
+ <PlayerIcon
487
+ name={
488
+ gestureHint.type === 'volume'
489
+ ? gestureHint.value === 0
490
+ ? 'volume-off'
491
+ : gestureHint.value < 0.5
492
+ ? 'volume-medium'
493
+ : 'volume-high'
494
+ : 'brightness-6'
495
+ }
496
+ size={22}
497
+ color="#fff"
498
+ />
499
+ <View style={styles.gestureBarBg}>
500
+ <View
501
+ style={[
502
+ styles.gestureBarFill,
503
+ { height: `${Math.round(gestureHint.value * 100)}%` },
504
+ ]}
505
+ />
506
+ </View>
507
+ <Text style={styles.gestureValueText}>
508
+ {Math.round(gestureHint.value * 100)}%
509
+ </Text>
510
+ </>
511
+ ) : (
512
+ <>
513
+ <PlayerIcon
514
+ name={gestureHint.value >= 0 ? 'fast-forward' : 'rewind'}
515
+ size={22}
516
+ color="#fff"
517
+ />
518
+ <Text style={styles.gestureSeekText}>
519
+ {gestureHint.value >= 0 ? '+' : ''}
520
+ {Math.round(gestureHint.value)}s
521
+ </Text>
522
+ <Text style={styles.gestureSeekTime}>{formatTime(seekTarget.current)}</Text>
523
+ </>
524
+ )}
525
+ </View>
526
+ )}
527
+ </View>
528
+ );
529
+ };
530
+
531
+ const styles = StyleSheet.create({
532
+ overlay: {
533
+ ...StyleSheet.absoluteFillObject,
534
+ backgroundColor: 'rgba(0,0,0,0.15)',
535
+ justifyContent: 'flex-end',
536
+ },
537
+ backBtn: {
538
+ position: 'absolute',
539
+ width: 36,
540
+ height: 36,
541
+ borderRadius: 18,
542
+ backgroundColor: 'rgba(0,0,0,0.4)',
543
+ justifyContent: 'center',
544
+ alignItems: 'center',
545
+ },
546
+ bottomBar: {
547
+ position: 'absolute',
548
+ bottom: 0,
549
+ left: 0,
550
+ right: 0,
551
+ flexDirection: 'row',
552
+ alignItems: 'center',
553
+ paddingHorizontal: 12,
554
+ paddingTop: 4,
555
+ },
556
+ timeText: {
557
+ color: '#fff',
558
+ fontSize: 12,
559
+ fontVariant: ['tabular-nums'],
560
+ minWidth: 36,
561
+ textAlign: 'center',
562
+ },
563
+ sliderContainer: {
564
+ flex: 1,
565
+ marginHorizontal: 4,
566
+ },
567
+ slider: {
568
+ width: '100%',
569
+ height: Platform.OS === 'ios' ? 20 : 28,
570
+ },
571
+ bottomBtn: {
572
+ marginLeft: 6,
573
+ width: 32,
574
+ height: 32,
575
+ justifyContent: 'center',
576
+ alignItems: 'center',
577
+ },
578
+ playBtn: {
579
+ width: 32,
580
+ height: 32,
581
+ justifyContent: 'center',
582
+ alignItems: 'center',
583
+ },
584
+ speedText: {
585
+ color: '#fff',
586
+ fontSize: 11,
587
+ fontWeight: '600',
588
+ },
589
+ speedPicker: {
590
+ position: 'absolute',
591
+ backgroundColor: 'rgba(0,0,0,0.85)',
592
+ borderRadius: 8,
593
+ paddingVertical: 4,
594
+ paddingHorizontal: 4,
595
+ flexDirection: 'row',
596
+ gap: 4,
597
+ },
598
+ speedOption: {
599
+ paddingHorizontal: 10,
600
+ paddingVertical: 6,
601
+ borderRadius: 6,
602
+ },
603
+ speedOptionText: {
604
+ color: 'rgba(255,255,255,0.8)',
605
+ fontSize: 12,
606
+ fontWeight: '500',
607
+ },
608
+ gestureIndicator: {
609
+ position: 'absolute',
610
+ top: '50%',
611
+ left: '50%',
612
+ marginTop: -60,
613
+ marginLeft: -36,
614
+ width: 72,
615
+ alignItems: 'center',
616
+ backgroundColor: 'rgba(0,0,0,0.7)',
617
+ borderRadius: 8,
618
+ paddingVertical: 8,
619
+ paddingHorizontal: 6,
620
+ },
621
+ gestureBarBg: {
622
+ width: 4,
623
+ height: 60,
624
+ backgroundColor: 'rgba(255,255,255,0.3)',
625
+ borderRadius: 2,
626
+ marginTop: 6,
627
+ overflow: 'hidden',
628
+ justifyContent: 'flex-end',
629
+ },
630
+ gestureBarFill: {
631
+ width: '100%',
632
+ backgroundColor: '#fff',
633
+ borderRadius: 2,
634
+ },
635
+ gestureValueText: {
636
+ color: '#fff',
637
+ fontSize: 10,
638
+ marginTop: 4,
639
+ fontVariant: ['tabular-nums'],
640
+ },
641
+ gestureSeekText: {
642
+ color: '#fff',
643
+ fontSize: 14,
644
+ fontWeight: '600',
645
+ marginTop: 4,
646
+ fontVariant: ['tabular-nums'],
647
+ },
648
+ gestureSeekTime: {
649
+ color: 'rgba(255,255,255,0.7)',
650
+ fontSize: 10,
651
+ marginTop: 2,
652
+ fontVariant: ['tabular-nums'],
653
+ },
654
+ });
Binary file
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ export { ExpoVideoPlayer } from './ExpoVideoPlayer';
2
+ export { VideoControls } from './VideoControls';
3
+ export { useFullscreen } from './useFullscreen';
4
+ export { PlayerIcon } from './PlayerIcon';
5
+ export type { ExpoVideoPlayerProps, VideoControlsProps, VideoPlayerTheme } from './types';
6
+
7
+ // 从 expo-video 重新导出常用符号,方便使用者直接引入
8
+ export {
9
+ useVideoPlayer,
10
+ VideoView,
11
+ createVideoPlayer,
12
+ isPictureInPictureSupported,
13
+ } from 'expo-video';
14
+ export type {
15
+ VideoSource,
16
+ VideoPlayer,
17
+ VideoPlayerStatus,
18
+ VideoPlayerEvents,
19
+ VideoContentFit,
20
+ DRMOptions,
21
+ BufferOptions,
22
+ VideoMetadata,
23
+ } from 'expo-video';