@stepincto/expo-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 (180) hide show
  1. package/README.md +45 -0
  2. package/android/build.gradle +32 -0
  3. package/android/src/main/AndroidManifest.xml +20 -0
  4. package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
  5. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
  6. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  7. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  8. package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
  9. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
  10. package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
  11. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
  12. package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
  14. package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
  15. package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
  16. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  17. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
  18. package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
  19. package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
  20. package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
  21. package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
  22. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  23. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
  24. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
  25. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  26. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
  27. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
  28. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
  29. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
  30. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
  31. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
  32. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
  33. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
  34. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
  35. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
  36. package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
  37. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  38. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
  39. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
  40. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
  41. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
  42. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
  43. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  44. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
  45. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  46. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  47. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  48. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  49. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  50. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  51. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  52. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  53. package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
  54. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  55. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  56. package/android/src/main/res/values/styles.xml +9 -0
  57. package/app.plugin.js +1 -0
  58. package/build/NativeVideoModule.d.ts +16 -0
  59. package/build/NativeVideoModule.d.ts.map +1 -0
  60. package/build/NativeVideoModule.js +3 -0
  61. package/build/NativeVideoModule.js.map +1 -0
  62. package/build/NativeVideoModule.web.d.ts +3 -0
  63. package/build/NativeVideoModule.web.d.ts.map +1 -0
  64. package/build/NativeVideoModule.web.js +2 -0
  65. package/build/NativeVideoModule.web.js.map +1 -0
  66. package/build/NativeVideoView.d.ts +4 -0
  67. package/build/NativeVideoView.d.ts.map +1 -0
  68. package/build/NativeVideoView.js +6 -0
  69. package/build/NativeVideoView.js.map +1 -0
  70. package/build/VideoModule.d.ts +38 -0
  71. package/build/VideoModule.d.ts.map +1 -0
  72. package/build/VideoModule.js +53 -0
  73. package/build/VideoModule.js.map +1 -0
  74. package/build/VideoPlayer.d.ts +15 -0
  75. package/build/VideoPlayer.d.ts.map +1 -0
  76. package/build/VideoPlayer.js +52 -0
  77. package/build/VideoPlayer.js.map +1 -0
  78. package/build/VideoPlayer.types.d.ts +532 -0
  79. package/build/VideoPlayer.types.d.ts.map +1 -0
  80. package/build/VideoPlayer.types.js +2 -0
  81. package/build/VideoPlayer.types.js.map +1 -0
  82. package/build/VideoPlayer.web.d.ts +75 -0
  83. package/build/VideoPlayer.web.d.ts.map +1 -0
  84. package/build/VideoPlayer.web.js +376 -0
  85. package/build/VideoPlayer.web.js.map +1 -0
  86. package/build/VideoPlayerEvents.types.d.ts +262 -0
  87. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  88. package/build/VideoPlayerEvents.types.js +2 -0
  89. package/build/VideoPlayerEvents.types.js.map +1 -0
  90. package/build/VideoThumbnail.d.ts +29 -0
  91. package/build/VideoThumbnail.d.ts.map +1 -0
  92. package/build/VideoThumbnail.js +3 -0
  93. package/build/VideoThumbnail.js.map +1 -0
  94. package/build/VideoView.d.ts +44 -0
  95. package/build/VideoView.d.ts.map +1 -0
  96. package/build/VideoView.js +76 -0
  97. package/build/VideoView.js.map +1 -0
  98. package/build/VideoView.types.d.ts +147 -0
  99. package/build/VideoView.types.d.ts.map +1 -0
  100. package/build/VideoView.types.js +2 -0
  101. package/build/VideoView.types.js.map +1 -0
  102. package/build/VideoView.web.d.ts +9 -0
  103. package/build/VideoView.web.d.ts.map +1 -0
  104. package/build/VideoView.web.js +180 -0
  105. package/build/VideoView.web.js.map +1 -0
  106. package/build/index.d.ts +9 -0
  107. package/build/index.d.ts.map +1 -0
  108. package/build/index.js +7 -0
  109. package/build/index.js.map +1 -0
  110. package/build/resolveAssetSource.d.ts +3 -0
  111. package/build/resolveAssetSource.d.ts.map +1 -0
  112. package/build/resolveAssetSource.js +3 -0
  113. package/build/resolveAssetSource.js.map +1 -0
  114. package/build/resolveAssetSource.web.d.ts +4 -0
  115. package/build/resolveAssetSource.web.d.ts.map +1 -0
  116. package/build/resolveAssetSource.web.js +16 -0
  117. package/build/resolveAssetSource.web.js.map +1 -0
  118. package/expo-module.config.json +9 -0
  119. package/ios/Cache/CachableRequest.swift +44 -0
  120. package/ios/Cache/CachedResource.swift +97 -0
  121. package/ios/Cache/CachingHelpers.swift +92 -0
  122. package/ios/Cache/MediaFileHandle.swift +94 -0
  123. package/ios/Cache/MediaInfo.swift +147 -0
  124. package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
  125. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  126. package/ios/Cache/VideoCacheManager.swift +338 -0
  127. package/ios/ContentKeyDelegate.swift +214 -0
  128. package/ios/ContentKeyManager.swift +21 -0
  129. package/ios/Enums/AudioMixingMode.swift +37 -0
  130. package/ios/Enums/ContentType.swift +12 -0
  131. package/ios/Enums/DRMType.swift +20 -0
  132. package/ios/Enums/PlayerStatus.swift +10 -0
  133. package/ios/Enums/VideoContentFit.swift +39 -0
  134. package/ios/ExpoVideo.podspec +29 -0
  135. package/ios/NowPlayingManager.swift +296 -0
  136. package/ios/Records/BufferOptions.swift +12 -0
  137. package/ios/Records/DRMOptions.swift +24 -0
  138. package/ios/Records/PlaybackError.swift +10 -0
  139. package/ios/Records/Tracks.swift +176 -0
  140. package/ios/Records/VideoEventPayloads.swift +76 -0
  141. package/ios/Records/VideoMetadata.swift +16 -0
  142. package/ios/Records/VideoSize.swift +15 -0
  143. package/ios/Records/VideoSource.swift +25 -0
  144. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  145. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  146. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  147. package/ios/VideoAsset.swift +123 -0
  148. package/ios/VideoExceptions.swift +53 -0
  149. package/ios/VideoItem.swift +11 -0
  150. package/ios/VideoManager.swift +140 -0
  151. package/ios/VideoModule.swift +383 -0
  152. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  153. package/ios/VideoPlayer.swift +435 -0
  154. package/ios/VideoPlayerAudioTracks.swift +72 -0
  155. package/ios/VideoPlayerItem.swift +97 -0
  156. package/ios/VideoPlayerObserver.swift +523 -0
  157. package/ios/VideoPlayerSubtitles.swift +71 -0
  158. package/ios/VideoSourceLoader.swift +89 -0
  159. package/ios/VideoSourceLoaderListener.swift +34 -0
  160. package/ios/VideoView.swift +224 -0
  161. package/package.json +59 -0
  162. package/plugin/build/tsconfig.tsbuildinfo +1 -0
  163. package/plugin/build/withExpoVideo.d.ts +7 -0
  164. package/plugin/build/withExpoVideo.js +38 -0
  165. package/src/NativeVideoModule.ts +20 -0
  166. package/src/NativeVideoModule.web.ts +1 -0
  167. package/src/NativeVideoView.ts +8 -0
  168. package/src/VideoModule.ts +59 -0
  169. package/src/VideoPlayer.tsx +67 -0
  170. package/src/VideoPlayer.types.ts +613 -0
  171. package/src/VideoPlayer.web.tsx +451 -0
  172. package/src/VideoPlayerEvents.types.ts +313 -0
  173. package/src/VideoThumbnail.ts +31 -0
  174. package/src/VideoView.tsx +86 -0
  175. package/src/VideoView.types.ts +165 -0
  176. package/src/VideoView.web.tsx +214 -0
  177. package/src/index.ts +46 -0
  178. package/src/resolveAssetSource.ts +2 -0
  179. package/src/resolveAssetSource.web.ts +17 -0
  180. package/src/ts-declarations/react-native-assets.d.ts +1 -0
@@ -0,0 +1,214 @@
1
+ import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+
4
+ import VideoPlayer, { getSourceUri } from './VideoPlayer.web';
5
+ import type { VideoViewProps } from './VideoView.types';
6
+
7
+ function createAudioContext(): AudioContext | null {
8
+ return typeof window !== 'undefined' ? new window.AudioContext() : null;
9
+ }
10
+
11
+ function createZeroGainNode(audioContext: AudioContext | null): GainNode | null {
12
+ const zeroGainNode = audioContext?.createGain() ?? null;
13
+
14
+ if (audioContext && zeroGainNode) {
15
+ zeroGainNode.gain.value = 0;
16
+ zeroGainNode.connect(audioContext.destination);
17
+ }
18
+ return zeroGainNode;
19
+ }
20
+
21
+ function mapStyles(style: VideoViewProps['style']): React.CSSProperties {
22
+ const flattenedStyles = StyleSheet.flatten(style);
23
+ // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast.
24
+ return flattenedStyles as React.CSSProperties;
25
+ }
26
+
27
+ export function isPictureInPictureSupported(): boolean {
28
+ return typeof document === 'object' && typeof document.exitPictureInPicture === 'function';
29
+ }
30
+
31
+ export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoViewProps, ref) => {
32
+ const videoRef = useRef<null | HTMLVideoElement>(null);
33
+ const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);
34
+ const hasToSetupAudioContext = useRef(false);
35
+ const fullscreenChangeListener = useRef<null | (() => void)>(null);
36
+ const isWaitingForFirstFrame = useRef(false);
37
+
38
+ /**
39
+ * Audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.
40
+ * Using audio context nodes allows muting videos without displaying the mute icon in the video player.
41
+ * We have to keep the context that called createMediaElementSource(videoRef), as the method can't be called
42
+ * for the second time with another context and there is no way to unbind the video and audio context afterward.
43
+ */
44
+ const audioContextRef = useRef<null | AudioContext>(null);
45
+ const zeroGainNodeRef = useRef<null | GainNode>(null);
46
+
47
+ useImperativeHandle(ref, () => ({
48
+ enterFullscreen: async () => {
49
+ if (!props.allowsFullscreen) {
50
+ return;
51
+ }
52
+ await videoRef.current?.requestFullscreen();
53
+ },
54
+ exitFullscreen: async () => {
55
+ await document.exitFullscreen();
56
+ },
57
+ startPictureInPicture: async () => {
58
+ await videoRef.current?.requestPictureInPicture();
59
+ },
60
+ stopPictureInPicture: async () => {
61
+ try {
62
+ await document.exitPictureInPicture();
63
+ } catch (e) {
64
+ if (e instanceof DOMException && e.name === 'InvalidStateError') {
65
+ console.warn('The VideoView is not in Picture-in-Picture mode.');
66
+ } else {
67
+ throw e;
68
+ }
69
+ }
70
+ },
71
+ }));
72
+
73
+ useEffect(() => {
74
+ const onEnter = () => {
75
+ props.onPictureInPictureStart?.();
76
+ };
77
+ const onLeave = () => {
78
+ props.onPictureInPictureStop?.();
79
+ };
80
+ const onLoadStart = () => {
81
+ isWaitingForFirstFrame.current = true;
82
+ };
83
+ const onCanPlay = () => {
84
+ if (isWaitingForFirstFrame.current) {
85
+ props.onFirstFrameRender?.();
86
+ }
87
+ isWaitingForFirstFrame.current = false;
88
+ };
89
+ videoRef.current?.addEventListener('enterpictureinpicture', onEnter);
90
+ videoRef.current?.addEventListener('leavepictureinpicture', onLeave);
91
+ videoRef.current?.addEventListener('loadstart', onLoadStart);
92
+ videoRef.current?.addEventListener('loadeddata', onCanPlay);
93
+
94
+ return () => {
95
+ videoRef.current?.removeEventListener('enterpictureinpicture', onEnter);
96
+ videoRef.current?.removeEventListener('leavepictureinpicture', onLeave);
97
+ videoRef.current?.removeEventListener('loadstart', onLoadStart);
98
+ videoRef.current?.removeEventListener('loadeddata', onCanPlay);
99
+ };
100
+ }, [videoRef, props.onPictureInPictureStop, props.onPictureInPictureStart]);
101
+
102
+ // Adds the video view as a candidate for being the audio source for the player (when multiple views play from one
103
+ // player only one will emit audio).
104
+ function attachAudioNodes() {
105
+ const audioContext = audioContextRef.current;
106
+ const zeroGainNode = zeroGainNodeRef.current;
107
+ const mediaNode = mediaNodeRef.current;
108
+
109
+ if (audioContext && zeroGainNode && mediaNode) {
110
+ props.player.mountAudioNode(audioContext, zeroGainNode, mediaNode);
111
+ } else {
112
+ console.warn(
113
+ "Couldn't mount audio node, this might affect the audio playback when using multiple video views with the same player."
114
+ );
115
+ }
116
+ }
117
+
118
+ function detachAudioNodes() {
119
+ const audioContext = audioContextRef.current;
120
+ const mediaNode = mediaNodeRef.current;
121
+ if (audioContext && mediaNode && videoRef.current) {
122
+ props.player.unmountAudioNode(videoRef.current, audioContext, mediaNode);
123
+ }
124
+ }
125
+
126
+ function maybeSetupAudioContext() {
127
+ if (
128
+ !hasToSetupAudioContext.current ||
129
+ !navigator.userActivation.hasBeenActive ||
130
+ !videoRef.current
131
+ ) {
132
+ return;
133
+ }
134
+ const audioContext = createAudioContext();
135
+
136
+ detachAudioNodes();
137
+ audioContextRef.current = audioContext;
138
+ zeroGainNodeRef.current = createZeroGainNode(audioContextRef.current);
139
+ mediaNodeRef.current = audioContext
140
+ ? audioContext.createMediaElementSource(videoRef.current)
141
+ : null;
142
+ attachAudioNodes();
143
+ hasToSetupAudioContext.current = false;
144
+ }
145
+
146
+ function fullscreenListener() {
147
+ if (document.fullscreenElement === videoRef.current) {
148
+ props.onFullscreenEnter?.();
149
+ } else {
150
+ props.onFullscreenExit?.();
151
+ }
152
+ }
153
+
154
+ function setupFullscreenListener() {
155
+ fullscreenChangeListener.current = fullscreenListener;
156
+ videoRef.current?.addEventListener('fullscreenchange', fullscreenChangeListener.current);
157
+ }
158
+
159
+ function cleanupFullscreenListener() {
160
+ if (fullscreenChangeListener.current) {
161
+ videoRef.current?.removeEventListener('fullscreenchange', fullscreenChangeListener.current);
162
+ fullscreenChangeListener.current = null;
163
+ }
164
+ }
165
+
166
+ useEffect(() => {
167
+ if (videoRef.current) {
168
+ props.player?.mountVideoView(videoRef.current);
169
+ }
170
+ setupFullscreenListener();
171
+ attachAudioNodes();
172
+
173
+ return () => {
174
+ if (videoRef.current) {
175
+ props.player?.unmountVideoView(videoRef.current);
176
+ }
177
+ cleanupFullscreenListener();
178
+ detachAudioNodes();
179
+ };
180
+ }, [props.player]);
181
+
182
+ return (
183
+ <video
184
+ controls={props.nativeControls ?? true}
185
+ controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'}
186
+ crossOrigin={props.crossOrigin}
187
+ style={{
188
+ ...mapStyles(props.style),
189
+ objectFit: props.contentFit,
190
+ }}
191
+ onPlay={() => {
192
+ maybeSetupAudioContext();
193
+ }}
194
+ // The player can autoplay when muted, unmuting by a user should create the audio context
195
+ onVolumeChange={() => {
196
+ maybeSetupAudioContext();
197
+ }}
198
+ ref={(newRef) => {
199
+ // This is called with a null value before `player.unmountVideoView` is called,
200
+ // we can't assign null to videoRef if we want to unmount it from the player.
201
+ if (newRef && !newRef.isEqualNode(videoRef.current)) {
202
+ videoRef.current = newRef;
203
+ hasToSetupAudioContext.current = true;
204
+ maybeSetupAudioContext();
205
+ }
206
+ }}
207
+ disablePictureInPicture={!props.allowsPictureInPicture}
208
+ playsInline={props.playsInline}
209
+ src={getSourceUri(props.player?.src) ?? ''}
210
+ />
211
+ );
212
+ });
213
+
214
+ export default VideoView;
package/src/index.ts ADDED
@@ -0,0 +1,46 @@
1
+ export {
2
+ isPictureInPictureSupported,
3
+ clearVideoCacheAsync,
4
+ setVideoCacheSizeAsync,
5
+ getCurrentVideoCacheSize,
6
+ preCacheVideoPartialAsync,
7
+ preCacheVideoAsync,
8
+ isVideoCachedAsync,
9
+ } from './VideoModule';
10
+ export { VideoView } from './VideoView';
11
+ export { useVideoPlayer } from './VideoPlayer';
12
+
13
+ export { VideoContentFit, VideoViewProps, SurfaceType } from './VideoView.types';
14
+ export { VideoThumbnail } from './VideoThumbnail';
15
+
16
+ export { createVideoPlayer } from './VideoPlayer';
17
+
18
+ export {
19
+ VideoPlayer,
20
+ VideoPlayerStatus,
21
+ VideoSource,
22
+ PlayerError,
23
+ VideoMetadata,
24
+ DRMType,
25
+ DRMOptions,
26
+ BufferOptions,
27
+ AudioMixingMode,
28
+ VideoThumbnailOptions,
29
+ VideoSize,
30
+ SubtitleTrack,
31
+ AudioTrack,
32
+ VideoTrack,
33
+ ContentType,
34
+ } from './VideoPlayer.types';
35
+
36
+ export {
37
+ VideoPlayerEvents,
38
+ StatusChangeEventPayload,
39
+ PlayingChangeEventPayload,
40
+ PlaybackRateChangeEventPayload,
41
+ VolumeChangeEventPayload,
42
+ MutedChangeEventPayload,
43
+ TimeUpdateEventPayload,
44
+ SourceChangeEventPayload,
45
+ SourceLoadEventPayload,
46
+ } from './VideoPlayerEvents.types';
@@ -0,0 +1,2 @@
1
+ import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
2
+ export default resolveAssetSource;
@@ -0,0 +1,17 @@
1
+ import { getAssetByID } from '@react-native/assets-registry/registry';
2
+
3
+ // Minimal `resolveAssetSource` implementation for video on web, based on the version from `expo-asset`
4
+ export default function resolveAssetSource(assetId: number): { uri: string } | null {
5
+ const asset = getAssetByID(assetId);
6
+ if (!asset) {
7
+ return null;
8
+ }
9
+ const type = !asset.type ? '' : `.${asset.type}`;
10
+ const assetPath = __DEV__
11
+ ? asset.httpServerLocation + '/' + asset.name + type
12
+ : asset.httpServerLocation.replace(/\.\.\//g, '_') + '/' + asset.name + type;
13
+
14
+ // The base has to have a valid syntax but doesn't matter - it's removed below as we use a relative path
15
+ const fromUrl = new URL(assetPath, 'https://expo.dev');
16
+ return { uri: fromUrl.toString().replace(fromUrl.origin, '') };
17
+ }
@@ -0,0 +1 @@
1
+ /// <reference path="../../../expo-asset/src/ts-declarations/react-native-assets.d.ts" />