react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.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 (236) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +303 -2
  3. package/android/BUILD_VALIDATION.md +62 -0
  4. package/android/CMakeLists.txt +99 -0
  5. package/android/build-validation.json +262 -0
  6. package/android/build.gradle +185 -0
  7. package/android/cpp/EncoderTargets.cpp +226 -0
  8. package/android/cpp/EncoderTargets.h +70 -0
  9. package/android/cpp/JNIHelpers.cpp +15 -0
  10. package/android/cpp/JNIHelpers.h +20 -0
  11. package/android/cpp/NativeEventDispatcher.cpp +59 -0
  12. package/android/cpp/NativeEventDispatcher.h +31 -0
  13. package/android/cpp/NativeHardwareBuffer.cpp +397 -0
  14. package/android/cpp/NativeHardwareBuffer.h +34 -0
  15. package/android/cpp/NativeRgbaBuffer.cpp +45 -0
  16. package/android/cpp/NativeRgbaBuffer.h +9 -0
  17. package/android/cpp/ReturnedVideoFrames.h +37 -0
  18. package/android/cpp/RgbaFrameLease.h +53 -0
  19. package/android/cpp/VideoCapabilities.cpp +87 -0
  20. package/android/cpp/VideoCapabilities.h +44 -0
  21. package/android/cpp/VideoComposition.cpp +170 -0
  22. package/android/cpp/VideoComposition.h +36 -0
  23. package/android/cpp/VideoCompositionFramesExtractor.cpp +82 -0
  24. package/android/cpp/VideoCompositionFramesExtractor.h +48 -0
  25. package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +230 -0
  26. package/android/cpp/VideoCompositionFramesExtractorHostObject.h +42 -0
  27. package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.cpp +133 -0
  28. package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.h +46 -0
  29. package/android/cpp/VideoEncoderHostObject.cpp +318 -0
  30. package/android/cpp/VideoEncoderHostObject.h +76 -0
  31. package/android/cpp/VideoFrame.cpp +293 -0
  32. package/android/cpp/VideoFrame.h +27 -0
  33. package/android/cpp/VideoPlayer.cpp +96 -0
  34. package/android/cpp/VideoPlayer.h +52 -0
  35. package/android/cpp/VideoPlayerHostObject.cpp +225 -0
  36. package/android/cpp/VideoPlayerHostObject.h +43 -0
  37. package/android/cpp/cpp-adapter.cpp +316 -0
  38. package/android/gradle.properties +5 -0
  39. package/android/instrumentation-inventory.json +178 -0
  40. package/android/proguard-rules.pro +4 -0
  41. package/android/src/main/AndroidManifest.xml +2 -0
  42. package/android/src/main/java/com/azzapp/rnskv/AudioCompositionExporter.java +537 -0
  43. package/android/src/main/java/com/azzapp/rnskv/AudioCompositionPlayer.java +321 -0
  44. package/android/src/main/java/com/azzapp/rnskv/BufferLifetime.java +55 -0
  45. package/android/src/main/java/com/azzapp/rnskv/DecodeRequests.java +100 -0
  46. package/android/src/main/java/com/azzapp/rnskv/DecoderWindow.java +55 -0
  47. package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
  48. package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
  49. package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
  50. package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
  51. package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
  52. package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
  53. package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
  54. package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +35 -0
  55. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
  56. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
  57. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
  58. package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +83 -0
  59. package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
  60. package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
  61. package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
  62. package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
  63. package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
  64. package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
  65. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +772 -0
  66. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +354 -0
  67. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
  68. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
  69. package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
  70. package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
  71. package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
  72. package/cpp/RNSVCheckedSizes.h +54 -0
  73. package/cpp/RNSVEventEmitter.cpp +199 -0
  74. package/cpp/RNSVEventEmitter.h +31 -0
  75. package/cpp/RNSVHostObject.cpp +79 -0
  76. package/cpp/RNSVHostObject.h +67 -0
  77. package/cpp/RNSVMemoryBudget.cpp +68 -0
  78. package/cpp/RNSVMemoryBudget.h +100 -0
  79. package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
  80. package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
  81. package/docs/ANDROID_DEVICE_306.md +95 -0
  82. package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
  83. package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
  84. package/docs/BENCHMARKS.md +368 -0
  85. package/docs/IOS_DEVICE_TESTS.md +115 -0
  86. package/docs/IOS_SIMULATOR_TESTS.md +74 -0
  87. package/docs/MIGRATION.md +82 -0
  88. package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
  89. package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
  90. package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
  91. package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
  92. package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
  93. package/docs/PERFORMANCE_SIMULATOR.md +89 -0
  94. package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
  95. package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
  96. package/docs/UPSTREAM_NOTES.md +188 -0
  97. package/docs/assets/banner.svg +26 -0
  98. package/examples/webgpu-overlay.ts +147 -0
  99. package/ios/AVAssetTrackUtils.h +24 -0
  100. package/ios/AudioCompositionUtils.h +27 -0
  101. package/ios/AudioCompositionUtils.mm +113 -0
  102. package/ios/DecoderWindow.h +52 -0
  103. package/ios/NativeResourceStats.h +32 -0
  104. package/ios/RNSVColorSpace.h +50 -0
  105. package/ios/RNSVJSIUtils.h +46 -0
  106. package/ios/RNSVScopedPixelBuffer.h +17 -0
  107. package/ios/RNSVVideoPlayer.h +46 -0
  108. package/ios/RNSVVideoPlayer.mm +410 -0
  109. package/ios/ReactNativeSkiaVideo.h +8 -0
  110. package/ios/ReactNativeSkiaVideo.mm +310 -0
  111. package/ios/SeekPolicy.h +27 -0
  112. package/ios/VideoComposition.h +148 -0
  113. package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
  114. package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
  115. package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
  116. package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
  117. package/ios/VideoCompositionItemDecoder.h +60 -0
  118. package/ios/VideoCompositionItemDecoder.mm +480 -0
  119. package/ios/VideoEncoderHostObject.h +83 -0
  120. package/ios/VideoEncoderHostObject.mm +706 -0
  121. package/ios/VideoFrame.h +66 -0
  122. package/ios/VideoFrame.mm +108 -0
  123. package/ios/VideoPlayerHostObject.h +52 -0
  124. package/ios/VideoPlayerHostObject.mm +332 -0
  125. package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
  126. package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
  127. package/lib/module/RNSkiaVideoModule.js +20 -0
  128. package/lib/module/RNSkiaVideoModule.js.map +1 -0
  129. package/lib/module/canvas.js +18 -0
  130. package/lib/module/canvas.js.map +1 -0
  131. package/lib/module/exportCache.js +22 -0
  132. package/lib/module/exportCache.js.map +1 -0
  133. package/lib/module/exportFrames.js +338 -0
  134. package/lib/module/exportFrames.js.map +1 -0
  135. package/lib/module/exportQueue.js +52 -0
  136. package/lib/module/exportQueue.js.map +1 -0
  137. package/lib/module/exportTarget.js +137 -0
  138. package/lib/module/exportTarget.js.map +1 -0
  139. package/lib/module/exportVideoComposition.js +492 -0
  140. package/lib/module/exportVideoComposition.js.map +1 -0
  141. package/lib/module/frameInterop.js +532 -0
  142. package/lib/module/frameInterop.js.map +1 -0
  143. package/lib/module/gpu.js +222 -0
  144. package/lib/module/gpu.js.map +1 -0
  145. package/lib/module/gpuDevice.js +54 -0
  146. package/lib/module/gpuDevice.js.map +1 -0
  147. package/lib/module/index.js +36 -0
  148. package/lib/module/index.js.map +1 -0
  149. package/lib/module/memory.js +38 -0
  150. package/lib/module/memory.js.map +1 -0
  151. package/lib/module/package.json +1 -0
  152. package/lib/module/previewLifecycle.js +52 -0
  153. package/lib/module/previewLifecycle.js.map +1 -0
  154. package/lib/module/runtimeYield.js +10 -0
  155. package/lib/module/runtimeYield.js.map +1 -0
  156. package/lib/module/types.js +4 -0
  157. package/lib/module/types.js.map +1 -0
  158. package/lib/module/utils/useEventListener.js +18 -0
  159. package/lib/module/utils/useEventListener.js.map +1 -0
  160. package/lib/module/videoCompositionPlayer.js +348 -0
  161. package/lib/module/videoCompositionPlayer.js.map +1 -0
  162. package/lib/module/videoFrameImage.js +185 -0
  163. package/lib/module/videoFrameImage.js.map +1 -0
  164. package/lib/module/videoPlayer.js +172 -0
  165. package/lib/module/videoPlayer.js.map +1 -0
  166. package/lib/module/yuvBlit.js +299 -0
  167. package/lib/module/yuvBlit.js.map +1 -0
  168. package/lib/typescript/package.json +1 -0
  169. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
  170. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
  171. package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
  172. package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
  173. package/lib/typescript/src/canvas.d.ts +4 -0
  174. package/lib/typescript/src/canvas.d.ts.map +1 -0
  175. package/lib/typescript/src/exportCache.d.ts +6 -0
  176. package/lib/typescript/src/exportCache.d.ts.map +1 -0
  177. package/lib/typescript/src/exportFrames.d.ts +57 -0
  178. package/lib/typescript/src/exportFrames.d.ts.map +1 -0
  179. package/lib/typescript/src/exportQueue.d.ts +5 -0
  180. package/lib/typescript/src/exportQueue.d.ts.map +1 -0
  181. package/lib/typescript/src/exportTarget.d.ts +28 -0
  182. package/lib/typescript/src/exportTarget.d.ts.map +1 -0
  183. package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
  184. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
  185. package/lib/typescript/src/frameInterop.d.ts +17 -0
  186. package/lib/typescript/src/frameInterop.d.ts.map +1 -0
  187. package/lib/typescript/src/gpu.d.ts +39 -0
  188. package/lib/typescript/src/gpu.d.ts.map +1 -0
  189. package/lib/typescript/src/gpuDevice.d.ts +11 -0
  190. package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
  191. package/lib/typescript/src/index.d.ts +21 -0
  192. package/lib/typescript/src/index.d.ts.map +1 -0
  193. package/lib/typescript/src/memory.d.ts +25 -0
  194. package/lib/typescript/src/memory.d.ts.map +1 -0
  195. package/lib/typescript/src/previewLifecycle.d.ts +22 -0
  196. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
  197. package/lib/typescript/src/runtimeYield.d.ts +4 -0
  198. package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
  199. package/lib/typescript/src/types.d.ts +631 -0
  200. package/lib/typescript/src/types.d.ts.map +1 -0
  201. package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
  202. package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
  203. package/lib/typescript/src/videoCompositionPlayer.d.ts +85 -0
  204. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
  205. package/lib/typescript/src/videoFrameImage.d.ts +60 -0
  206. package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
  207. package/lib/typescript/src/videoPlayer.d.ts +99 -0
  208. package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
  209. package/lib/typescript/src/yuvBlit.d.ts +50 -0
  210. package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
  211. package/package.json +154 -4
  212. package/patches/react-native-skia+3.7.0.patch +502 -0
  213. package/react-native-skia-webgpu-video.podspec +35 -0
  214. package/scripts/apply-skia-canvas-patch.mjs +48 -0
  215. package/scripts/skia-canvas-patch.json +291 -0
  216. package/src/NativeReactNativeSkiaVideo.ts +8 -0
  217. package/src/RNSkiaVideoModule.ts +27 -0
  218. package/src/canvas.ts +22 -0
  219. package/src/exportCache.ts +27 -0
  220. package/src/exportFrames.ts +421 -0
  221. package/src/exportQueue.ts +65 -0
  222. package/src/exportTarget.ts +179 -0
  223. package/src/exportVideoComposition.ts +718 -0
  224. package/src/frameInterop.ts +684 -0
  225. package/src/gpu.ts +290 -0
  226. package/src/gpuDevice.ts +80 -0
  227. package/src/index.tsx +45 -0
  228. package/src/memory.ts +48 -0
  229. package/src/previewLifecycle.ts +60 -0
  230. package/src/runtimeYield.ts +6 -0
  231. package/src/types.ts +661 -0
  232. package/src/utils/useEventListener.ts +22 -0
  233. package/src/videoCompositionPlayer.ts +520 -0
  234. package/src/videoFrameImage.ts +205 -0
  235. package/src/videoPlayer.ts +318 -0
  236. package/src/yuvBlit.ts +331 -0
@@ -0,0 +1,361 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import android.net.Uri;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
6
+
7
+ import androidx.annotation.NonNull;
8
+ import androidx.media3.common.MediaItem;
9
+ import androidx.media3.common.PlaybackException;
10
+ import androidx.media3.common.Player;
11
+ import androidx.media3.common.VideoSize;
12
+ import androidx.media3.common.PlaybackParameters;
13
+ import androidx.media3.common.util.UnstableApi;
14
+ import androidx.media3.exoplayer.ExoPlayer;
15
+
16
+ import javax.microedition.khronos.egl.EGLContext;
17
+ import javax.microedition.khronos.egl.EGL10;
18
+
19
+ /**
20
+ * A class that wraps ExoPlayer to play video, and extract frames from it using OpenGL
21
+ */
22
+ @UnstableApi
23
+ public class VideoPlayer {
24
+
25
+ private ExoPlayer player;
26
+
27
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
28
+
29
+ private boolean isInitialized = false;
30
+
31
+ private float volume = 1f;
32
+
33
+ private float playbackSpeed = 1f;
34
+
35
+ private long duration = 0L;
36
+
37
+ private long currentPosition = 0L;
38
+
39
+ private boolean isPlaying = false;
40
+
41
+ private Boolean isLooping = false;
42
+
43
+ private boolean isSeeking = false;
44
+
45
+ private EGLResourcesHolder eglResourcesHolder;
46
+
47
+ private GLFrameExtractor glFrameExtractor;
48
+
49
+ private int videoWidth;
50
+ private int videoHeight;
51
+
52
+ private final int outputWidth;
53
+ private final int outputHeight;
54
+
55
+ private volatile boolean released = false;
56
+
57
+ private final NativeEventDispatcher eventDispatcher;
58
+
59
+ /**
60
+ * Create a new VideoPlayer with the given URI
61
+ */
62
+ @UnstableApi
63
+ public VideoPlayer(String uriStr, int width, int height, NativeEventDispatcher eventDispatcher) {
64
+ this.eventDispatcher = eventDispatcher;
65
+ outputWidth = width;
66
+ outputHeight = height;
67
+ mainHandler.post(() -> {
68
+ if (released) {
69
+ return;
70
+ }
71
+ player = new ExoPlayer.Builder(ReactNativeSkiaVideoModule.currentReactApplicationContext()).build();
72
+
73
+ MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.parse(uriStr)).build();
74
+ player.setMediaItem(mediaItem);
75
+
76
+ player.addListener(new Player.Listener() {
77
+ private boolean isBuffering = false;
78
+
79
+ public void setBuffering(boolean buffering) {
80
+ if (isBuffering != buffering) {
81
+ isBuffering = buffering;
82
+ dispatchEventIfNoReleased(isBuffering ? "bufferingStart" : "bufferingEnd", null);
83
+ }
84
+ }
85
+
86
+ @Override
87
+ public void onPlaybackStateChanged(final int playbackState) {
88
+ if (released) {
89
+ return;
90
+ }
91
+ if (playbackState == Player.STATE_BUFFERING) {
92
+ setBuffering(true);
93
+ } else if (playbackState == Player.STATE_READY) {
94
+ if (!isInitialized) {
95
+ isInitialized = true;
96
+ duration = player.getDuration();
97
+ VideoSize videoSize = player.getVideoSize();
98
+ ;
99
+ videoHeight = videoSize.height;
100
+ videoWidth = videoSize.width;
101
+ if (glFrameExtractor != null) {
102
+ player.setVideoSurface(glFrameExtractor.getSurface());
103
+ }
104
+ handleReady();
105
+ }
106
+ if (isSeeking) {
107
+ isSeeking = false;
108
+ dispatchEventIfNoReleased("seekComplete", null);
109
+ }
110
+ } else if (playbackState == Player.STATE_ENDED) {
111
+ dispatchEventIfNoReleased("complete", null);
112
+ }
113
+
114
+ if (playbackState != Player.STATE_BUFFERING) {
115
+ setBuffering(false);
116
+ }
117
+ }
118
+
119
+ @Override
120
+ public void onPlayerError(@NonNull final PlaybackException error) {
121
+ setBuffering(false);
122
+ if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
123
+ // See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
124
+ player.seekToDefaultPosition();
125
+ player.prepare();
126
+ } else {
127
+ dispatchEventIfNoReleased("error", error.getMessage());
128
+ }
129
+ }
130
+
131
+ @Override
132
+ public void onIsPlayingChanged(boolean playing) {
133
+ isPlaying = playing;
134
+ dispatchEventIfNoReleased("playingStatusChange", playing);
135
+ }
136
+ });
137
+
138
+ player.prepare();
139
+
140
+ dispatchBufferingUpdate();
141
+ });
142
+ }
143
+
144
+ long previousBufferedPosition = 0;
145
+
146
+ private void handleReady() {
147
+ updateCurrentPosition();
148
+ dispatchEventIfNoReleased("ready", new int[]{videoWidth, videoHeight, 0});
149
+ }
150
+
151
+ private void dispatchBufferingUpdate() {
152
+ if (released || player == null) {
153
+ return;
154
+ }
155
+ long bufferedPosition = player.getBufferedPosition();
156
+ if (bufferedPosition != previousBufferedPosition) {
157
+ previousBufferedPosition = bufferedPosition;
158
+ dispatchEventIfNoReleased("bufferingUpdate", bufferedPosition);
159
+ }
160
+ mainHandler.postDelayed(this::dispatchBufferingUpdate, 500);
161
+ }
162
+
163
+ private void updateCurrentPosition() {
164
+ if (released || player == null) {
165
+ return;
166
+ }
167
+ this.currentPosition = player.getCurrentPosition();
168
+ mainHandler.postDelayed(this::updateCurrentPosition, 50);
169
+ }
170
+
171
+ /**
172
+ * Start playing the video
173
+ */
174
+ public void play() {
175
+ mainHandler.post(() -> { if (!released && player != null) player.play(); });
176
+ }
177
+
178
+ /**
179
+ * Pause the video
180
+ */
181
+ public void pause() {
182
+ mainHandler.post(() -> { if (!released && player != null) player.pause(); });
183
+ }
184
+
185
+ /**
186
+ * Seek to a specific location in the video
187
+ */
188
+ public void seekTo(long location) {
189
+ isSeeking = true;
190
+ mainHandler.post(() -> { if (!released && player != null) player.seekTo(location); });
191
+ }
192
+
193
+ public void setupGL() {
194
+ if (Looper.myLooper() != Looper.getMainLooper()) {
195
+ throw new RuntimeException("setupGL should be called on UI Thread");
196
+ }
197
+ if (released || eglResourcesHolder != null) {
198
+ return;
199
+ }
200
+ eglResourcesHolder = EGLResourcesHolder.createWithPBBufferSurface(EGL10.EGL_NO_CONTEXT);
201
+ eglResourcesHolder.runWithContextCurrent(() ->
202
+ glFrameExtractor = new GLFrameExtractor(false, NativeHardwareBuffer.isEnabled()));
203
+ if (player != null) {
204
+ player.setVideoSurface(glFrameExtractor.getSurface());
205
+ }
206
+ }
207
+
208
+ /**
209
+ * @return whether the video is playing
210
+ */
211
+ public boolean getIsPlaying() {
212
+ return isPlaying;
213
+ }
214
+
215
+ /**
216
+ * @return the current position in the video
217
+ */
218
+ public long getCurrentPosition() {
219
+ return currentPosition;
220
+ }
221
+
222
+ /**
223
+ * @return the duration of the video
224
+ */
225
+ public long getDuration() {
226
+ return duration;
227
+ }
228
+
229
+ /**
230
+ * @return the volume of the video between 0 and 1
231
+ */
232
+ public float getVolume() {
233
+ return volume;
234
+ }
235
+
236
+ /**
237
+ * Set the volume of the video
238
+ *
239
+ * @param value the volume to set between 0 and 1
240
+ */
241
+ public void setVolume(float value) {
242
+ volume = value;
243
+ mainHandler.post(() -> {
244
+ if (!released && player != null) player.setVolume(Math.max(0f, Math.min(1f, value)));
245
+ });
246
+ }
247
+
248
+ /**
249
+ * @return whether the video is looping
250
+ */
251
+ public boolean getIsLooping() {
252
+ return isLooping;
253
+ }
254
+
255
+ /**
256
+ * Set whether the video should loop
257
+ *
258
+ * @param value whether the video should loop
259
+ */
260
+ public void setIsLooping(boolean value) {
261
+ isLooping = value;
262
+ mainHandler.post(() -> {
263
+ if (!released && player != null) {
264
+ player.setRepeatMode(value ? Player.REPEAT_MODE_ALL : Player.REPEAT_MODE_OFF);
265
+ }
266
+ });
267
+ }
268
+
269
+ /**
270
+ * @return the playback speed of the video
271
+ */
272
+ public float getPlaybackSpeed() {
273
+ return playbackSpeed;
274
+ }
275
+
276
+ /**
277
+ * Set the playback speed of the video
278
+ *
279
+ * @param value the playback speed to set (must be greater than 0)
280
+ */
281
+ public void setPlaybackSpeed(float value) {
282
+ playbackSpeed = Math.max(0.1f, value); // Minimum speed of 0.1x
283
+ mainHandler.post(() -> {
284
+ if (!released && player != null) {
285
+ player.setPlaybackParameters(new PlaybackParameters(playbackSpeed, 1.0f));
286
+ }
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Decode the next frame of the video
292
+ *
293
+ * @return whether the frame was decoded successfully
294
+ */
295
+ public VideoFrame decodeNextFrame() {
296
+ if (Looper.myLooper() != Looper.getMainLooper()) {
297
+ throw new RuntimeException("decodeNextFrame should be called on UI Thread");
298
+ }
299
+ if (eglResourcesHolder == null || glFrameExtractor == null) {
300
+ return null;
301
+ }
302
+ if (!glFrameExtractor.hasPendingFrame()) {
303
+ // Polled at every vsync, including while paused: skip the EGL context
304
+ // switch, the expensive part of this method, when nothing is new.
305
+ return null;
306
+ }
307
+ boolean downscale = outputWidth > 0 && outputHeight > 0;
308
+ int width = downscale ? outputWidth : videoWidth;
309
+ int height = downscale ? outputHeight : videoHeight;
310
+ if (width > 0 && height > 0) {
311
+ return eglResourcesHolder.withContextCurrent(() ->
312
+ glFrameExtractor.decodeNextFrame(width, height) ? glFrameExtractor.snapshotFrame(0) : null);
313
+ }
314
+ return null;
315
+ }
316
+
317
+ /**
318
+ * Release the video player and its resources
319
+ */
320
+ public void release() {
321
+ if (released) {
322
+ return;
323
+ }
324
+ released = true;
325
+ mainHandler.removeCallbacksAndMessages(null);
326
+ // The GL objects were created (setupGL) and are used (decodeNextFrame) on
327
+ // the main thread, with the extractor's EGL context current. dispose()
328
+ // reaches here from the JS thread, where no context is bound: GL deletes
329
+ // issued there are silently ignored and the textures, shared with the
330
+ // Skia context, leak. Tear everything down on the main thread instead,
331
+ // with the right context current, once the player stopped rendering into
332
+ // the surface.
333
+ mainHandler.post(() -> {
334
+ if (player != null) {
335
+ player.stop();
336
+ player.release();
337
+ player = null;
338
+ }
339
+ if (glFrameExtractor != null) {
340
+ GLFrameExtractor extractor = glFrameExtractor;
341
+ glFrameExtractor = null;
342
+ if (eglResourcesHolder != null) {
343
+ eglResourcesHolder.runWithContextCurrent(extractor::release);
344
+ } else {
345
+ extractor.release();
346
+ }
347
+ }
348
+ if (eglResourcesHolder != null) {
349
+ eglResourcesHolder.release();
350
+ eglResourcesHolder = null;
351
+ }
352
+ });
353
+ }
354
+
355
+ private void dispatchEventIfNoReleased(String eventName, Object value) {
356
+ if (released) {
357
+ return;
358
+ }
359
+ eventDispatcher.dispatchEvent(eventName, value);
360
+ }
361
+ }
@@ -0,0 +1,54 @@
1
+ #pragma once
2
+
3
+ #include <cmath>
4
+ #include <cstddef>
5
+ #include <cstdint>
6
+ #include <limits>
7
+ #include <stdexcept>
8
+ #include <string>
9
+
10
+ namespace RNSkiaVideo {
11
+
12
+ inline size_t checkedStorageInteger(double value, const char* label) {
13
+ // The JS boundary must stay in the exactly representable integer range;
14
+ // comparing to SIZE_MAX as a double alone rounds that limit up on 64-bit.
15
+ if (!std::isfinite(value) || value < 0 || std::floor(value) != value ||
16
+ value > 9007199254740991.0 ||
17
+ (sizeof(size_t) < 8 && value > static_cast<double>(std::numeric_limits<size_t>::max()))) {
18
+ throw std::invalid_argument(std::string("Invalid ") + label);
19
+ }
20
+ return static_cast<size_t>(value);
21
+ }
22
+
23
+ inline int checkedPositiveInt(double value, const char* label) {
24
+ if (!std::isfinite(value) || value <= 0 || std::floor(value) != value ||
25
+ value > std::numeric_limits<int>::max()) {
26
+ throw std::invalid_argument(std::string("Invalid ") + label);
27
+ }
28
+ return static_cast<int>(value);
29
+ }
30
+
31
+ inline size_t checkedSizeProduct(size_t left, size_t right, const char* label) {
32
+ if (right && left > std::numeric_limits<size_t>::max() / right) {
33
+ throw std::invalid_argument(std::string("Invalid ") + label + ": size overflow");
34
+ }
35
+ return left * right;
36
+ }
37
+
38
+ inline size_t checkedPixelStorage(int width, int height, size_t rowBytes) {
39
+ if (width <= 0 || height <= 0 ||
40
+ rowBytes < checkedSizeProduct(static_cast<size_t>(width), 4, "pixel stride")) {
41
+ throw std::invalid_argument("Invalid pixel dimensions/stride");
42
+ }
43
+ return checkedSizeProduct(rowBytes, static_cast<size_t>(height), "pixel storage");
44
+ }
45
+
46
+ inline double checkedMediaSeconds(double seconds) {
47
+ // All native decode/encode boundaries use a nanosecond CMTime timescale.
48
+ if (!std::isfinite(seconds) || seconds < 0 ||
49
+ seconds >= static_cast<double>(std::numeric_limits<int64_t>::max()) / 1e9) {
50
+ throw std::invalid_argument("Invalid media seconds");
51
+ }
52
+ return seconds;
53
+ }
54
+ } // namespace RNSkiaVideo
@@ -0,0 +1,199 @@
1
+ #include "RNSVEventEmitter.h"
2
+ #include "RNSVRuntimeLifecycleMonitor.h"
3
+
4
+ #include <map>
5
+ #include <mutex>
6
+ #include <stdexcept>
7
+ #include <utility>
8
+ #include <vector>
9
+
10
+ namespace RNSkiaVideo {
11
+
12
+ // Native producers and queued callbacks hold no HostObject pointer. JS handles
13
+ // live in this state until they can be released on the owning JS thread.
14
+ struct EventEmitter::State : RuntimeLifecycleListener {
15
+ using ListenerMap = std::map<std::string,
16
+ std::map<uint64_t, std::shared_ptr<jsi::Function>>>;
17
+ std::mutex mutex;
18
+ jsi::Runtime* runtime;
19
+ bool closed = false;
20
+ uint64_t nextId = 1;
21
+ uint64_t generation = 0;
22
+ ListenerMap listeners;
23
+ std::vector<ListenerMap> retiredListeners;
24
+ std::map<uint64_t, jsi::Value> payloads;
25
+
26
+ explicit State(jsi::Runtime& runtime) : runtime(&runtime) {}
27
+
28
+ void onRuntimeDestroyed(jsi::Runtime* destroyedRuntime) override {
29
+ ListenerMap releasedListeners;
30
+ std::vector<ListenerMap> releasedRetired;
31
+ std::map<uint64_t, jsi::Value> releasedPayloads;
32
+ {
33
+ std::lock_guard<std::mutex> lock(mutex);
34
+ if (runtime != destroyedRuntime) return;
35
+ closed = true;
36
+ ++generation;
37
+ releasedListeners = std::move(listeners);
38
+ releasedRetired = std::move(retiredListeners);
39
+ releasedPayloads = std::move(payloads);
40
+ runtime = nullptr;
41
+ }
42
+ // Releasing a JS handle can destroy a native HostObject which calls back
43
+ // into this state. Release handles outside its mutex to allow reentry.
44
+ }
45
+ };
46
+
47
+ EventEmitter::EventEmitter(jsi::Runtime& runtime,
48
+ std::shared_ptr<react::CallInvoker> callInvoker)
49
+ : state(std::make_shared<State>(runtime)), callInvoker(std::move(callInvoker)) {
50
+ RuntimeLifecycleMonitor::addListener(runtime, state);
51
+ }
52
+
53
+ EventEmitter::~EventEmitter() {
54
+ retireListeners(true);
55
+ }
56
+
57
+ jsi::Function EventEmitter::on(std::string eventName, jsi::Function listener) {
58
+ std::lock_guard<std::mutex> lock(state->mutex);
59
+ if (!state->runtime || state->closed) {
60
+ throw std::runtime_error("Video event source is no longer alive");
61
+ }
62
+ const auto id = state->nextId++;
63
+ state->listeners[eventName].emplace(
64
+ id, std::make_shared<jsi::Function>(std::move(listener)));
65
+ std::weak_ptr<State> weakState = state;
66
+ return jsi::Function::createFromHostFunction(
67
+ *state->runtime, jsi::PropNameID::forAscii(*state->runtime, "dispose"), 0,
68
+ [weakState, eventName = std::move(eventName), id](
69
+ jsi::Runtime&, const jsi::Value&, const jsi::Value*, size_t) {
70
+ if (auto retained = weakState.lock()) {
71
+ std::shared_ptr<jsi::Function> released;
72
+ {
73
+ std::lock_guard<std::mutex> lock(retained->mutex);
74
+ auto entry = retained->listeners.find(eventName);
75
+ if (entry != retained->listeners.end()) {
76
+ auto found = entry->second.find(id);
77
+ if (found != entry->second.end()) {
78
+ released = std::move(found->second);
79
+ entry->second.erase(found);
80
+ }
81
+ if (entry->second.empty()) retained->listeners.erase(entry);
82
+ }
83
+ }
84
+ }
85
+ return jsi::Value::undefined();
86
+ });
87
+ }
88
+
89
+ void EventEmitter::emit(std::string eventName) {
90
+ emit(std::move(eventName), [](jsi::Runtime&) {
91
+ return jsi::Value::undefined();
92
+ });
93
+ }
94
+
95
+ void EventEmitter::emit(std::string eventName, jsi::Value data) {
96
+ uint64_t id;
97
+ {
98
+ std::lock_guard<std::mutex> lock(state->mutex);
99
+ if (state->closed || !state->runtime) return;
100
+ id = state->nextId++;
101
+ state->payloads.emplace(id, std::move(data));
102
+ }
103
+ enqueue(std::move(eventName), {}, id);
104
+ }
105
+
106
+ void EventEmitter::emit(std::string eventName,
107
+ std::function<jsi::Value(jsi::Runtime&)> dataFactory) {
108
+ enqueue(std::move(eventName), std::move(dataFactory));
109
+ }
110
+
111
+ void EventEmitter::enqueue(
112
+ std::string eventName,
113
+ std::function<jsi::Value(jsi::Runtime&)> dataFactory, uint64_t payloadId) {
114
+ uint64_t generation;
115
+ {
116
+ std::lock_guard<std::mutex> lock(state->mutex);
117
+ if (state->closed || !state->runtime) return;
118
+ generation = state->generation;
119
+ }
120
+ std::weak_ptr<State> weakState = state;
121
+ callInvoker->invokeAsync([weakState, eventName = std::move(eventName),
122
+ dataFactory = std::move(dataFactory), payloadId,
123
+ generation]() {
124
+ auto retained = weakState.lock();
125
+ if (!retained) return;
126
+ jsi::Runtime* runtime;
127
+ std::vector<std::shared_ptr<jsi::Function>> listeners;
128
+ std::vector<State::ListenerMap> retiredListeners;
129
+ std::map<uint64_t, jsi::Value>::node_type stalePayload;
130
+ jsi::Value data;
131
+ {
132
+ std::lock_guard<std::mutex> lock(retained->mutex);
133
+ runtime = retained->runtime;
134
+ retiredListeners = std::move(retained->retiredListeners);
135
+ if (!runtime || retained->closed || retained->generation != generation) {
136
+ stalePayload = retained->payloads.extract(payloadId);
137
+ return;
138
+ }
139
+ auto entry = retained->listeners.find(eventName);
140
+ if (entry != retained->listeners.end()) {
141
+ for (const auto& listener : entry->second) listeners.push_back(listener.second);
142
+ }
143
+ if (payloadId) {
144
+ auto payload = retained->payloads.find(payloadId);
145
+ if (payload != retained->payloads.end()) {
146
+ data = std::move(payload->second);
147
+ retained->payloads.erase(payload);
148
+ }
149
+ }
150
+ }
151
+ // Listener calls may unsubscribe or dispose the source; never hold the
152
+ // state mutex across JS execution. Factories only run on the JS thread.
153
+ if (listeners.empty()) return;
154
+ if (dataFactory) data = dataFactory(*runtime);
155
+ for (const auto& listener : listeners) {
156
+ {
157
+ std::lock_guard<std::mutex> lock(retained->mutex);
158
+ if (retained->closed || retained->generation != generation) break;
159
+ }
160
+ listener->call(*runtime, data);
161
+ }
162
+ });
163
+ }
164
+
165
+ jsi::Runtime* EventEmitter::getRuntime() {
166
+ std::lock_guard<std::mutex> lock(state->mutex);
167
+ return state->closed ? nullptr : state->runtime;
168
+ }
169
+
170
+ void EventEmitter::retireListeners(bool close) {
171
+ auto retained = state;
172
+ {
173
+ std::lock_guard<std::mutex> lock(retained->mutex);
174
+ if (!retained->runtime) return;
175
+ retained->closed |= close;
176
+ ++retained->generation;
177
+ if (!retained->listeners.empty()) {
178
+ retained->retiredListeners.push_back(std::move(retained->listeners));
179
+ retained->listeners.clear();
180
+ }
181
+ }
182
+ // A queued cleanup retains the state, not the HostObject. Runtime teardown
183
+ // also clears it, so a dropped callback cannot leave dangling JS handles.
184
+ callInvoker->invokeAsync([retained]() {
185
+ std::vector<State::ListenerMap> listeners;
186
+ std::map<uint64_t, jsi::Value> payloads;
187
+ {
188
+ std::lock_guard<std::mutex> lock(retained->mutex);
189
+ listeners = std::move(retained->retiredListeners);
190
+ if (retained->closed) payloads = std::move(retained->payloads);
191
+ }
192
+ });
193
+ }
194
+
195
+ void EventEmitter::removeAllListeners() {
196
+ retireListeners(false);
197
+ }
198
+
199
+ } // namespace RNSkiaVideo
@@ -0,0 +1,31 @@
1
+ #pragma once
2
+
3
+ #include <ReactCommon/CallInvoker.h>
4
+ #include <jsi/jsi.h>
5
+ #include <memory>
6
+
7
+ namespace RNSkiaVideo {
8
+ using namespace facebook;
9
+ class EventEmitter {
10
+ public:
11
+ EventEmitter(jsi::Runtime& runtime,
12
+ std::shared_ptr<react::CallInvoker> callInvoker);
13
+ ~EventEmitter();
14
+ jsi::Function on(std::string eventName, jsi::Function listener);
15
+ void emit(std::string eventName);
16
+ void emit(std::string eventName, jsi::Value data);
17
+ void emit(std::string eventName,
18
+ std::function<jsi::Value(jsi::Runtime&)> dataFactory);
19
+ void removeAllListeners();
20
+ jsi::Runtime* getRuntime();
21
+
22
+ private:
23
+ struct State;
24
+ std::shared_ptr<State> state;
25
+ std::shared_ptr<react::CallInvoker> callInvoker;
26
+ void enqueue(std::string eventName,
27
+ std::function<jsi::Value(jsi::Runtime&)> dataFactory,
28
+ uint64_t payloadId = 0);
29
+ void retireListeners(bool close);
30
+ };
31
+ } // namespace RNSkiaVideo