@granite-js/video 1.0.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/GraniteVideo.podspec +72 -0
  3. package/android/README.md +232 -0
  4. package/android/build.gradle +117 -0
  5. package/android/gradle.properties +8 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/run/granite/video/GraniteVideoModule.kt +70 -0
  8. package/android/src/main/java/run/granite/video/GraniteVideoPackage.kt +43 -0
  9. package/android/src/main/java/run/granite/video/GraniteVideoView.kt +384 -0
  10. package/android/src/main/java/run/granite/video/GraniteVideoViewManager.kt +318 -0
  11. package/android/src/main/java/run/granite/video/event/GraniteVideoEvents.kt +273 -0
  12. package/android/src/main/java/run/granite/video/event/VideoEventDispatcher.kt +66 -0
  13. package/android/src/main/java/run/granite/video/event/VideoEventListenerAdapter.kt +157 -0
  14. package/android/src/main/java/run/granite/video/provider/GraniteVideoProvider.kt +346 -0
  15. package/android/src/media3/AndroidManifest.xml +9 -0
  16. package/android/src/media3/java/run/granite/video/provider/media3/ExoPlayerProvider.kt +386 -0
  17. package/android/src/media3/java/run/granite/video/provider/media3/Media3ContentProvider.kt +29 -0
  18. package/android/src/media3/java/run/granite/video/provider/media3/Media3Initializer.kt +25 -0
  19. package/android/src/media3/java/run/granite/video/provider/media3/factory/ExoPlayerFactory.kt +32 -0
  20. package/android/src/media3/java/run/granite/video/provider/media3/factory/MediaSourceFactory.kt +61 -0
  21. package/android/src/media3/java/run/granite/video/provider/media3/factory/TrackSelectorFactory.kt +26 -0
  22. package/android/src/media3/java/run/granite/video/provider/media3/factory/VideoSurfaceFactory.kt +62 -0
  23. package/android/src/media3/java/run/granite/video/provider/media3/listener/ExoPlayerEventListener.kt +104 -0
  24. package/android/src/media3/java/run/granite/video/provider/media3/scheduler/ProgressScheduler.kt +56 -0
  25. package/android/src/test/java/run/granite/video/GraniteVideoViewRobolectricTest.kt +598 -0
  26. package/android/src/test/java/run/granite/video/event/VideoEventListenerAdapterTest.kt +319 -0
  27. package/android/src/test/java/run/granite/video/helpers/FakeGraniteVideoProvider.kt +161 -0
  28. package/android/src/test/java/run/granite/video/helpers/TestProgressScheduler.kt +42 -0
  29. package/android/src/test/java/run/granite/video/provider/GraniteVideoRegistryTest.kt +232 -0
  30. package/android/src/test/java/run/granite/video/provider/ProviderContractTest.kt +174 -0
  31. package/android/src/test/java/run/granite/video/provider/media3/listener/ExoPlayerEventListenerTest.kt +243 -0
  32. package/android/src/test/resources/kotest.properties +2 -0
  33. package/dist/module/GraniteVideo.js +458 -0
  34. package/dist/module/GraniteVideo.js.map +1 -0
  35. package/dist/module/GraniteVideoNativeComponent.ts +265 -0
  36. package/dist/module/index.js +7 -0
  37. package/dist/module/index.js.map +1 -0
  38. package/dist/module/package.json +1 -0
  39. package/dist/module/types.js +4 -0
  40. package/dist/module/types.js.map +1 -0
  41. package/dist/typescript/GraniteVideo.d.ts +12 -0
  42. package/dist/typescript/GraniteVideoNativeComponent.d.ts +189 -0
  43. package/dist/typescript/index.d.ts +5 -0
  44. package/dist/typescript/types.d.ts +328 -0
  45. package/ios/GraniteVideoComponentsProvider.h +10 -0
  46. package/ios/GraniteVideoProvider.swift +280 -0
  47. package/ios/GraniteVideoView.h +15 -0
  48. package/ios/GraniteVideoView.mm +661 -0
  49. package/ios/Providers/AVPlayerProvider.swift +541 -0
  50. package/package.json +106 -0
  51. package/src/GraniteVideo.tsx +575 -0
  52. package/src/GraniteVideoNativeComponent.ts +265 -0
  53. package/src/index.ts +8 -0
  54. package/src/types.ts +464 -0
@@ -0,0 +1,575 @@
1
+ import React, { forwardRef, useRef, useImperativeHandle, useCallback } from 'react';
2
+ import {
3
+ Platform,
4
+ StyleSheet,
5
+ View,
6
+ Image,
7
+ findNodeHandle,
8
+ NativeModules,
9
+ type StyleProp,
10
+ type ViewStyle,
11
+ type NativeSyntheticEvent,
12
+ } from 'react-native';
13
+ import NativeGraniteVideoView, {
14
+ Commands,
15
+ type NativeProps,
16
+ OnVideoLoadStartEvent,
17
+ OnVideoLoadEvent,
18
+ OnVideoErrorEvent,
19
+ OnVideoProgressEvent,
20
+ OnVideoSeekEvent,
21
+ OnVideoBufferEvent,
22
+ OnVideoBandwidthUpdateEvent,
23
+ OnVideoPlaybackStateChangedEvent,
24
+ OnVideoPlaybackRateChangeEvent,
25
+ OnVideoVolumeChangeEvent,
26
+ OnVideoAudioFocusChangedEvent,
27
+ OnVideoPictureInPictureStatusChangedEvent,
28
+ OnVideoControlsVisibilityChangeEvent,
29
+ OnVideoExternalPlaybackChangeEvent,
30
+ OnVideoAspectRatioEvent,
31
+ TransferEndEvent,
32
+ } from './GraniteVideoNativeComponent';
33
+ import type { VideoRef, VideoSource, VideoProps, OnLoadData } from './types';
34
+
35
+ const { GraniteVideoModule } = NativeModules;
36
+
37
+ // For Fabric (New Architecture), the component is always available through codegenNativeComponent
38
+ // We don't need to check UIManager.getViewManagerConfig which is Old Architecture only
39
+
40
+ function normalizeSource(source: VideoSource | number): NativeProps['source'] | undefined {
41
+ if (typeof source === 'number') {
42
+ // require() - not yet supported in native
43
+ return undefined;
44
+ }
45
+
46
+ return {
47
+ uri: source.uri,
48
+ type: source.type,
49
+ startPosition: source.startPosition,
50
+ cropStart: source.cropStart,
51
+ cropEnd: source.cropEnd,
52
+ };
53
+ }
54
+
55
+ function normalizeSelectedTrack(
56
+ track?: VideoProps['selectedAudioTrack']
57
+ ): NativeProps['selectedAudioTrack'] | undefined {
58
+ if (!track) {
59
+ return undefined;
60
+ }
61
+ return {
62
+ type: track.type,
63
+ value: track.value?.toString(),
64
+ };
65
+ }
66
+
67
+ function normalizeSelectedVideoTrack(
68
+ track?: VideoProps['selectedVideoTrack']
69
+ ): NativeProps['selectedVideoTrack'] | undefined {
70
+ if (!track) {
71
+ return undefined;
72
+ }
73
+ return {
74
+ type: track.type,
75
+ value: track.value,
76
+ };
77
+ }
78
+
79
+ function normalizeDrm(drm?: VideoProps['drm']): NativeProps['drm'] | undefined {
80
+ if (!drm) {
81
+ return undefined;
82
+ }
83
+ return {
84
+ type: drm.type,
85
+ licenseServer: drm.licenseServer,
86
+ contentId: drm.contentId,
87
+ certificateUrl: drm.certificateUrl,
88
+ base64Certificate: drm.base64Certificate,
89
+ };
90
+ }
91
+
92
+ function normalizeBufferConfig(config?: VideoProps['bufferConfig']): NativeProps['bufferConfig'] | undefined {
93
+ if (!config) {
94
+ return undefined;
95
+ }
96
+ return {
97
+ minBufferMs: config.minBufferMs,
98
+ maxBufferMs: config.maxBufferMs,
99
+ bufferForPlaybackMs: config.bufferForPlaybackMs,
100
+ bufferForPlaybackAfterRebufferMs: config.bufferForPlaybackAfterRebufferMs,
101
+ backBufferDurationMs: config.backBufferDurationMs,
102
+ cacheSizeMB: config.cacheSizeMB,
103
+ };
104
+ }
105
+
106
+ function getPosterUri(poster?: VideoProps['poster']): string | undefined {
107
+ if (!poster) {
108
+ return undefined;
109
+ }
110
+ if (typeof poster === 'string') {
111
+ return poster;
112
+ }
113
+ if (typeof poster === 'object' && 'uri' in poster && poster.uri) {
114
+ return poster.uri;
115
+ }
116
+ const resolved = Image.resolveAssetSource(poster as any);
117
+ return resolved?.uri;
118
+ }
119
+
120
+ const VideoBase = forwardRef<VideoRef, VideoProps>((props, ref) => {
121
+ const {
122
+ // Test ID
123
+ testID,
124
+ // Style
125
+ style,
126
+ // Source
127
+ source,
128
+ // Poster
129
+ poster,
130
+ posterResizeMode = 'contain',
131
+ // Playback Control
132
+ paused = false,
133
+ muted = false,
134
+ volume = 1.0,
135
+ rate = 1.0,
136
+ repeat = false,
137
+ playInBackground = false,
138
+ playWhenInactive = false,
139
+ automaticallyWaitsToMinimizeStalling = true,
140
+ shutterColor,
141
+ // Display
142
+ resizeMode = 'contain',
143
+ viewType = 'surface',
144
+ useTextureView = false,
145
+ useSecureView = false,
146
+ // Buffering
147
+ bufferConfig,
148
+ minLoadRetryCount = 3,
149
+ maxBitRate,
150
+ preferredForwardBufferDuration,
151
+ // Track Selection
152
+ selectedAudioTrack,
153
+ selectedTextTrack,
154
+ selectedVideoTrack,
155
+ // DRM
156
+ drm,
157
+ localSourceEncryptionKeyScheme,
158
+ // Ads
159
+ adTagUrl,
160
+ adLanguage,
161
+ // Controls
162
+ controls = false,
163
+ showNotificationControls = false,
164
+ disableFocus = false,
165
+ disableDisconnectError = false,
166
+ focusable = true,
167
+ hideShutterView = false,
168
+ preventsDisplaySleepDuringVideoPlayback = true,
169
+ // Fullscreen
170
+ fullscreen = false,
171
+ fullscreenAutorotate = true,
172
+ fullscreenOrientation = 'all',
173
+ // Picture in Picture
174
+ pictureInPicture = false,
175
+ // Content
176
+ contentStartTime,
177
+ allowsExternalPlayback = true,
178
+ audioOutput = 'speaker',
179
+ ignoreSilentSwitch = 'inherit',
180
+ mixWithOthers = 'inherit',
181
+ // Debug
182
+ debug,
183
+ // Events
184
+ onLoadStart,
185
+ onLoad,
186
+ onError,
187
+ onProgress,
188
+ onSeek,
189
+ onEnd,
190
+ onBuffer,
191
+ onBandwidthUpdate,
192
+ onPlaybackStateChanged,
193
+ onPlaybackRateChange,
194
+ onVolumeChange,
195
+ onIdle,
196
+ onReadyForDisplay,
197
+ onAudioFocusChanged,
198
+ onAudioBecomingNoisy,
199
+ onFullscreenPlayerWillPresent,
200
+ onFullscreenPlayerDidPresent,
201
+ onFullscreenPlayerWillDismiss,
202
+ onFullscreenPlayerDidDismiss,
203
+ onPictureInPictureStatusChanged,
204
+ onRestoreUserInterfaceForPictureInPictureStop,
205
+ onControlsVisibilityChange,
206
+ onExternalPlaybackChange,
207
+ onAspectRatio,
208
+ onTransferEnd,
209
+ } = props;
210
+
211
+ const nativeRef = useRef<React.ElementRef<typeof NativeGraniteVideoView>>(null);
212
+
213
+ // === Imperative Handle ===
214
+ useImperativeHandle(ref, () => ({
215
+ seek: (time: number, tolerance?: number) => {
216
+ if (nativeRef.current) {
217
+ Commands.seek(nativeRef.current, time, tolerance ?? 0);
218
+ }
219
+ },
220
+ pause: () => {
221
+ if (nativeRef.current) {
222
+ Commands.pause(nativeRef.current);
223
+ }
224
+ },
225
+ resume: () => {
226
+ if (nativeRef.current) {
227
+ Commands.resume(nativeRef.current);
228
+ }
229
+ },
230
+ setVolume: (vol: number) => {
231
+ if (nativeRef.current) {
232
+ Commands.adjustVolume(nativeRef.current, vol);
233
+ }
234
+ },
235
+ setFullScreen: (fs: boolean) => {
236
+ if (nativeRef.current) {
237
+ Commands.setFullScreen(nativeRef.current, fs);
238
+ }
239
+ },
240
+ presentFullscreenPlayer: () => {
241
+ if (nativeRef.current) {
242
+ Commands.setFullScreen(nativeRef.current, true);
243
+ }
244
+ },
245
+ dismissFullscreenPlayer: () => {
246
+ if (nativeRef.current) {
247
+ Commands.setFullScreen(nativeRef.current, false);
248
+ }
249
+ },
250
+ enterPictureInPicture: () => {
251
+ if (nativeRef.current) {
252
+ Commands.enterPictureInPicture(nativeRef.current);
253
+ }
254
+ },
255
+ exitPictureInPicture: () => {
256
+ if (nativeRef.current) {
257
+ Commands.exitPictureInPicture(nativeRef.current);
258
+ }
259
+ },
260
+ setSource: (newSource: VideoSource) => {
261
+ if (nativeRef.current && newSource.uri) {
262
+ Commands.loadSource(nativeRef.current, newSource.uri);
263
+ }
264
+ },
265
+ getCurrentPosition: async () => {
266
+ const handle = findNodeHandle(nativeRef.current);
267
+ if (handle && GraniteVideoModule?.getCurrentPosition) {
268
+ return GraniteVideoModule.getCurrentPosition(handle);
269
+ }
270
+ return 0;
271
+ },
272
+ save: async (options?: { type?: string }) => {
273
+ const handle = findNodeHandle(nativeRef.current);
274
+ if (handle && GraniteVideoModule?.save) {
275
+ return GraniteVideoModule.save(handle, options ?? {});
276
+ }
277
+ return { uri: '' };
278
+ },
279
+ restoreUserInterfaceForPictureInPictureStopCompleted: () => {
280
+ // iOS specific - handled internally
281
+ },
282
+ }));
283
+
284
+ // === Event Handlers ===
285
+ const handleLoadStart = useCallback(
286
+ (event: NativeSyntheticEvent<OnVideoLoadStartEvent>) => {
287
+ onLoadStart?.(event.nativeEvent);
288
+ },
289
+ [onLoadStart]
290
+ );
291
+
292
+ const handleLoad = useCallback(
293
+ (event: NativeSyntheticEvent<OnVideoLoadEvent>) => {
294
+ const nativeEvent = event.nativeEvent;
295
+ const loadData: OnLoadData = {
296
+ ...nativeEvent,
297
+ naturalSize: {
298
+ ...nativeEvent.naturalSize,
299
+ orientation: nativeEvent.naturalSize.orientation as OnLoadData['naturalSize']['orientation'],
300
+ },
301
+ audioTracks: [],
302
+ textTracks: [],
303
+ videoTracks: [],
304
+ };
305
+ onLoad?.(loadData);
306
+ },
307
+ [onLoad]
308
+ );
309
+
310
+ const handleError = useCallback(
311
+ (event: NativeSyntheticEvent<OnVideoErrorEvent>) => {
312
+ onError?.(event.nativeEvent);
313
+ },
314
+ [onError]
315
+ );
316
+
317
+ const handleProgress = useCallback(
318
+ (event: NativeSyntheticEvent<OnVideoProgressEvent>) => {
319
+ onProgress?.(event.nativeEvent);
320
+ },
321
+ [onProgress]
322
+ );
323
+
324
+ const handleSeek = useCallback(
325
+ (event: NativeSyntheticEvent<OnVideoSeekEvent>) => {
326
+ onSeek?.(event.nativeEvent);
327
+ },
328
+ [onSeek]
329
+ );
330
+
331
+ const handleEnd = useCallback(() => {
332
+ onEnd?.();
333
+ }, [onEnd]);
334
+
335
+ const handleBuffer = useCallback(
336
+ (event: NativeSyntheticEvent<OnVideoBufferEvent>) => {
337
+ onBuffer?.(event.nativeEvent);
338
+ },
339
+ [onBuffer]
340
+ );
341
+
342
+ const handleBandwidthUpdate = useCallback(
343
+ (event: NativeSyntheticEvent<OnVideoBandwidthUpdateEvent>) => {
344
+ onBandwidthUpdate?.(event.nativeEvent);
345
+ },
346
+ [onBandwidthUpdate]
347
+ );
348
+
349
+ const handlePlaybackStateChanged = useCallback(
350
+ (event: NativeSyntheticEvent<OnVideoPlaybackStateChangedEvent>) => {
351
+ onPlaybackStateChanged?.(event.nativeEvent);
352
+ },
353
+ [onPlaybackStateChanged]
354
+ );
355
+
356
+ const handlePlaybackRateChange = useCallback(
357
+ (event: NativeSyntheticEvent<OnVideoPlaybackRateChangeEvent>) => {
358
+ onPlaybackRateChange?.(event.nativeEvent);
359
+ },
360
+ [onPlaybackRateChange]
361
+ );
362
+
363
+ const handleVolumeChange = useCallback(
364
+ (event: NativeSyntheticEvent<OnVideoVolumeChangeEvent>) => {
365
+ onVolumeChange?.(event.nativeEvent);
366
+ },
367
+ [onVolumeChange]
368
+ );
369
+
370
+ const handleIdle = useCallback(() => {
371
+ onIdle?.();
372
+ }, [onIdle]);
373
+
374
+ const handleReadyForDisplay = useCallback(() => {
375
+ onReadyForDisplay?.();
376
+ }, [onReadyForDisplay]);
377
+
378
+ const handleAudioFocusChanged = useCallback(
379
+ (event: NativeSyntheticEvent<OnVideoAudioFocusChangedEvent>) => {
380
+ onAudioFocusChanged?.(event.nativeEvent);
381
+ },
382
+ [onAudioFocusChanged]
383
+ );
384
+
385
+ const handleAudioBecomingNoisy = useCallback(() => {
386
+ onAudioBecomingNoisy?.();
387
+ }, [onAudioBecomingNoisy]);
388
+
389
+ const handleFullscreenPlayerWillPresent = useCallback(() => {
390
+ onFullscreenPlayerWillPresent?.();
391
+ }, [onFullscreenPlayerWillPresent]);
392
+
393
+ const handleFullscreenPlayerDidPresent = useCallback(() => {
394
+ onFullscreenPlayerDidPresent?.();
395
+ }, [onFullscreenPlayerDidPresent]);
396
+
397
+ const handleFullscreenPlayerWillDismiss = useCallback(() => {
398
+ onFullscreenPlayerWillDismiss?.();
399
+ }, [onFullscreenPlayerWillDismiss]);
400
+
401
+ const handleFullscreenPlayerDidDismiss = useCallback(() => {
402
+ onFullscreenPlayerDidDismiss?.();
403
+ }, [onFullscreenPlayerDidDismiss]);
404
+
405
+ const handlePictureInPictureStatusChanged = useCallback(
406
+ (event: NativeSyntheticEvent<OnVideoPictureInPictureStatusChangedEvent>) => {
407
+ onPictureInPictureStatusChanged?.(event.nativeEvent);
408
+ },
409
+ [onPictureInPictureStatusChanged]
410
+ );
411
+
412
+ const handleRestoreUserInterfaceForPictureInPictureStop = useCallback(() => {
413
+ onRestoreUserInterfaceForPictureInPictureStop?.();
414
+ }, [onRestoreUserInterfaceForPictureInPictureStop]);
415
+
416
+ const handleControlsVisibilityChange = useCallback(
417
+ (event: NativeSyntheticEvent<OnVideoControlsVisibilityChangeEvent>) => {
418
+ onControlsVisibilityChange?.(event.nativeEvent);
419
+ },
420
+ [onControlsVisibilityChange]
421
+ );
422
+
423
+ const handleExternalPlaybackChange = useCallback(
424
+ (event: NativeSyntheticEvent<OnVideoExternalPlaybackChangeEvent>) => {
425
+ onExternalPlaybackChange?.(event.nativeEvent);
426
+ },
427
+ [onExternalPlaybackChange]
428
+ );
429
+
430
+ const handleAspectRatio = useCallback(
431
+ (event: NativeSyntheticEvent<OnVideoAspectRatioEvent>) => {
432
+ onAspectRatio?.(event.nativeEvent);
433
+ },
434
+ [onAspectRatio]
435
+ );
436
+
437
+ const handleTransferEnd = useCallback(
438
+ (event: NativeSyntheticEvent<TransferEndEvent>) => {
439
+ onTransferEnd?.(event.nativeEvent);
440
+ },
441
+ [onTransferEnd]
442
+ );
443
+
444
+ // === Render ===
445
+ const containerStyle: StyleProp<ViewStyle> = [styles.container, style];
446
+
447
+ return (
448
+ <View style={containerStyle} testID={testID}>
449
+ <NativeGraniteVideoView
450
+ ref={nativeRef}
451
+ style={styles.video}
452
+ source={normalizeSource(source)}
453
+ poster={getPosterUri(poster)}
454
+ posterResizeMode={posterResizeMode}
455
+ paused={paused}
456
+ muted={muted}
457
+ volume={volume}
458
+ rate={rate}
459
+ repeat={repeat}
460
+ playInBackground={playInBackground}
461
+ playWhenInactive={playWhenInactive}
462
+ automaticallyWaitsToMinimizeStalling={automaticallyWaitsToMinimizeStalling}
463
+ shutterColor={shutterColor}
464
+ resizeMode={resizeMode}
465
+ viewType={viewType}
466
+ useTextureView={useTextureView}
467
+ useSecureView={useSecureView}
468
+ bufferConfig={normalizeBufferConfig(bufferConfig)}
469
+ minLoadRetryCount={minLoadRetryCount}
470
+ maxBitRate={maxBitRate}
471
+ preferredForwardBufferDuration={preferredForwardBufferDuration}
472
+ selectedAudioTrack={normalizeSelectedTrack(selectedAudioTrack)}
473
+ selectedTextTrack={normalizeSelectedTrack(selectedTextTrack)}
474
+ selectedVideoTrack={normalizeSelectedVideoTrack(selectedVideoTrack)}
475
+ drm={normalizeDrm(drm)}
476
+ localSourceEncryptionKeyScheme={localSourceEncryptionKeyScheme}
477
+ adTagUrl={adTagUrl}
478
+ adLanguage={adLanguage}
479
+ controls={controls}
480
+ showNotificationControls={showNotificationControls}
481
+ disableFocus={disableFocus}
482
+ disableDisconnectError={disableDisconnectError}
483
+ focusable={focusable}
484
+ hideShutterView={hideShutterView}
485
+ preventsDisplaySleepDuringVideoPlayback={preventsDisplaySleepDuringVideoPlayback}
486
+ fullscreen={fullscreen}
487
+ fullscreenAutorotate={fullscreenAutorotate}
488
+ fullscreenOrientation={fullscreenOrientation}
489
+ pictureInPicture={pictureInPicture}
490
+ contentStartTime={contentStartTime}
491
+ allowsExternalPlayback={allowsExternalPlayback}
492
+ audioOutput={audioOutput}
493
+ ignoreSilentSwitch={ignoreSilentSwitch}
494
+ mixWithOthers={mixWithOthers}
495
+ enableDebug={debug?.enable}
496
+ enableDebugThread={debug?.thread}
497
+ onVideoLoadStart={handleLoadStart}
498
+ onVideoLoad={handleLoad}
499
+ onVideoError={handleError}
500
+ onVideoProgress={handleProgress}
501
+ onVideoSeek={handleSeek}
502
+ onVideoEnd={handleEnd}
503
+ onVideoBuffer={handleBuffer}
504
+ onVideoBandwidthUpdate={handleBandwidthUpdate}
505
+ onVideoPlaybackStateChanged={handlePlaybackStateChanged}
506
+ onVideoPlaybackRateChange={handlePlaybackRateChange}
507
+ onVideoVolumeChange={handleVolumeChange}
508
+ onVideoIdle={handleIdle}
509
+ onVideoReadyForDisplay={handleReadyForDisplay}
510
+ onVideoAudioFocusChanged={handleAudioFocusChanged}
511
+ onVideoAudioBecomingNoisy={handleAudioBecomingNoisy}
512
+ onVideoFullscreenPlayerWillPresent={handleFullscreenPlayerWillPresent}
513
+ onVideoFullscreenPlayerDidPresent={handleFullscreenPlayerDidPresent}
514
+ onVideoFullscreenPlayerWillDismiss={handleFullscreenPlayerWillDismiss}
515
+ onVideoFullscreenPlayerDidDismiss={handleFullscreenPlayerDidDismiss}
516
+ onVideoPictureInPictureStatusChanged={handlePictureInPictureStatusChanged}
517
+ onVideoRestoreUserInterfaceForPictureInPictureStop={handleRestoreUserInterfaceForPictureInPictureStop}
518
+ onVideoControlsVisibilityChange={handleControlsVisibilityChange}
519
+ onVideoExternalPlaybackChange={handleExternalPlaybackChange}
520
+ onVideoAspectRatio={handleAspectRatio}
521
+ onTransferEnd={handleTransferEnd}
522
+ />
523
+ </View>
524
+ );
525
+ });
526
+
527
+ VideoBase.displayName = 'Video';
528
+
529
+ // Type for GraniteVideo with static properties
530
+ type VideoComponent = typeof VideoBase & {
531
+ isAvailable: boolean;
532
+ };
533
+
534
+ // Static property to indicate availability
535
+ // For Fabric (New Architecture), the component is always available
536
+ (VideoBase as VideoComponent).isAvailable = true;
537
+
538
+ export const Video = VideoBase as VideoComponent;
539
+
540
+ const styles = StyleSheet.create({
541
+ container: {
542
+ overflow: 'hidden',
543
+ },
544
+ video: {
545
+ ...StyleSheet.absoluteFillObject,
546
+ },
547
+ });
548
+
549
+ // === Static Methods ===
550
+ export async function clearCache(): Promise<void> {
551
+ if (GraniteVideoModule?.clearCache) {
552
+ return GraniteVideoModule.clearCache();
553
+ }
554
+ }
555
+
556
+ export async function getWidevineLevel(): Promise<number> {
557
+ if (Platform.OS === 'android' && GraniteVideoModule?.getWidevineLevel) {
558
+ return GraniteVideoModule.getWidevineLevel();
559
+ }
560
+ return 0;
561
+ }
562
+
563
+ export async function isCodecSupported(mimeType: string, width: number, height: number): Promise<boolean> {
564
+ if (GraniteVideoModule?.isCodecSupported) {
565
+ return GraniteVideoModule.isCodecSupported(mimeType, width, height);
566
+ }
567
+ return false;
568
+ }
569
+
570
+ export async function isHEVCSupported(): Promise<boolean> {
571
+ if (GraniteVideoModule?.isHEVCSupported) {
572
+ return GraniteVideoModule.isHEVCSupported();
573
+ }
574
+ return false;
575
+ }