react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.1

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 (237) 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/DisplayVsyncPeriod.java +72 -0
  48. package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
  49. package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
  50. package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
  51. package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
  52. package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
  53. package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
  54. package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
  55. package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +43 -0
  56. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
  57. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
  58. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
  59. package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +114 -0
  60. package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
  61. package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
  62. package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
  63. package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
  64. package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
  65. package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
  66. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +779 -0
  67. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +463 -0
  68. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
  69. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
  70. package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
  71. package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
  72. package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
  73. package/cpp/RNSVCheckedSizes.h +54 -0
  74. package/cpp/RNSVEventEmitter.cpp +199 -0
  75. package/cpp/RNSVEventEmitter.h +31 -0
  76. package/cpp/RNSVHostObject.cpp +79 -0
  77. package/cpp/RNSVHostObject.h +67 -0
  78. package/cpp/RNSVMemoryBudget.cpp +68 -0
  79. package/cpp/RNSVMemoryBudget.h +100 -0
  80. package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
  81. package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
  82. package/docs/ANDROID_DEVICE_306.md +95 -0
  83. package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
  84. package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
  85. package/docs/BENCHMARKS.md +368 -0
  86. package/docs/IOS_DEVICE_TESTS.md +115 -0
  87. package/docs/IOS_SIMULATOR_TESTS.md +74 -0
  88. package/docs/MIGRATION.md +82 -0
  89. package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
  90. package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
  91. package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
  92. package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
  93. package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
  94. package/docs/PERFORMANCE_SIMULATOR.md +89 -0
  95. package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
  96. package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
  97. package/docs/UPSTREAM_NOTES.md +188 -0
  98. package/docs/assets/banner.svg +26 -0
  99. package/examples/webgpu-overlay.ts +147 -0
  100. package/ios/AVAssetTrackUtils.h +24 -0
  101. package/ios/AudioCompositionUtils.h +27 -0
  102. package/ios/AudioCompositionUtils.mm +113 -0
  103. package/ios/DecoderWindow.h +52 -0
  104. package/ios/NativeResourceStats.h +32 -0
  105. package/ios/RNSVColorSpace.h +50 -0
  106. package/ios/RNSVJSIUtils.h +46 -0
  107. package/ios/RNSVScopedPixelBuffer.h +17 -0
  108. package/ios/RNSVVideoPlayer.h +46 -0
  109. package/ios/RNSVVideoPlayer.mm +410 -0
  110. package/ios/ReactNativeSkiaVideo.h +8 -0
  111. package/ios/ReactNativeSkiaVideo.mm +310 -0
  112. package/ios/SeekPolicy.h +27 -0
  113. package/ios/VideoComposition.h +148 -0
  114. package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
  115. package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
  116. package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
  117. package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
  118. package/ios/VideoCompositionItemDecoder.h +60 -0
  119. package/ios/VideoCompositionItemDecoder.mm +480 -0
  120. package/ios/VideoEncoderHostObject.h +83 -0
  121. package/ios/VideoEncoderHostObject.mm +706 -0
  122. package/ios/VideoFrame.h +66 -0
  123. package/ios/VideoFrame.mm +108 -0
  124. package/ios/VideoPlayerHostObject.h +52 -0
  125. package/ios/VideoPlayerHostObject.mm +332 -0
  126. package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
  127. package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
  128. package/lib/module/RNSkiaVideoModule.js +20 -0
  129. package/lib/module/RNSkiaVideoModule.js.map +1 -0
  130. package/lib/module/canvas.js +18 -0
  131. package/lib/module/canvas.js.map +1 -0
  132. package/lib/module/exportCache.js +22 -0
  133. package/lib/module/exportCache.js.map +1 -0
  134. package/lib/module/exportFrames.js +338 -0
  135. package/lib/module/exportFrames.js.map +1 -0
  136. package/lib/module/exportQueue.js +52 -0
  137. package/lib/module/exportQueue.js.map +1 -0
  138. package/lib/module/exportTarget.js +137 -0
  139. package/lib/module/exportTarget.js.map +1 -0
  140. package/lib/module/exportVideoComposition.js +492 -0
  141. package/lib/module/exportVideoComposition.js.map +1 -0
  142. package/lib/module/frameInterop.js +532 -0
  143. package/lib/module/frameInterop.js.map +1 -0
  144. package/lib/module/gpu.js +222 -0
  145. package/lib/module/gpu.js.map +1 -0
  146. package/lib/module/gpuDevice.js +54 -0
  147. package/lib/module/gpuDevice.js.map +1 -0
  148. package/lib/module/index.js +36 -0
  149. package/lib/module/index.js.map +1 -0
  150. package/lib/module/memory.js +38 -0
  151. package/lib/module/memory.js.map +1 -0
  152. package/lib/module/package.json +1 -0
  153. package/lib/module/previewLifecycle.js +52 -0
  154. package/lib/module/previewLifecycle.js.map +1 -0
  155. package/lib/module/runtimeYield.js +10 -0
  156. package/lib/module/runtimeYield.js.map +1 -0
  157. package/lib/module/types.js +4 -0
  158. package/lib/module/types.js.map +1 -0
  159. package/lib/module/utils/useEventListener.js +18 -0
  160. package/lib/module/utils/useEventListener.js.map +1 -0
  161. package/lib/module/videoCompositionPlayer.js +368 -0
  162. package/lib/module/videoCompositionPlayer.js.map +1 -0
  163. package/lib/module/videoFrameImage.js +185 -0
  164. package/lib/module/videoFrameImage.js.map +1 -0
  165. package/lib/module/videoPlayer.js +172 -0
  166. package/lib/module/videoPlayer.js.map +1 -0
  167. package/lib/module/yuvBlit.js +299 -0
  168. package/lib/module/yuvBlit.js.map +1 -0
  169. package/lib/typescript/package.json +1 -0
  170. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
  171. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
  172. package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
  173. package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
  174. package/lib/typescript/src/canvas.d.ts +4 -0
  175. package/lib/typescript/src/canvas.d.ts.map +1 -0
  176. package/lib/typescript/src/exportCache.d.ts +6 -0
  177. package/lib/typescript/src/exportCache.d.ts.map +1 -0
  178. package/lib/typescript/src/exportFrames.d.ts +57 -0
  179. package/lib/typescript/src/exportFrames.d.ts.map +1 -0
  180. package/lib/typescript/src/exportQueue.d.ts +5 -0
  181. package/lib/typescript/src/exportQueue.d.ts.map +1 -0
  182. package/lib/typescript/src/exportTarget.d.ts +28 -0
  183. package/lib/typescript/src/exportTarget.d.ts.map +1 -0
  184. package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
  185. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
  186. package/lib/typescript/src/frameInterop.d.ts +17 -0
  187. package/lib/typescript/src/frameInterop.d.ts.map +1 -0
  188. package/lib/typescript/src/gpu.d.ts +39 -0
  189. package/lib/typescript/src/gpu.d.ts.map +1 -0
  190. package/lib/typescript/src/gpuDevice.d.ts +11 -0
  191. package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
  192. package/lib/typescript/src/index.d.ts +21 -0
  193. package/lib/typescript/src/index.d.ts.map +1 -0
  194. package/lib/typescript/src/memory.d.ts +25 -0
  195. package/lib/typescript/src/memory.d.ts.map +1 -0
  196. package/lib/typescript/src/previewLifecycle.d.ts +22 -0
  197. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
  198. package/lib/typescript/src/runtimeYield.d.ts +4 -0
  199. package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
  200. package/lib/typescript/src/types.d.ts +631 -0
  201. package/lib/typescript/src/types.d.ts.map +1 -0
  202. package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
  203. package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
  204. package/lib/typescript/src/videoCompositionPlayer.d.ts +105 -0
  205. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
  206. package/lib/typescript/src/videoFrameImage.d.ts +60 -0
  207. package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
  208. package/lib/typescript/src/videoPlayer.d.ts +99 -0
  209. package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
  210. package/lib/typescript/src/yuvBlit.d.ts +50 -0
  211. package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
  212. package/package.json +155 -4
  213. package/patches/react-native-skia+3.7.0.patch +502 -0
  214. package/react-native-skia-webgpu-video.podspec +35 -0
  215. package/scripts/apply-skia-canvas-patch.mjs +48 -0
  216. package/scripts/skia-canvas-patch.json +291 -0
  217. package/src/NativeReactNativeSkiaVideo.ts +8 -0
  218. package/src/RNSkiaVideoModule.ts +27 -0
  219. package/src/canvas.ts +22 -0
  220. package/src/exportCache.ts +27 -0
  221. package/src/exportFrames.ts +421 -0
  222. package/src/exportQueue.ts +65 -0
  223. package/src/exportTarget.ts +179 -0
  224. package/src/exportVideoComposition.ts +718 -0
  225. package/src/frameInterop.ts +684 -0
  226. package/src/gpu.ts +290 -0
  227. package/src/gpuDevice.ts +80 -0
  228. package/src/index.tsx +45 -0
  229. package/src/memory.ts +48 -0
  230. package/src/previewLifecycle.ts +60 -0
  231. package/src/runtimeYield.ts +6 -0
  232. package/src/types.ts +661 -0
  233. package/src/utils/useEventListener.ts +22 -0
  234. package/src/videoCompositionPlayer.ts +568 -0
  235. package/src/videoFrameImage.ts +205 -0
  236. package/src/videoPlayer.ts +318 -0
  237. package/src/yuvBlit.ts +331 -0
@@ -0,0 +1,639 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import android.graphics.Bitmap;
4
+ import android.media.MediaCodec;
5
+ import android.media.MediaCodecInfo;
6
+ import android.media.MediaCodecList;
7
+ import android.media.MediaFormat;
8
+ import android.media.MediaMuxer;
9
+ import android.opengl.GLES20;
10
+ import android.os.SystemClock;
11
+ import android.util.Log;
12
+ import android.view.Surface;
13
+
14
+ import java.io.IOException;
15
+ import java.nio.ByteBuffer;
16
+ import java.util.ArrayList;
17
+ import java.util.List;
18
+
19
+ import javax.microedition.khronos.egl.EGL10;
20
+ import javax.microedition.khronos.egl.EGLContext;
21
+
22
+
23
+ /**
24
+ * Helper class for encoding video (and the audio of the composition items,
25
+ * if any).
26
+ */
27
+ public class VideoEncoder {
28
+
29
+ private static final String TAG = "VideoEncoder";
30
+
31
+ public static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding
32
+
33
+ public static final String MIME_TYPE_HEVC = "video/hevc"; // H.265 High Efficiency Video Coding
34
+
35
+ public static final int DEFAULT_I_FRAME_INTERVAL_SECONDS = 1;
36
+
37
+ /**
38
+ * Maps a codec name from the JS API onto its Android mime type, falling back
39
+ * to H.264 for anything the device cannot encode — including an unknown name.
40
+ * H.264 is mandated by the platform, so the fallback is always available.
41
+ *
42
+ * @param codec the codec name, "h264" or "hevc"; null means H.264
43
+ * @return the mime type to encode with
44
+ */
45
+ public static String resolveMimeType(String codec) {
46
+ if ("hevc".equals(codec) && isCodecSupported("hevc")) {
47
+ return MIME_TYPE_HEVC;
48
+ }
49
+ return MIME_TYPE;
50
+ }
51
+
52
+ /**
53
+ * Whether the device has an encoder for the given codec.
54
+ *
55
+ * HEVC decoding is far more common than HEVC encoding on Android, so this
56
+ * asks specifically for encoders rather than for codec support at large.
57
+ *
58
+ * @param codec the codec name, "h264" or "hevc"
59
+ * @return true if an encoder exists for it
60
+ */
61
+ public static boolean isCodecSupported(String codec) {
62
+ String mimeType;
63
+ if ("h264".equals(codec)) {
64
+ mimeType = MIME_TYPE;
65
+ } else if ("hevc".equals(codec)) {
66
+ mimeType = MIME_TYPE_HEVC;
67
+ } else {
68
+ return false;
69
+ }
70
+ MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
71
+ for (MediaCodecInfo info : codecList.getCodecInfos()) {
72
+ if (!info.isEncoder()) {
73
+ continue;
74
+ }
75
+ for (String supported : info.getSupportedTypes()) {
76
+ if (supported.equalsIgnoreCase(mimeType)) {
77
+ return true;
78
+ }
79
+ }
80
+ }
81
+ return false;
82
+ }
83
+
84
+ private final String outputPath;
85
+
86
+ private final int width;
87
+
88
+ private final int height;
89
+
90
+ private final int frameRate;
91
+
92
+ private final int bitRate;
93
+
94
+ private final String encoderName;
95
+
96
+ private final String mimeType;
97
+
98
+ private final VideoComposition composition;
99
+
100
+ private final int audioSampleRate;
101
+
102
+ private final int audioChannelCount;
103
+
104
+ private final int audioBitRate;
105
+
106
+ private final boolean hasAudio;
107
+
108
+ private MediaCodec encoder;
109
+
110
+ private Surface inputSurface;
111
+
112
+ private EGLResourcesHolder eglResourcesHolder;
113
+
114
+ private TextureRenderer textureRenderer;
115
+
116
+ private int uploadTexture;
117
+ private int uploadWidth;
118
+ private int uploadHeight;
119
+ private ByteBuffer packedUpload;
120
+ private long lastPresentationTimeUs = -1;
121
+ private long submittedFrames;
122
+ private long writtenFrames;
123
+ private static final int MAX_IN_FLIGHT_FRAMES = 4;
124
+ private static final long MAX_PENDING_SAMPLE_BYTES = 16L * 1024 * 1024;
125
+ private long pendingSampleBytes;
126
+
127
+ private MediaMuxer muxer;
128
+
129
+ private int trackIndex;
130
+
131
+ private int audioTrackIndex;
132
+
133
+ private boolean muxerStarted;
134
+
135
+ private final MediaCodec.BufferInfo bufferInfo;
136
+
137
+ private Thread audioThread;
138
+
139
+ private volatile boolean audioCanceled = false;
140
+
141
+ private volatile Exception audioException;
142
+
143
+ private final List<PendingSample> pendingVideoSamples = new ArrayList<>();
144
+
145
+ private final List<PendingSample> pendingAudioSamples = new ArrayList<>();
146
+
147
+ /**
148
+ * Creates a new VideoEncoder.
149
+ *
150
+ * @param outputPath the path to write the encoded video to
151
+ * @param width the width of the video
152
+ * @param height the height of the video
153
+ * @param frameRate the frame rate of the video
154
+ * @param bitRate the bit rate of the video
155
+ * @param encoderName the name of the encoder to use, or null to use the default encoder
156
+ * @param codec the codec to encode with ("h264" or "hevc"), or null for
157
+ * H.264; ignored when encoderName names an encoder, and
158
+ * falls back to H.264 when the device cannot encode it
159
+ * @param composition the composition being exported, used to encode the
160
+ * audio of its audio-enabled items; can be null
161
+ * @param audioSampleRate the sample rate of the exported audio track
162
+ * @param audioChannelCount the number of channels of the exported audio track
163
+ * @param audioBitRate the bit rate of the exported audio track
164
+ */
165
+ public VideoEncoder(
166
+ String outputPath,
167
+ int width,
168
+ int height,
169
+ int frameRate,
170
+ int bitRate,
171
+ String encoderName,
172
+ String codec,
173
+ VideoComposition composition,
174
+ int audioSampleRate,
175
+ int audioChannelCount,
176
+ int audioBitRate
177
+ ) {
178
+ this.outputPath = outputPath;
179
+ this.width = width;
180
+ this.height = height;
181
+ this.frameRate = frameRate;
182
+ this.bitRate = bitRate;
183
+ this.encoderName = encoderName;
184
+ // Resolved once here so the rest of the object works with a mime type the
185
+ // device is known to have an encoder for.
186
+ this.mimeType = resolveMimeType(codec);
187
+ this.composition = composition;
188
+ this.audioSampleRate = audioSampleRate;
189
+ this.audioChannelCount = audioChannelCount;
190
+ this.audioBitRate = audioBitRate;
191
+ this.hasAudio = composition != null && composition.hasAudio();
192
+ bufferInfo = new MediaCodec.BufferInfo();
193
+ }
194
+
195
+ /**
196
+ * Configures encoder and muxer state, and prepares the input Surface.
197
+ */
198
+ public void prepare() throws IOException {
199
+ if (encoder != null) throw new IllegalStateException("Encoder already prepared");
200
+ encoder = encoderName != null
201
+ ? MediaCodec.createByCodecName(encoderName)
202
+ : MediaCodec.createEncoderByType(mimeType);
203
+
204
+ MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);
205
+ format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
206
+ MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
207
+ format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
208
+ format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
209
+ format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL_SECONDS);
210
+
211
+ encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
212
+
213
+ inputSurface = encoder.createInputSurface();
214
+ eglResourcesHolder = EGLResourcesHolder.createWithWindowedSurface(EGL10.EGL_NO_CONTEXT, inputSurface);
215
+ eglResourcesHolder.runWithContextCurrent(() -> {
216
+ textureRenderer = new TextureRenderer();
217
+ int[] textures = new int[1];
218
+ GLES20.glGenTextures(1, textures, 0);
219
+ uploadTexture = textures[0];
220
+ EGLUtils.configureTexture(GLES20.GL_TEXTURE_2D, uploadTexture);
221
+ });
222
+ encoder.start();
223
+
224
+ try {
225
+ muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
226
+ } catch (IOException ioe) {
227
+ throw new RuntimeException("MediaMuxer creation failed", ioe);
228
+ }
229
+
230
+ trackIndex = -1;
231
+ audioTrackIndex = -1;
232
+ muxerStarted = false;
233
+
234
+ if (hasAudio) {
235
+ AudioCompositionExporter audioExporter = new AudioCompositionExporter(
236
+ composition,
237
+ audioSampleRate,
238
+ audioChannelCount,
239
+ audioBitRate,
240
+ new AudioCompositionExporter.Sink() {
241
+ @Override
242
+ public void onAudioFormat(MediaFormat format) {
243
+ synchronized (VideoEncoder.this) {
244
+ audioTrackIndex = muxer.addTrack(format);
245
+ maybeStartMuxer();
246
+ }
247
+ }
248
+
249
+ @Override
250
+ public void onAudioSample(ByteBuffer buffer, MediaCodec.BufferInfo info) {
251
+ writeOrQueueSample(false, buffer, info);
252
+ }
253
+ },
254
+ () -> audioCanceled
255
+ );
256
+ audioThread = new Thread(() -> {
257
+ try {
258
+ audioExporter.run();
259
+ } catch (Exception e) {
260
+ audioException = e;
261
+ }
262
+ }, "ReactNativeSkiaVideo-AudioExportThread");
263
+ audioThread.start();
264
+ }
265
+ }
266
+
267
+ public void makeGLContextCurrent() {
268
+ eglResourcesHolder.makeCurrent();
269
+ }
270
+
271
+ /** Uploads owned pixels into this encoder's private context, without retaining the input buffer. */
272
+ public void encodeFrameRgba(ByteBuffer rgba, int sourceWidth, int sourceHeight,
273
+ int bytesPerRow, double time) {
274
+ int required = RgbaLayout.requiredBytes(sourceWidth, sourceHeight, bytesPerRow);
275
+ if (rgba == null || rgba.remaining() < required || !Double.isFinite(time) || time < 0) {
276
+ throw new IllegalArgumentException("Invalid encoder RGBA frame or timestamp");
277
+ }
278
+ if (encoder == null || eglResourcesHolder == null) {
279
+ throw new IllegalStateException("Encoder has not been prepared");
280
+ }
281
+ awaitEncoderCapacity();
282
+ ByteBuffer source = rgba.duplicate();
283
+ if (bytesPerRow != RgbaLayout.bytesPerRow(sourceWidth)) {
284
+ int packedSize = RgbaLayout.byteSize(sourceWidth, sourceHeight);
285
+ if (packedUpload == null || packedUpload.capacity() != packedSize) {
286
+ packedUpload = ByteBuffer.allocateDirect(packedSize);
287
+ }
288
+ packedUpload.clear();
289
+ int rowSize = RgbaLayout.bytesPerRow(sourceWidth);
290
+ int start = source.position();
291
+ for (int row = 0; row < sourceHeight; row++) {
292
+ source.limit(start + row * bytesPerRow + rowSize);
293
+ source.position(start + row * bytesPerRow);
294
+ packedUpload.put(source);
295
+ }
296
+ packedUpload.flip();
297
+ source = packedUpload;
298
+ }
299
+ final ByteBuffer upload = source;
300
+ eglResourcesHolder.runWithContextCurrent(() -> {
301
+ GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
302
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, uploadTexture);
303
+ if (sourceWidth != uploadWidth || sourceHeight != uploadHeight) {
304
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA,
305
+ sourceWidth, sourceHeight, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, upload);
306
+ uploadWidth = sourceWidth;
307
+ uploadHeight = sourceHeight;
308
+ } else {
309
+ GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, sourceWidth, sourceHeight,
310
+ GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, upload);
311
+ }
312
+ EGLUtils.checkGlError("encoder RGBA upload");
313
+ encodeTextureCurrent(uploadTexture, time);
314
+ });
315
+ }
316
+
317
+ /**
318
+ * Encodes a GL_TEXTURE_2D of this encoder's context: a hardware buffer the
319
+ * export rendered into through WebGPU, imported as an EGLImage natively.
320
+ * The caller waited for the renderer's fence and keeps the buffer until the
321
+ * reads this draw queues are complete.
322
+ */
323
+ public void encodeTexture(int texture, double time) {
324
+ if (texture <= 0 || !Double.isFinite(time) || time < 0) {
325
+ throw new IllegalArgumentException("Invalid encoder texture or timestamp");
326
+ }
327
+ if (encoder == null || eglResourcesHolder == null) {
328
+ throw new IllegalStateException("Encoder has not been prepared");
329
+ }
330
+ awaitEncoderCapacity();
331
+ eglResourcesHolder.runWithContextCurrent(() -> encodeTextureCurrent(texture, time));
332
+ }
333
+
334
+ private void awaitEncoderCapacity() {
335
+ drainEncoder(false);
336
+ long deadline = SystemClock.uptimeMillis() + 15_000;
337
+ while (submittedFrames - writtenFrames >= MAX_IN_FLIGHT_FRAMES) {
338
+ if (audioCanceled || SystemClock.uptimeMillis() > deadline) {
339
+ throw new IllegalStateException("Video encoder did not make progress within its bounded queue");
340
+ }
341
+ drainEncoder(false, 10_000);
342
+ }
343
+ }
344
+
345
+ private void encodeTextureCurrent(int texture, double time) {
346
+ // Fail fast if the audio pipeline died: the muxer cannot start without
347
+ // the audio track and every video sample would pile up in
348
+ // pendingVideoSamples until the end of the export.
349
+ if (audioException != null) {
350
+ throw new RuntimeException("Could not encode composition audio", audioException);
351
+ }
352
+ long timeUS = TimeHelpers.secToUs(time);
353
+ if (timeUS <= lastPresentationTimeUs) {
354
+ throw new IllegalArgumentException("Encoder presentation timestamps must increase");
355
+ }
356
+ GLES20.glClearColor(0, 0, 0, 0);
357
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
358
+ GLES20.glViewport(0, 0, width, height);
359
+ textureRenderer.draw(texture, EGLUtils.IDENTITY_MATRIX);
360
+ eglResourcesHolder.setPresentationTime(timeUS * 1000);
361
+ if (!eglResourcesHolder.swapBuffers()) {
362
+ throw new RuntimeException("eglSwapBuffer failed");
363
+ }
364
+ lastPresentationTimeUs = timeUS;
365
+ submittedFrames++;
366
+ // Upload and sampling are ordered in the same private context. No shared
367
+ // Skia texture can be overwritten while this draw is queued.
368
+ drainEncoder(false);
369
+ }
370
+
371
+ public void finishWriting() {
372
+ drainEncoder(true);
373
+ if (audioThread != null) {
374
+ try {
375
+ audioThread.join(30_000);
376
+ if (audioThread.isAlive()) {
377
+ throw new IllegalStateException("Audio encoder did not finish within 30 seconds");
378
+ }
379
+ } catch (InterruptedException e) {
380
+ Thread.currentThread().interrupt();
381
+ throw new RuntimeException("Interrupted while writing audio", e);
382
+ }
383
+ audioThread = null;
384
+ if (audioException != null) {
385
+ throw new RuntimeException("Could not encode composition audio", audioException);
386
+ }
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Starts the muxer once every track has been registered and flushes the
392
+ * queued samples. Must be called with the monitor held.
393
+ */
394
+ private void maybeStartMuxer() {
395
+ if (muxerStarted
396
+ || trackIndex < 0
397
+ || (hasAudio && audioTrackIndex < 0)) {
398
+ return;
399
+ }
400
+ muxer.start();
401
+ muxerStarted = true;
402
+ for (PendingSample sample : pendingVideoSamples) {
403
+ muxer.writeSampleData(trackIndex, sample.buffer, sample.bufferInfo);
404
+ }
405
+ pendingVideoSamples.clear();
406
+ for (PendingSample sample : pendingAudioSamples) {
407
+ muxer.writeSampleData(audioTrackIndex, sample.buffer, sample.bufferInfo);
408
+ }
409
+ pendingAudioSamples.clear();
410
+ pendingSampleBytes = 0;
411
+ notifyAll();
412
+ }
413
+
414
+ /**
415
+ * Writes a sample to the muxer, or queues it until the muxer has started.
416
+ */
417
+ private synchronized void writeOrQueueSample(
418
+ boolean isVideo,
419
+ ByteBuffer buffer,
420
+ MediaCodec.BufferInfo info
421
+ ) {
422
+ if (muxerStarted) {
423
+ muxer.writeSampleData(isVideo ? trackIndex : audioTrackIndex, buffer, info);
424
+ return;
425
+ }
426
+ if (info.size > MAX_PENDING_SAMPLE_BYTES) {
427
+ throw new IllegalStateException("Encoded sample exceeds the pending muxer budget");
428
+ }
429
+ long deadline = SystemClock.uptimeMillis() + 15_000;
430
+ while (pendingSampleBytes + info.size > MAX_PENDING_SAMPLE_BYTES && !muxerStarted) {
431
+ if (audioCanceled || audioException != null || SystemClock.uptimeMillis() > deadline) {
432
+ throw new IllegalStateException("Muxer tracks did not become ready within the memory budget");
433
+ }
434
+ try {
435
+ wait(100);
436
+ } catch (InterruptedException error) {
437
+ Thread.currentThread().interrupt();
438
+ throw new IllegalStateException("Interrupted while waiting for muxer capacity", error);
439
+ }
440
+ }
441
+ if (muxerStarted) {
442
+ muxer.writeSampleData(isVideo ? trackIndex : audioTrackIndex, buffer, info);
443
+ return;
444
+ }
445
+ ByteBuffer copy = ByteBuffer.allocateDirect(info.size);
446
+ copy.put(buffer);
447
+ copy.flip();
448
+ MediaCodec.BufferInfo infoCopy = new MediaCodec.BufferInfo();
449
+ infoCopy.set(0, info.size, info.presentationTimeUs, info.flags);
450
+ (isVideo ? pendingVideoSamples : pendingAudioSamples)
451
+ .add(new PendingSample(copy, infoCopy));
452
+ pendingSampleBytes += info.size;
453
+ }
454
+
455
+ private static class PendingSample {
456
+ final ByteBuffer buffer;
457
+ final MediaCodec.BufferInfo bufferInfo;
458
+
459
+ PendingSample(ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) {
460
+ this.buffer = buffer;
461
+ this.bufferInfo = bufferInfo;
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Extracts all pending data from the encoder.
467
+ *
468
+ * @param endOfStream true if this is the end of the stream
469
+ */
470
+ private void drainEncoder(boolean endOfStream) {
471
+ // Wait for output only when flushing the stream at the end. During the
472
+ // export the encoder runs a few frames behind its input, so a positive
473
+ // timeout blocks the export thread for that long on every frame whose
474
+ // output is not ready yet; what is not ready now is drained with the next
475
+ // frame.
476
+ drainEncoder(endOfStream, endOfStream ? 10_000 : 0);
477
+ }
478
+
479
+ private void drainEncoder(boolean endOfStream, int timeoutUsec) {
480
+ long deadline = SystemClock.uptimeMillis() + 30_000;
481
+
482
+ if (endOfStream) {
483
+ encoder.signalEndOfInputStream();
484
+ }
485
+
486
+ while (true) {
487
+ if (audioCanceled || SystemClock.uptimeMillis() > deadline) {
488
+ throw new IllegalStateException("Video encoder did not finish within 30 seconds");
489
+ }
490
+ int encoderStatus = encoder.dequeueOutputBuffer(bufferInfo, timeoutUsec);
491
+ if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
492
+ // no output available yet
493
+ if (!endOfStream) {
494
+ break; // out of while
495
+ }
496
+ continue;
497
+ }
498
+ if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
499
+ // should happen before receiving buffers, and should only happen once
500
+ if (trackIndex >= 0) {
501
+ throw new RuntimeException("format changed twice");
502
+ }
503
+ synchronized (this) {
504
+ trackIndex = muxer.addTrack(encoder.getOutputFormat());
505
+ maybeStartMuxer();
506
+ }
507
+ } else if (encoderStatus < 0) {
508
+ Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
509
+ // let's ignore it
510
+ } else {
511
+ ByteBuffer encodedData = encoder.getOutputBuffer(encoderStatus);
512
+ if (encodedData == null) {
513
+ throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
514
+ }
515
+
516
+ if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
517
+ // The codec config data was pulled out and fed to the muxer when we got
518
+ // the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
519
+ bufferInfo.size = 0;
520
+ }
521
+
522
+ if (bufferInfo.size != 0) {
523
+ // adjust the ByteBuffer values to match BufferInfo (not needed?)
524
+ encodedData.position(bufferInfo.offset);
525
+ encodedData.limit(bufferInfo.offset + bufferInfo.size);
526
+
527
+ writeOrQueueSample(true, encodedData, bufferInfo);
528
+ writtenFrames++;
529
+ }
530
+
531
+ encoder.releaseOutputBuffer(encoderStatus, false);
532
+
533
+ if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
534
+ if (!endOfStream) {
535
+ Log.w(TAG, "reached end of stream unexpectedly");
536
+ }
537
+ break; // out of while
538
+ }
539
+ }
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Releases encoder resources. May be called after partial / failed initialization.
545
+ */
546
+ public void release() {
547
+ audioCanceled = true;
548
+ synchronized (this) { notifyAll(); }
549
+ if (audioThread != null) {
550
+ // Stop the audio pipeline before tearing down the muxer.
551
+ audioThread.interrupt();
552
+ boolean interrupted = Thread.interrupted();
553
+ // Never destroy the muxer while its audio worker can still write it.
554
+ while (audioThread.isAlive()) {
555
+ try {
556
+ audioThread.join(1000);
557
+ } catch (InterruptedException e) {
558
+ interrupted = true;
559
+ }
560
+ }
561
+ if (interrupted) Thread.currentThread().interrupt();
562
+ audioThread = null;
563
+ }
564
+ if (eglResourcesHolder != null) {
565
+ if (textureRenderer != null) {
566
+ // The program can only be deleted with the encoder's context current;
567
+ // on the export thread the Skia context is the one bound here.
568
+ eglResourcesHolder.runWithContextCurrent(() -> {
569
+ textureRenderer.release();
570
+ if (uploadTexture != 0) {
571
+ GLES20.glDeleteTextures(1, new int[]{uploadTexture}, 0);
572
+ uploadTexture = 0;
573
+ }
574
+ });
575
+ textureRenderer = null;
576
+ }
577
+ eglResourcesHolder.release();
578
+ eglResourcesHolder = null;
579
+ }
580
+ if (encoder != null) {
581
+ try {
582
+ encoder.stop();
583
+ } catch (IllegalStateException e) {
584
+ // the encoder never started or is in an error state (failed or
585
+ // canceled exports); release() below reclaims it anyway.
586
+ Log.w(TAG, "Could not stop the encoder", e);
587
+ }
588
+ encoder.release();
589
+ encoder = null;
590
+ }
591
+ if (inputSurface != null) {
592
+ inputSurface.release();
593
+ inputSurface = null;
594
+ }
595
+ if (muxer != null) {
596
+ try {
597
+ muxer.stop();
598
+ } catch (IllegalStateException e) {
599
+ // the muxer never started or has no sample (failed exports).
600
+ Log.w(TAG, "Could not stop the muxer", e);
601
+ }
602
+ muxer.release();
603
+ muxer = null;
604
+ }
605
+ // Direct ByteBuffers only reclaim their native memory once the Java
606
+ // object is collected; drop them eagerly.
607
+ synchronized (this) {
608
+ pendingVideoSamples.clear();
609
+ pendingAudioSamples.clear();
610
+ pendingSampleBytes = 0;
611
+ }
612
+ packedUpload = null;
613
+ }
614
+
615
+ public Bitmap saveTexture(int texture, int width, int height) {
616
+ int[] frame = new int[1];
617
+ GLES20.glGenFramebuffers(1, frame, 0);
618
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frame[0]);
619
+ GLES20.glFramebufferTexture2D(
620
+ GLES20.GL_FRAMEBUFFER,
621
+ GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, texture,
622
+ 0
623
+ );
624
+
625
+ ByteBuffer buffer = ByteBuffer.allocate(width * height * 4);
626
+ GLES20.glReadPixels(
627
+ 0, 0, width, height, GLES20.GL_RGBA,
628
+ GLES20.GL_UNSIGNED_BYTE, buffer
629
+ );
630
+
631
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
632
+ bitmap.copyPixelsFromBuffer(buffer);
633
+
634
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
635
+ GLES20.glDeleteFramebuffers(1, frame, 0);
636
+
637
+ return bitmap;
638
+ }
639
+ }
@@ -0,0 +1,74 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import java.nio.ByteBuffer;
4
+ import java.util.concurrent.atomic.AtomicLong;
5
+
6
+ /** An owned RGBA snapshot. No decoder texture or recyclable codec buffer escapes this class. */
7
+ public final class VideoFrame implements AutoCloseable {
8
+ private static final AtomicLong nextId = new AtomicLong(1);
9
+ private final long id;
10
+ private final long producerId;
11
+ private final ByteBuffer pixels;
12
+ private final NativeHardwareBuffer.Lease hardware;
13
+ private final int width;
14
+ private final int height;
15
+ private final int rotation;
16
+ private final long timestampNs;
17
+
18
+ VideoFrame(ByteBuffer ownedPixels, int width, int height, int rotation, long timestampNs,
19
+ long producerId) {
20
+ this.id = nextId.getAndIncrement();
21
+ int required = RgbaLayout.byteSize(width, height);
22
+ if (!ownedPixels.isDirect() || ownedPixels.capacity() != required) {
23
+ throw new IllegalArgumentException("RGBA frame byte size does not match its dimensions");
24
+ }
25
+ this.pixels = ownedPixels;
26
+ this.hardware = null;
27
+ this.producerId = producerId;
28
+ this.width = width;
29
+ this.height = height;
30
+ this.rotation = rotation;
31
+ this.timestampNs = timestampNs;
32
+ }
33
+
34
+ VideoFrame(long nativeLease, int width, int height, int rotation, long timestampNs,
35
+ long producerId) {
36
+ this(nativeLease, width, height, rotation, timestampNs, producerId, nextId.getAndIncrement());
37
+ }
38
+
39
+ private VideoFrame(long nativeLease, int width, int height, int rotation, long timestampNs,
40
+ long producerId, long id) {
41
+ this.hardware = new NativeHardwareBuffer.Lease(nativeLease);
42
+ this.pixels = null;
43
+ this.id = id; this.width = width; this.height = height; this.rotation = rotation;
44
+ this.timestampNs = timestampNs; this.producerId = producerId;
45
+ }
46
+
47
+ public VideoFrame retain() {
48
+ if (hardware == null) return this;
49
+ long retained = hardware.retain();
50
+ try { return new VideoFrame(retained, width, height, rotation, timestampNs, producerId, id); }
51
+ catch (Throwable error) { NativeHardwareBuffer.release(retained); throw error; }
52
+ }
53
+
54
+ /** Releases only this wrapper. Other snapshots of the same immutable AHB survive. */
55
+ @Override public void close() { if (hardware != null) hardware.close(); }
56
+ public boolean isHardwareBuffer() { return hardware != null; }
57
+ public long getNativeHardwareBufferHandle() { return hardware == null ? 0 : hardware.get(); }
58
+
59
+ /** A read-only view; its JNI root retains the allocation until all aliases die. */
60
+ public ByteBuffer getPixelBuffer() {
61
+ if (hardware == null) return pixels.asReadOnlyBuffer();
62
+ // Diagnostic/raw Java oracle only. Normal AHB rendering never reads CPU pixels.
63
+ ByteBuffer destination = NativeRgbaBuffer.allocate(RgbaLayout.byteSize(width, height));
64
+ hardware.read(destination);
65
+ return destination.asReadOnlyBuffer();
66
+ }
67
+ public int getWidth() { return width; }
68
+ public int getHeight() { return height; }
69
+ public int getBytesPerRow() { return RgbaLayout.bytesPerRow(width); }
70
+ public int getRotation() { return rotation; }
71
+ public long getTimestampNs() { return timestampNs; }
72
+ public long getId() { return id; }
73
+ public long getProducerId() { return producerId; }
74
+ }