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.
- package/LICENSE +22 -0
- package/README.md +303 -2
- package/android/BUILD_VALIDATION.md +62 -0
- package/android/CMakeLists.txt +99 -0
- package/android/build-validation.json +262 -0
- package/android/build.gradle +185 -0
- package/android/cpp/EncoderTargets.cpp +226 -0
- package/android/cpp/EncoderTargets.h +70 -0
- package/android/cpp/JNIHelpers.cpp +15 -0
- package/android/cpp/JNIHelpers.h +20 -0
- package/android/cpp/NativeEventDispatcher.cpp +59 -0
- package/android/cpp/NativeEventDispatcher.h +31 -0
- package/android/cpp/NativeHardwareBuffer.cpp +397 -0
- package/android/cpp/NativeHardwareBuffer.h +34 -0
- package/android/cpp/NativeRgbaBuffer.cpp +45 -0
- package/android/cpp/NativeRgbaBuffer.h +9 -0
- package/android/cpp/ReturnedVideoFrames.h +37 -0
- package/android/cpp/RgbaFrameLease.h +53 -0
- package/android/cpp/VideoCapabilities.cpp +87 -0
- package/android/cpp/VideoCapabilities.h +44 -0
- package/android/cpp/VideoComposition.cpp +170 -0
- package/android/cpp/VideoComposition.h +36 -0
- package/android/cpp/VideoCompositionFramesExtractor.cpp +82 -0
- package/android/cpp/VideoCompositionFramesExtractor.h +48 -0
- package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +230 -0
- package/android/cpp/VideoCompositionFramesExtractorHostObject.h +42 -0
- package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.cpp +133 -0
- package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.h +46 -0
- package/android/cpp/VideoEncoderHostObject.cpp +318 -0
- package/android/cpp/VideoEncoderHostObject.h +76 -0
- package/android/cpp/VideoFrame.cpp +293 -0
- package/android/cpp/VideoFrame.h +27 -0
- package/android/cpp/VideoPlayer.cpp +96 -0
- package/android/cpp/VideoPlayer.h +52 -0
- package/android/cpp/VideoPlayerHostObject.cpp +225 -0
- package/android/cpp/VideoPlayerHostObject.h +43 -0
- package/android/cpp/cpp-adapter.cpp +316 -0
- package/android/gradle.properties +5 -0
- package/android/instrumentation-inventory.json +178 -0
- package/android/proguard-rules.pro +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/azzapp/rnskv/AudioCompositionExporter.java +537 -0
- package/android/src/main/java/com/azzapp/rnskv/AudioCompositionPlayer.java +321 -0
- package/android/src/main/java/com/azzapp/rnskv/BufferLifetime.java +55 -0
- package/android/src/main/java/com/azzapp/rnskv/DecodeRequests.java +100 -0
- package/android/src/main/java/com/azzapp/rnskv/DecoderWindow.java +55 -0
- package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
- package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
- package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
- package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
- package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
- package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
- package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
- package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +35 -0
- package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
- package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
- package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
- package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +83 -0
- package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
- package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
- package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
- package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +772 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +354 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
- package/cpp/RNSVCheckedSizes.h +54 -0
- package/cpp/RNSVEventEmitter.cpp +199 -0
- package/cpp/RNSVEventEmitter.h +31 -0
- package/cpp/RNSVHostObject.cpp +79 -0
- package/cpp/RNSVHostObject.h +67 -0
- package/cpp/RNSVMemoryBudget.cpp +68 -0
- package/cpp/RNSVMemoryBudget.h +100 -0
- package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
- package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
- package/docs/ANDROID_DEVICE_306.md +95 -0
- package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
- package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
- package/docs/BENCHMARKS.md +368 -0
- package/docs/IOS_DEVICE_TESTS.md +115 -0
- package/docs/IOS_SIMULATOR_TESTS.md +74 -0
- package/docs/MIGRATION.md +82 -0
- package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
- package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
- package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
- package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
- package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
- package/docs/PERFORMANCE_SIMULATOR.md +89 -0
- package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
- package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
- package/docs/UPSTREAM_NOTES.md +188 -0
- package/docs/assets/banner.svg +26 -0
- package/examples/webgpu-overlay.ts +147 -0
- package/ios/AVAssetTrackUtils.h +24 -0
- package/ios/AudioCompositionUtils.h +27 -0
- package/ios/AudioCompositionUtils.mm +113 -0
- package/ios/DecoderWindow.h +52 -0
- package/ios/NativeResourceStats.h +32 -0
- package/ios/RNSVColorSpace.h +50 -0
- package/ios/RNSVJSIUtils.h +46 -0
- package/ios/RNSVScopedPixelBuffer.h +17 -0
- package/ios/RNSVVideoPlayer.h +46 -0
- package/ios/RNSVVideoPlayer.mm +410 -0
- package/ios/ReactNativeSkiaVideo.h +8 -0
- package/ios/ReactNativeSkiaVideo.mm +310 -0
- package/ios/SeekPolicy.h +27 -0
- package/ios/VideoComposition.h +148 -0
- package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
- package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
- package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
- package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
- package/ios/VideoCompositionItemDecoder.h +60 -0
- package/ios/VideoCompositionItemDecoder.mm +480 -0
- package/ios/VideoEncoderHostObject.h +83 -0
- package/ios/VideoEncoderHostObject.mm +706 -0
- package/ios/VideoFrame.h +66 -0
- package/ios/VideoFrame.mm +108 -0
- package/ios/VideoPlayerHostObject.h +52 -0
- package/ios/VideoPlayerHostObject.mm +332 -0
- package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
- package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
- package/lib/module/RNSkiaVideoModule.js +20 -0
- package/lib/module/RNSkiaVideoModule.js.map +1 -0
- package/lib/module/canvas.js +18 -0
- package/lib/module/canvas.js.map +1 -0
- package/lib/module/exportCache.js +22 -0
- package/lib/module/exportCache.js.map +1 -0
- package/lib/module/exportFrames.js +338 -0
- package/lib/module/exportFrames.js.map +1 -0
- package/lib/module/exportQueue.js +52 -0
- package/lib/module/exportQueue.js.map +1 -0
- package/lib/module/exportTarget.js +137 -0
- package/lib/module/exportTarget.js.map +1 -0
- package/lib/module/exportVideoComposition.js +492 -0
- package/lib/module/exportVideoComposition.js.map +1 -0
- package/lib/module/frameInterop.js +532 -0
- package/lib/module/frameInterop.js.map +1 -0
- package/lib/module/gpu.js +222 -0
- package/lib/module/gpu.js.map +1 -0
- package/lib/module/gpuDevice.js +54 -0
- package/lib/module/gpuDevice.js.map +1 -0
- package/lib/module/index.js +36 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/memory.js +38 -0
- package/lib/module/memory.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/previewLifecycle.js +52 -0
- package/lib/module/previewLifecycle.js.map +1 -0
- package/lib/module/runtimeYield.js +10 -0
- package/lib/module/runtimeYield.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/useEventListener.js +18 -0
- package/lib/module/utils/useEventListener.js.map +1 -0
- package/lib/module/videoCompositionPlayer.js +348 -0
- package/lib/module/videoCompositionPlayer.js.map +1 -0
- package/lib/module/videoFrameImage.js +185 -0
- package/lib/module/videoFrameImage.js.map +1 -0
- package/lib/module/videoPlayer.js +172 -0
- package/lib/module/videoPlayer.js.map +1 -0
- package/lib/module/yuvBlit.js +299 -0
- package/lib/module/yuvBlit.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
- package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
- package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
- package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
- package/lib/typescript/src/canvas.d.ts +4 -0
- package/lib/typescript/src/canvas.d.ts.map +1 -0
- package/lib/typescript/src/exportCache.d.ts +6 -0
- package/lib/typescript/src/exportCache.d.ts.map +1 -0
- package/lib/typescript/src/exportFrames.d.ts +57 -0
- package/lib/typescript/src/exportFrames.d.ts.map +1 -0
- package/lib/typescript/src/exportQueue.d.ts +5 -0
- package/lib/typescript/src/exportQueue.d.ts.map +1 -0
- package/lib/typescript/src/exportTarget.d.ts +28 -0
- package/lib/typescript/src/exportTarget.d.ts.map +1 -0
- package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
- package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
- package/lib/typescript/src/frameInterop.d.ts +17 -0
- package/lib/typescript/src/frameInterop.d.ts.map +1 -0
- package/lib/typescript/src/gpu.d.ts +39 -0
- package/lib/typescript/src/gpu.d.ts.map +1 -0
- package/lib/typescript/src/gpuDevice.d.ts +11 -0
- package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +21 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/memory.d.ts +25 -0
- package/lib/typescript/src/memory.d.ts.map +1 -0
- package/lib/typescript/src/previewLifecycle.d.ts +22 -0
- package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/runtimeYield.d.ts +4 -0
- package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +631 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
- package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
- package/lib/typescript/src/videoCompositionPlayer.d.ts +85 -0
- package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
- package/lib/typescript/src/videoFrameImage.d.ts +60 -0
- package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
- package/lib/typescript/src/videoPlayer.d.ts +99 -0
- package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
- package/lib/typescript/src/yuvBlit.d.ts +50 -0
- package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
- package/package.json +154 -4
- package/patches/react-native-skia+3.7.0.patch +502 -0
- package/react-native-skia-webgpu-video.podspec +35 -0
- package/scripts/apply-skia-canvas-patch.mjs +48 -0
- package/scripts/skia-canvas-patch.json +291 -0
- package/src/NativeReactNativeSkiaVideo.ts +8 -0
- package/src/RNSkiaVideoModule.ts +27 -0
- package/src/canvas.ts +22 -0
- package/src/exportCache.ts +27 -0
- package/src/exportFrames.ts +421 -0
- package/src/exportQueue.ts +65 -0
- package/src/exportTarget.ts +179 -0
- package/src/exportVideoComposition.ts +718 -0
- package/src/frameInterop.ts +684 -0
- package/src/gpu.ts +290 -0
- package/src/gpuDevice.ts +80 -0
- package/src/index.tsx +45 -0
- package/src/memory.ts +48 -0
- package/src/previewLifecycle.ts +60 -0
- package/src/runtimeYield.ts +6 -0
- package/src/types.ts +661 -0
- package/src/utils/useEventListener.ts +22 -0
- package/src/videoCompositionPlayer.ts +520 -0
- package/src/videoFrameImage.ts +205 -0
- package/src/videoPlayer.ts +318 -0
- package/src/yuvBlit.ts +331 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
#import "VideoEncoderHostObject.h"
|
|
2
|
+
#import "AudioCompositionUtils.h"
|
|
3
|
+
#import "NativeResourceStats.h"
|
|
4
|
+
#include "RNSVMemoryBudget.h"
|
|
5
|
+
#include "RNSVCheckedSizes.h"
|
|
6
|
+
#import "RNSVColorSpace.h"
|
|
7
|
+
#import "RNSVJSIUtils.h"
|
|
8
|
+
#import "RNSVScopedPixelBuffer.h"
|
|
9
|
+
// VTCopyVideoEncoderList, for asking the device which codecs it can encode
|
|
10
|
+
// rather than inferring it from the chip generation.
|
|
11
|
+
#import <VideoToolbox/VideoToolbox.h>
|
|
12
|
+
#import <future>
|
|
13
|
+
#include <chrono>
|
|
14
|
+
#include <cstring>
|
|
15
|
+
#include <stdexcept>
|
|
16
|
+
#include <thread>
|
|
17
|
+
|
|
18
|
+
NS_INLINE NSError* createErrorWithMessage(NSString* message) {
|
|
19
|
+
return [NSError errorWithDomain:@"com.azzapp.rnskv"
|
|
20
|
+
code:0
|
|
21
|
+
userInfo:@{NSLocalizedDescriptionKey : message}];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
namespace RNSkiaVideo {
|
|
25
|
+
|
|
26
|
+
VideoEncoderHostObject::VideoEncoderHostObject(
|
|
27
|
+
std::string outPath, int width, int height, int frameRate, int bitRate,
|
|
28
|
+
std::string codec, int audioBitRate, int audioSampleRate,
|
|
29
|
+
int audioChannelCount, std::shared_ptr<VideoComposition> composition,
|
|
30
|
+
bool directEncoder) {
|
|
31
|
+
this->directEncoder = directEncoder;
|
|
32
|
+
this->outPath = outPath;
|
|
33
|
+
this->width = width;
|
|
34
|
+
this->height = height;
|
|
35
|
+
this->frameRate = frameRate;
|
|
36
|
+
this->bitRate = bitRate;
|
|
37
|
+
// Resolved here rather than in prepare() so that the fallback is decided
|
|
38
|
+
// once, before anything is allocated, and the rest of the object can treat
|
|
39
|
+
// this as a codec the device is known to have.
|
|
40
|
+
this->codec = isCodecSupported(codec) ? codec : "h264";
|
|
41
|
+
this->audioBitRate = audioBitRate;
|
|
42
|
+
this->audioSampleRate = audioSampleRate;
|
|
43
|
+
this->audioChannelCount = audioChannelCount;
|
|
44
|
+
this->composition = composition;
|
|
45
|
+
NativeResourceStats::encoders.add(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
VideoEncoderHostObject::~VideoEncoderHostObject() {
|
|
49
|
+
release();
|
|
50
|
+
NativeResourceStats::encoders.remove(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
bool VideoEncoderHostObject::isCodecSupported(const std::string& codec) {
|
|
54
|
+
if (codec == "h264") {
|
|
55
|
+
// Every device that runs this library encodes H.264.
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (codec != "hevc") {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// VTCopyVideoEncoderList is the only answer that comes from the encoders the
|
|
62
|
+
// device actually has, rather than from a hardcoded chip generation. It
|
|
63
|
+
// reports hardware and software encoders alike, which is what we want: an
|
|
64
|
+
// HEVC export that lands on a software encoder is slow but correct.
|
|
65
|
+
CFArrayRef encoders = NULL;
|
|
66
|
+
if (VTCopyVideoEncoderList(NULL, &encoders) != noErr || encoders == NULL) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
bool supported = false;
|
|
70
|
+
for (CFIndex i = 0, n = CFArrayGetCount(encoders); i < n && !supported; i++) {
|
|
71
|
+
auto encoder = (CFDictionaryRef)CFArrayGetValueAtIndex(encoders, i);
|
|
72
|
+
auto codecType = (CFNumberRef)CFDictionaryGetValue(
|
|
73
|
+
encoder, kVTVideoEncoderList_CodecType);
|
|
74
|
+
int32_t value = 0;
|
|
75
|
+
if (codecType &&
|
|
76
|
+
CFNumberGetValue(codecType, kCFNumberSInt32Type, &value) &&
|
|
77
|
+
value == kCMVideoCodecType_HEVC) {
|
|
78
|
+
supported = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
CFRelease(encoders);
|
|
82
|
+
return supported;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
std::vector<jsi::PropNameID>
|
|
86
|
+
VideoEncoderHostObject::getPropertyNames(jsi::Runtime& rt) {
|
|
87
|
+
std::vector<jsi::PropNameID> result;
|
|
88
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("prepare")));
|
|
89
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("encodeFrame")));
|
|
90
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("acquireFrameBuffer")));
|
|
91
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("releaseFrameBuffer")));
|
|
92
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("finishWriting")));
|
|
93
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("dispose")));
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The methods are created once per runtime (see RNSVHostObject):
|
|
98
|
+
// `encodeFrame` is read for every exported frame.
|
|
99
|
+
jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
|
|
100
|
+
const jsi::PropNameID& propNameId) {
|
|
101
|
+
auto propName = propNameId.utf8(runtime);
|
|
102
|
+
if (propName == "prepare") {
|
|
103
|
+
return getFunction(
|
|
104
|
+
runtime, propName, 0,
|
|
105
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
106
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
107
|
+
return runPooled([&] { prepare(); });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (propName == "encodeFrame") {
|
|
111
|
+
return getFunction(
|
|
112
|
+
runtime, propName, 2,
|
|
113
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
114
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
115
|
+
if (count < 2 || !arguments[0].isObject() || !arguments[1].isNumber()) {
|
|
116
|
+
throw jsi::JSError(runtime, "encodeFrame expects a frame and seconds");
|
|
117
|
+
}
|
|
118
|
+
auto descriptor = arguments[0].asObject(runtime);
|
|
119
|
+
auto time = CMTimeMakeWithSeconds(checkedMediaSeconds(arguments[1].asNumber()), NSEC_PER_SEC);
|
|
120
|
+
auto kindValue = descriptor.getProperty(runtime, "kind");
|
|
121
|
+
auto kind = kindValue.isString() ? kindValue.asString(runtime).utf8(runtime)
|
|
122
|
+
: std::string();
|
|
123
|
+
if (kind == "bgra") {
|
|
124
|
+
auto data = descriptor.getProperty(runtime, "data").asObject(runtime)
|
|
125
|
+
.getArrayBuffer(runtime);
|
|
126
|
+
auto rowValue = descriptor.getProperty(runtime, "bytesPerRow");
|
|
127
|
+
size_t rowBytes = rowValue.isNumber()
|
|
128
|
+
? checkedStorageInteger(rowValue.asNumber(), "BGRA row bytes")
|
|
129
|
+
: checkedSizeProduct(static_cast<size_t>(width), 4, "BGRA row bytes");
|
|
130
|
+
size_t required = checkedPixelStorage(width, height, rowBytes);
|
|
131
|
+
if (descriptor.getProperty(runtime, "width").asNumber() != width ||
|
|
132
|
+
descriptor.getProperty(runtime, "height").asNumber() != height ||
|
|
133
|
+
data.size(runtime) < required) {
|
|
134
|
+
throw jsi::JSError(runtime, "Invalid BGRA encoder dimensions/storage");
|
|
135
|
+
}
|
|
136
|
+
return runPooled([&] { encodePixels(data.data(runtime), rowBytes, time); });
|
|
137
|
+
}
|
|
138
|
+
if (kind == "rgba-pixels") {
|
|
139
|
+
auto pixels = descriptor.getProperty(runtime, "pixels").asObject(runtime);
|
|
140
|
+
auto storage = pixels.getProperty(runtime, "buffer").asObject(runtime)
|
|
141
|
+
.getArrayBuffer(runtime);
|
|
142
|
+
size_t offset = checkedStorageInteger(
|
|
143
|
+
pixels.getProperty(runtime, "byteOffset").asNumber(), "RGBA byte offset");
|
|
144
|
+
size_t length = checkedStorageInteger(
|
|
145
|
+
pixels.getProperty(runtime, "byteLength").asNumber(), "RGBA byte length");
|
|
146
|
+
auto rowValue = descriptor.getProperty(runtime, "rowBytes");
|
|
147
|
+
size_t rowBytes = rowValue.isNumber()
|
|
148
|
+
? checkedStorageInteger(rowValue.asNumber(), "RGBA row bytes")
|
|
149
|
+
: checkedSizeProduct(static_cast<size_t>(width), 4, "RGBA row bytes");
|
|
150
|
+
size_t required = checkedPixelStorage(width, height, rowBytes);
|
|
151
|
+
if (descriptor.getProperty(runtime, "width").asNumber() != width ||
|
|
152
|
+
descriptor.getProperty(runtime, "height").asNumber() != height ||
|
|
153
|
+
offset > storage.size(runtime) ||
|
|
154
|
+
length > storage.size(runtime) - offset ||
|
|
155
|
+
length < required) {
|
|
156
|
+
throw jsi::JSError(runtime, "Invalid RGBA encoder pixel storage");
|
|
157
|
+
}
|
|
158
|
+
return runPooled([&] {
|
|
159
|
+
encodePixels(storage.data(runtime) + offset, rowBytes, time, true);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (kind != "native-buffer") {
|
|
163
|
+
throw jsi::JSError(runtime, "encodeFrame needs bgra, native-buffer or rgba-pixels");
|
|
164
|
+
}
|
|
165
|
+
auto pointer = descriptor.getProperty(runtime, "nativeBuffer");
|
|
166
|
+
if (!pointer.isBigInt()) {
|
|
167
|
+
throw jsi::JSError(runtime, "nativeBuffer must be a CVPixelBuffer BigInt");
|
|
168
|
+
}
|
|
169
|
+
auto buffer = reinterpret_cast<CVPixelBufferRef>(
|
|
170
|
+
pointer.asBigInt(runtime).asUint64(runtime));
|
|
171
|
+
if (!buffer) throw jsi::JSError(runtime, "Null encoder nativeBuffer");
|
|
172
|
+
return runPooled([&] { encodeFrame(buffer, time); });
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (propName == "acquireFrameBuffer") {
|
|
176
|
+
return getFunction(
|
|
177
|
+
runtime, propName, 0,
|
|
178
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
179
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
180
|
+
uintptr_t pointer = 0;
|
|
181
|
+
runPooled([&] { pointer = acquireFrameBuffer(); });
|
|
182
|
+
return jsi::BigInt::fromUint64(runtime, pointer);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (propName == "releaseFrameBuffer") {
|
|
186
|
+
return getFunction(
|
|
187
|
+
runtime, propName, 0,
|
|
188
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
189
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
190
|
+
return runPooled([&] { releaseFrameBuffer(); });
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (propName == "finishWriting") {
|
|
194
|
+
return getFunction(
|
|
195
|
+
runtime, propName, 0,
|
|
196
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
197
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
198
|
+
return runPooled([&] { finish(); });
|
|
199
|
+
});
|
|
200
|
+
} else if (propName == "dispose") {
|
|
201
|
+
return getFunction(
|
|
202
|
+
runtime, propName, 0,
|
|
203
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
204
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
205
|
+
return runPooled([&] { this->release(); });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return jsi::Value::undefined();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
void VideoEncoderHostObject::prepare() {
|
|
212
|
+
std::lock_guard<std::recursive_mutex> guard(stateMutex);
|
|
213
|
+
if (disposed) throw createErrorWithMessage(@"Encoder is disposed");
|
|
214
|
+
if (prepared) return;
|
|
215
|
+
try { prepareImpl(); }
|
|
216
|
+
catch (...) { release(); throw; }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
void VideoEncoderHostObject::prepareImpl() {
|
|
220
|
+
if (width <= 0 || height <= 0 || frameRate <= 0 || bitRate <= 0) {
|
|
221
|
+
throw createErrorWithMessage(@"Invalid encoder dimensions, cadence or bitrate");
|
|
222
|
+
}
|
|
223
|
+
// Validate/reserve before starting a writer or an audio callback. A failed
|
|
224
|
+
// preparation rolls this reservation back immediately, even if JS retains
|
|
225
|
+
// the encoder host object while reporting the error.
|
|
226
|
+
size_t rowBytes = checkedSizeProduct(static_cast<size_t>(width), 4, "encoder stride");
|
|
227
|
+
auto capacity = checkedSizeProduct(checkedPixelStorage(width, height, rowBytes),
|
|
228
|
+
3, "encoder pool capacity");
|
|
229
|
+
poolMemoryToken = MemoryBudget::instance().reserve(capacity, "iOS encoder pool capacity");
|
|
230
|
+
NSDictionary* attributes = @{
|
|
231
|
+
(NSString*)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
|
|
232
|
+
(NSString*)kCVPixelBufferWidthKey : @(width),
|
|
233
|
+
(NSString*)kCVPixelBufferHeightKey : @(height),
|
|
234
|
+
(NSString*)kCVPixelBufferMetalCompatibilityKey : @YES,
|
|
235
|
+
(NSString*)kCVPixelBufferIOSurfacePropertiesKey : @{},
|
|
236
|
+
};
|
|
237
|
+
// Allocate a fresh buffer per frame from this pool instead of reusing a
|
|
238
|
+
// single CVPixelBuffer. AVAssetWriter encodes appended buffers
|
|
239
|
+
// asynchronously, so a reused buffer could be overwritten by the next frame
|
|
240
|
+
// while the encoder is still reading it, producing torn frames on fast
|
|
241
|
+
// motion. The pool only recycles a buffer once every reference to it (the
|
|
242
|
+
// encoder's included) is gone.
|
|
243
|
+
if (pixelBufferPool) {
|
|
244
|
+
CVPixelBufferPoolRelease(pixelBufferPool);
|
|
245
|
+
pixelBufferPool = NULL;
|
|
246
|
+
}
|
|
247
|
+
CVReturn status = CVPixelBufferPoolCreate(
|
|
248
|
+
kCFAllocatorDefault, NULL, (__bridge CFDictionaryRef)attributes,
|
|
249
|
+
&pixelBufferPool);
|
|
250
|
+
|
|
251
|
+
if (status != kCVReturnSuccess) {
|
|
252
|
+
throw createErrorWithMessage(@"Could not create pixel buffer pool");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
CVPixelBufferRef probeBuffer = NULL;
|
|
257
|
+
if (CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool,
|
|
258
|
+
&probeBuffer) != kCVReturnSuccess || !probeBuffer)
|
|
259
|
+
throw createErrorWithMessage(@"Could not probe encoder pixel storage");
|
|
260
|
+
ScopedPixelBuffer probe(probeBuffer);
|
|
261
|
+
MemoryBudget::instance().resize(poolMemoryToken,
|
|
262
|
+
checkedSizeProduct(pixelBufferBytes(probe.get()), 3, "aligned encoder pool"),
|
|
263
|
+
"iOS aligned encoder pool capacity");
|
|
264
|
+
|
|
265
|
+
NSError* error = nil;
|
|
266
|
+
assetWriter = [AVAssetWriter
|
|
267
|
+
assetWriterWithURL:
|
|
268
|
+
[NSURL fileURLWithPath:
|
|
269
|
+
[NSString
|
|
270
|
+
stringWithCString:outPath.c_str()
|
|
271
|
+
encoding:[NSString defaultCStringEncoding]]]
|
|
272
|
+
fileType:AVFileTypeMPEG4
|
|
273
|
+
error:&error];
|
|
274
|
+
if (error) {
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
bool isHEVC = codec == "hevc";
|
|
279
|
+
|
|
280
|
+
// The profile is codec specific and the H.264 constants are rejected outright
|
|
281
|
+
// for HEVC, so the compression properties are built per codec rather than
|
|
282
|
+
// shared. HEVC's Main profile is the interoperable one — Main10 would mean
|
|
283
|
+
// a 10 bit pixel format, which this encoder does not produce.
|
|
284
|
+
NSMutableDictionary* compressionProperties = [@{
|
|
285
|
+
AVVideoAverageBitRateKey : @(bitRate),
|
|
286
|
+
AVVideoMaxKeyFrameIntervalKey : @(frameRate),
|
|
287
|
+
} mutableCopy];
|
|
288
|
+
compressionProperties[AVVideoProfileLevelKey] =
|
|
289
|
+
isHEVC ? (id)kVTProfileLevel_HEVC_Main_AutoLevel
|
|
290
|
+
: (id)AVVideoProfileLevelH264HighAutoLevel;
|
|
291
|
+
|
|
292
|
+
auto videoSettings = @{
|
|
293
|
+
AVVideoCodecKey : isHEVC ? AVVideoCodecTypeHEVC : AVVideoCodecTypeH264,
|
|
294
|
+
AVVideoWidthKey : @(width),
|
|
295
|
+
AVVideoHeightKey : @(height),
|
|
296
|
+
// The frames come out of Skia in Rec.709; say so in the file rather than
|
|
297
|
+
// leaving every player to guess (see RNSVColorSpace.h).
|
|
298
|
+
AVVideoColorPropertiesKey : SDRColorProperties(),
|
|
299
|
+
AVVideoCompressionPropertiesKey : compressionProperties,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
assetWriterInput =
|
|
303
|
+
[AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
|
|
304
|
+
outputSettings:videoSettings];
|
|
305
|
+
assetWriterInput.expectsMediaDataInRealTime = NO;
|
|
306
|
+
assetWriterInput.performsMultiPassEncodingIfSupported = NO;
|
|
307
|
+
if ([assetWriter canAddInput:assetWriterInput]) {
|
|
308
|
+
[assetWriter addInput:assetWriterInput];
|
|
309
|
+
} else {
|
|
310
|
+
throw assetWriter.error
|
|
311
|
+
?: createErrorWithMessage(@"could not add output to asset writer");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (composition && composition->hasAudio()) {
|
|
316
|
+
setupAudio();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (![assetWriter startWriting]) {
|
|
320
|
+
throw assetWriter.error ?: createErrorWithMessage(@"Could not start writing");
|
|
321
|
+
}
|
|
322
|
+
[assetWriter startSessionAtSourceTime:kCMTimeZero];
|
|
323
|
+
|
|
324
|
+
if (audioWriterInput) {
|
|
325
|
+
startWritingAudio();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
prepared = true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
void VideoEncoderHostObject::waitUntilReady() {
|
|
332
|
+
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
|
333
|
+
while (!assetWriterInput.isReadyForMoreMediaData) {
|
|
334
|
+
if (disposed || assetWriter.status != AVAssetWriterStatusWriting) {
|
|
335
|
+
throw assetWriter.error ?: createErrorWithMessage(@"Encoder stopped writing");
|
|
336
|
+
}
|
|
337
|
+
if (std::chrono::steady_clock::now() >= deadline) {
|
|
338
|
+
throw createErrorWithMessage(@"Encoder input stalled for 10 seconds");
|
|
339
|
+
}
|
|
340
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
CVPixelBufferRef VideoEncoderHostObject::acquireOutputBuffer() {
|
|
345
|
+
waitUntilReady();
|
|
346
|
+
// AVAssetWriter retains submitted buffers. Refuse allocation past the
|
|
347
|
+
// threshold and wait for the writer to release one instead of growing.
|
|
348
|
+
NSDictionary* auxiliary = @{(id)kCVPixelBufferPoolAllocationThresholdKey : @3};
|
|
349
|
+
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
|
350
|
+
CVPixelBufferRef buffer = NULL;
|
|
351
|
+
while (true) {
|
|
352
|
+
CVReturn status = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(
|
|
353
|
+
kCFAllocatorDefault, pixelBufferPool,
|
|
354
|
+
(__bridge CFDictionaryRef)auxiliary, &buffer);
|
|
355
|
+
if (status == kCVReturnSuccess && buffer) return buffer;
|
|
356
|
+
if (status != kCVReturnWouldExceedAllocationThreshold) {
|
|
357
|
+
throw createErrorWithMessage(@"Could not allocate encoder pixel buffer");
|
|
358
|
+
}
|
|
359
|
+
if (std::chrono::steady_clock::now() >= deadline || disposed ||
|
|
360
|
+
assetWriter.status != AVAssetWriterStatusWriting) {
|
|
361
|
+
throw createErrorWithMessage(@"Bounded encoder pool stalled");
|
|
362
|
+
}
|
|
363
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
uintptr_t VideoEncoderHostObject::acquireFrameBuffer() {
|
|
368
|
+
std::lock_guard<std::recursive_mutex> guard(stateMutex);
|
|
369
|
+
if (!prepared || disposed) throw createErrorWithMessage(@"Encoder not prepared");
|
|
370
|
+
if (vendedBuffer) {
|
|
371
|
+
throw createErrorWithMessage(@"Previous encoder frame buffer was neither encoded nor released");
|
|
372
|
+
}
|
|
373
|
+
vendedBuffer = acquireOutputBuffer();
|
|
374
|
+
return reinterpret_cast<uintptr_t>(vendedBuffer);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
void VideoEncoderHostObject::releaseFrameBuffer() {
|
|
378
|
+
std::lock_guard<std::recursive_mutex> guard(stateMutex);
|
|
379
|
+
if (vendedBuffer) {
|
|
380
|
+
CVPixelBufferRelease(vendedBuffer);
|
|
381
|
+
vendedBuffer = NULL;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
void VideoEncoderHostObject::encodePixels(const uint8_t* pixels,
|
|
386
|
+
size_t rowBytes, CMTime time,
|
|
387
|
+
bool rgba) {
|
|
388
|
+
std::lock_guard<std::recursive_mutex> guard(stateMutex);
|
|
389
|
+
if (!prepared || disposed) throw createErrorWithMessage(@"Encoder not prepared");
|
|
390
|
+
CVPixelBufferRef output = acquireOutputBuffer();
|
|
391
|
+
CVReturn status = CVPixelBufferLockBaseAddress(output, 0);
|
|
392
|
+
if (status != kCVReturnSuccess) {
|
|
393
|
+
CVPixelBufferRelease(output);
|
|
394
|
+
throw createErrorWithMessage(@"Could not lock encoder pixel buffer");
|
|
395
|
+
}
|
|
396
|
+
auto destination = static_cast<uint8_t*>(CVPixelBufferGetBaseAddress(output));
|
|
397
|
+
size_t destinationStride = CVPixelBufferGetBytesPerRow(output);
|
|
398
|
+
if (!destination) {
|
|
399
|
+
CVPixelBufferUnlockBaseAddress(output, 0);
|
|
400
|
+
CVPixelBufferRelease(output);
|
|
401
|
+
throw createErrorWithMessage(@"Encoder pixel storage unavailable");
|
|
402
|
+
}
|
|
403
|
+
for (int row = 0; row < height; ++row) {
|
|
404
|
+
auto sourceRow = pixels + row * rowBytes;
|
|
405
|
+
auto targetRow = destination + row * destinationStride;
|
|
406
|
+
if (!rgba) {
|
|
407
|
+
memcpy(targetRow, sourceRow, static_cast<size_t>(width) * 4);
|
|
408
|
+
} else {
|
|
409
|
+
for (int column = 0; column < width; ++column) {
|
|
410
|
+
targetRow[column * 4] = sourceRow[column * 4 + 2];
|
|
411
|
+
targetRow[column * 4 + 1] = sourceRow[column * 4 + 1];
|
|
412
|
+
targetRow[column * 4 + 2] = sourceRow[column * 4];
|
|
413
|
+
targetRow[column * 4 + 3] = sourceRow[column * 4 + 3];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
CVPixelBufferUnlockBaseAddress(output, 0);
|
|
418
|
+
try { appendBuffer(output, time); }
|
|
419
|
+
catch (...) { CVPixelBufferRelease(output); throw; }
|
|
420
|
+
CVPixelBufferRelease(output);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
void VideoEncoderHostObject::encodeFrame(CVPixelBufferRef source, CMTime time) {
|
|
424
|
+
std::lock_guard<std::recursive_mutex> guard(stateMutex);
|
|
425
|
+
if (!prepared || disposed) throw createErrorWithMessage(@"Encoder not prepared");
|
|
426
|
+
if (CVPixelBufferGetWidth(source) != static_cast<size_t>(width) ||
|
|
427
|
+
CVPixelBufferGetHeight(source) != static_cast<size_t>(height) ||
|
|
428
|
+
CVPixelBufferGetPixelFormatType(source) != kCVPixelFormatType_32BGRA) {
|
|
429
|
+
throw createErrorWithMessage(@"Encoder requires exact-sized BGRA native buffer");
|
|
430
|
+
}
|
|
431
|
+
if (source == vendedBuffer) {
|
|
432
|
+
// The GPU already drew into this pool buffer; take over the lent reference.
|
|
433
|
+
vendedBuffer = NULL;
|
|
434
|
+
try { appendBuffer(source, time); }
|
|
435
|
+
catch (...) { CVPixelBufferRelease(source); throw; }
|
|
436
|
+
CVPixelBufferRelease(source);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (directEncoder) {
|
|
440
|
+
CVPixelBufferRetain(source);
|
|
441
|
+
try { appendBuffer(source, time); }
|
|
442
|
+
catch (...) { CVPixelBufferRelease(source); throw; }
|
|
443
|
+
CVPixelBufferRelease(source);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
CVPixelBufferRef output = acquireOutputBuffer();
|
|
447
|
+
CVReturn sourceStatus = CVPixelBufferLockBaseAddress(source, kCVPixelBufferLock_ReadOnly);
|
|
448
|
+
CVReturn targetStatus = CVPixelBufferLockBaseAddress(output, 0);
|
|
449
|
+
if (sourceStatus != kCVReturnSuccess || targetStatus != kCVReturnSuccess) {
|
|
450
|
+
if (sourceStatus == kCVReturnSuccess)
|
|
451
|
+
CVPixelBufferUnlockBaseAddress(source, kCVPixelBufferLock_ReadOnly);
|
|
452
|
+
if (targetStatus == kCVReturnSuccess) CVPixelBufferUnlockBaseAddress(output, 0);
|
|
453
|
+
CVPixelBufferRelease(output);
|
|
454
|
+
throw createErrorWithMessage(@"Could not lock encoder copy buffers");
|
|
455
|
+
}
|
|
456
|
+
auto input = static_cast<const uint8_t*>(CVPixelBufferGetBaseAddress(source));
|
|
457
|
+
auto destination = static_cast<uint8_t*>(CVPixelBufferGetBaseAddress(output));
|
|
458
|
+
size_t inputStride = CVPixelBufferGetBytesPerRow(source);
|
|
459
|
+
size_t targetStride = CVPixelBufferGetBytesPerRow(output);
|
|
460
|
+
if (!input || !destination) {
|
|
461
|
+
CVPixelBufferUnlockBaseAddress(source, kCVPixelBufferLock_ReadOnly);
|
|
462
|
+
CVPixelBufferUnlockBaseAddress(output, 0);
|
|
463
|
+
CVPixelBufferRelease(output);
|
|
464
|
+
throw createErrorWithMessage(@"Could not access encoder copy buffers");
|
|
465
|
+
}
|
|
466
|
+
for (int row = 0; row < height; ++row) {
|
|
467
|
+
memcpy(destination + row * targetStride, input + row * inputStride, static_cast<size_t>(width) * 4);
|
|
468
|
+
}
|
|
469
|
+
CVPixelBufferUnlockBaseAddress(source, kCVPixelBufferLock_ReadOnly);
|
|
470
|
+
CVPixelBufferUnlockBaseAddress(output, 0);
|
|
471
|
+
try { appendBuffer(output, time); }
|
|
472
|
+
catch (...) { CVPixelBufferRelease(output); throw; }
|
|
473
|
+
CVPixelBufferRelease(output);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
void VideoEncoderHostObject::appendBuffer(CVPixelBufferRef pixelBuffer,
|
|
477
|
+
CMTime time) {
|
|
478
|
+
waitUntilReady();
|
|
479
|
+
TagBufferAsSDR(pixelBuffer);
|
|
480
|
+
size_t bytes = pixelBufferBytes(pixelBuffer);
|
|
481
|
+
NativeResourceStats::encoderSubmittingBuffers.add(1);
|
|
482
|
+
NativeResourceStats::encoderSubmittingBytes.add(bytes);
|
|
483
|
+
CMSampleBufferRef sampleBuffer = NULL;
|
|
484
|
+
CMVideoFormatDescriptionRef formatDescription = NULL;
|
|
485
|
+
OSStatus status = CMVideoFormatDescriptionCreateForImageBuffer(
|
|
486
|
+
NULL, pixelBuffer, &formatDescription);
|
|
487
|
+
CMSampleTimingInfo timingInfo = {CMTimeMake(1, frameRate), time, kCMTimeInvalid};
|
|
488
|
+
NSError* error = nil;
|
|
489
|
+
if (status != noErr || CMSampleBufferCreateForImageBuffer(
|
|
490
|
+
kCFAllocatorDefault, pixelBuffer, true, NULL, NULL,
|
|
491
|
+
formatDescription, &timingInfo, &sampleBuffer) != noErr) {
|
|
492
|
+
error = createErrorWithMessage(@"Could not create sample buffer from frame");
|
|
493
|
+
} else if (![assetWriterInput appendSampleBuffer:sampleBuffer]) {
|
|
494
|
+
error = assetWriter.error ?: createErrorWithMessage(@"Could not append frame");
|
|
495
|
+
}
|
|
496
|
+
if (sampleBuffer) CFRelease(sampleBuffer);
|
|
497
|
+
if (formatDescription) CFRelease(formatDescription);
|
|
498
|
+
NativeResourceStats::encoderSubmittingBuffers.remove(1);
|
|
499
|
+
NativeResourceStats::encoderSubmittingBytes.remove(bytes);
|
|
500
|
+
if (error) throw error;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
void VideoEncoderHostObject::setupAudio() {
|
|
504
|
+
auto audioComposition = buildAudioComposition(composition, nil);
|
|
505
|
+
if (!audioComposition.composition) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
NSError* error = nil;
|
|
510
|
+
audioReader = [AVAssetReader assetReaderWithAsset:audioComposition.composition
|
|
511
|
+
error:&error];
|
|
512
|
+
if (error) {
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
// The audio composition is padded with silence past the composition
|
|
516
|
+
// duration, clamp the export to the exact video duration.
|
|
517
|
+
audioReader.timeRange = CMTimeRangeMake(
|
|
518
|
+
kCMTimeZero, CMTimeMakeWithSeconds(composition->duration, NSEC_PER_SEC));
|
|
519
|
+
|
|
520
|
+
NSDictionary* pcmSettings = @{
|
|
521
|
+
AVFormatIDKey : @(kAudioFormatLinearPCM),
|
|
522
|
+
AVSampleRateKey : @(audioSampleRate),
|
|
523
|
+
AVNumberOfChannelsKey : @(audioChannelCount),
|
|
524
|
+
AVLinearPCMBitDepthKey : @(16),
|
|
525
|
+
AVLinearPCMIsFloatKey : @(NO),
|
|
526
|
+
AVLinearPCMIsBigEndianKey : @(NO),
|
|
527
|
+
AVLinearPCMIsNonInterleaved : @(NO)
|
|
528
|
+
};
|
|
529
|
+
audioMixOutput = [[AVAssetReaderAudioMixOutput alloc]
|
|
530
|
+
initWithAudioTracks:[audioComposition.composition
|
|
531
|
+
tracksWithMediaType:AVMediaTypeAudio]
|
|
532
|
+
audioSettings:pcmSettings];
|
|
533
|
+
if (audioComposition.audioMix) {
|
|
534
|
+
audioMixOutput.audioMix = audioComposition.audioMix;
|
|
535
|
+
}
|
|
536
|
+
if (![audioReader canAddOutput:audioMixOutput]) {
|
|
537
|
+
throw createErrorWithMessage(@"Could not read composition audio");
|
|
538
|
+
}
|
|
539
|
+
[audioReader addOutput:audioMixOutput];
|
|
540
|
+
|
|
541
|
+
NSDictionary* aacSettings = @{
|
|
542
|
+
AVFormatIDKey : @(kAudioFormatMPEG4AAC),
|
|
543
|
+
AVSampleRateKey : @(audioSampleRate),
|
|
544
|
+
AVNumberOfChannelsKey : @(audioChannelCount),
|
|
545
|
+
AVEncoderBitRateKey : @(audioBitRate)
|
|
546
|
+
};
|
|
547
|
+
audioWriterInput =
|
|
548
|
+
[AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
|
|
549
|
+
outputSettings:aacSettings];
|
|
550
|
+
audioWriterInput.expectsMediaDataInRealTime = NO;
|
|
551
|
+
if ([assetWriter canAddInput:audioWriterInput]) {
|
|
552
|
+
[assetWriter addInput:audioWriterInput];
|
|
553
|
+
} else {
|
|
554
|
+
audioWriterInput = nil;
|
|
555
|
+
throw assetWriter.error
|
|
556
|
+
?: createErrorWithMessage(
|
|
557
|
+
@"could not add audio output to asset writer");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
void VideoEncoderHostObject::startWritingAudio() {
|
|
562
|
+
if (![audioReader startReading]) {
|
|
563
|
+
throw audioReader.error
|
|
564
|
+
?: createErrorWithMessage(@"Could not read composition audio");
|
|
565
|
+
}
|
|
566
|
+
audioCompletionSemaphore = dispatch_semaphore_create(0);
|
|
567
|
+
audioErrorHolder = [NSMutableArray array];
|
|
568
|
+
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(
|
|
569
|
+
DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
|
|
570
|
+
audioQueue = dispatch_queue_create("RNSkiaVideoAudioEncoder", attr);
|
|
571
|
+
|
|
572
|
+
// The block only captures ObjC objects, never `this`, so it can safely
|
|
573
|
+
// outlive this host object.
|
|
574
|
+
AVAssetWriterInput* input = audioWriterInput;
|
|
575
|
+
AVAssetReaderAudioMixOutput* output = audioMixOutput;
|
|
576
|
+
AVAssetReader* reader = audioReader;
|
|
577
|
+
AVAssetWriter* writer = assetWriter;
|
|
578
|
+
dispatch_semaphore_t semaphore = audioCompletionSemaphore;
|
|
579
|
+
NSMutableArray<NSError*>* errorHolder = audioErrorHolder;
|
|
580
|
+
__block BOOL finished = NO;
|
|
581
|
+
[input
|
|
582
|
+
requestMediaDataWhenReadyOnQueue:audioQueue
|
|
583
|
+
usingBlock:^{
|
|
584
|
+
if (finished) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
while (input.isReadyForMoreMediaData) {
|
|
588
|
+
CMSampleBufferRef sampleBuffer =
|
|
589
|
+
[output copyNextSampleBuffer];
|
|
590
|
+
if (!sampleBuffer) {
|
|
591
|
+
if (reader.status ==
|
|
592
|
+
AVAssetReaderStatusFailed) {
|
|
593
|
+
[errorHolder
|
|
594
|
+
addObject:reader.error
|
|
595
|
+
?: createErrorWithMessage(
|
|
596
|
+
@"Could not read "
|
|
597
|
+
@"composition "
|
|
598
|
+
@"audio")];
|
|
599
|
+
}
|
|
600
|
+
finished = YES;
|
|
601
|
+
[input markAsFinished];
|
|
602
|
+
dispatch_semaphore_signal(semaphore);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
BOOL appended =
|
|
606
|
+
[input appendSampleBuffer:sampleBuffer];
|
|
607
|
+
CFRelease(sampleBuffer);
|
|
608
|
+
if (!appended) {
|
|
609
|
+
[errorHolder
|
|
610
|
+
addObject:writer.error
|
|
611
|
+
?: createErrorWithMessage(
|
|
612
|
+
@"Could not append "
|
|
613
|
+
@"audio data to "
|
|
614
|
+
@"AVAssetWriter")];
|
|
615
|
+
[reader cancelReading];
|
|
616
|
+
finished = YES;
|
|
617
|
+
[input markAsFinished];
|
|
618
|
+
dispatch_semaphore_signal(semaphore);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
void VideoEncoderHostObject::finish() {
|
|
626
|
+
std::lock_guard<std::recursive_mutex> guard(stateMutex);
|
|
627
|
+
if (!prepared || disposed) throw createErrorWithMessage(@"Encoder not prepared");
|
|
628
|
+
[assetWriterInput markAsFinished];
|
|
629
|
+
if (audioWriterInput) {
|
|
630
|
+
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
|
631
|
+
while (dispatch_semaphore_wait(audioCompletionSemaphore,
|
|
632
|
+
dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC)) != 0) {
|
|
633
|
+
if (disposed) throw createErrorWithMessage(@"Encoder is disposed");
|
|
634
|
+
if (std::chrono::steady_clock::now() >= deadline)
|
|
635
|
+
throw createErrorWithMessage(@"Audio encoder finalization timed out");
|
|
636
|
+
}
|
|
637
|
+
NSError* audioError = audioErrorHolder.firstObject;
|
|
638
|
+
if (audioError) throw audioError;
|
|
639
|
+
}
|
|
640
|
+
// The completion may run after timeout/disposal; it owns its own state and
|
|
641
|
+
// never dereferences this host object or a destroyed stack promise.
|
|
642
|
+
struct WriterCompletion {
|
|
643
|
+
std::promise<void> done;
|
|
644
|
+
NSError* __strong error = nil;
|
|
645
|
+
};
|
|
646
|
+
auto completion = std::make_shared<WriterCompletion>();
|
|
647
|
+
auto future = completion->done.get_future();
|
|
648
|
+
AVAssetWriter* writer = assetWriter;
|
|
649
|
+
[writer finishWritingWithCompletionHandler:^{
|
|
650
|
+
completion->error = writer.status == AVAssetWriterStatusCompleted ? nil :
|
|
651
|
+
(writer.error ?: createErrorWithMessage(@"Failed to finalize export"));
|
|
652
|
+
completion->done.set_value();
|
|
653
|
+
}];
|
|
654
|
+
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
|
655
|
+
while (future.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) {
|
|
656
|
+
if (disposed || std::chrono::steady_clock::now() >= deadline) {
|
|
657
|
+
[writer cancelWriting];
|
|
658
|
+
throw createErrorWithMessage(disposed ? @"Encoder is disposed" :
|
|
659
|
+
@"Video encoder finalization timed out");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
future.get();
|
|
663
|
+
NSError* error = completion->error;
|
|
664
|
+
if (error) throw error;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
void VideoEncoderHostObject::release() {
|
|
668
|
+
// A closer announces cancellation before waiting for a synchronous encode
|
|
669
|
+
// or finish call. Those waits then wake promptly without freeing an in-use
|
|
670
|
+
// pool or writer from under the other runtime.
|
|
671
|
+
if (disposed.exchange(true)) return;
|
|
672
|
+
std::lock_guard<std::recursive_mutex> guard(stateMutex);
|
|
673
|
+
prepared = false;
|
|
674
|
+
if (audioReader && audioReader.status == AVAssetReaderStatusReading) {
|
|
675
|
+
[audioReader cancelReading];
|
|
676
|
+
}
|
|
677
|
+
if (audioQueue) {
|
|
678
|
+
AVAssetWriterInput* input = audioWriterInput;
|
|
679
|
+
BOOL shouldMarkFinished =
|
|
680
|
+
input && (assetWriter.status == AVAssetWriterStatusWriting ||
|
|
681
|
+
assetWriter.status == AVAssetWriterStatusFailed);
|
|
682
|
+
dispatch_sync(audioQueue, ^{
|
|
683
|
+
if (shouldMarkFinished) {
|
|
684
|
+
[input markAsFinished];
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
audioQueue = nil;
|
|
688
|
+
}
|
|
689
|
+
audioReader = nil;
|
|
690
|
+
audioMixOutput = nil;
|
|
691
|
+
audioWriterInput = nil;
|
|
692
|
+
if (assetWriter && assetWriter.status == AVAssetWriterStatusWriting) {
|
|
693
|
+
[assetWriter cancelWriting];
|
|
694
|
+
}
|
|
695
|
+
assetWriter = nil;
|
|
696
|
+
assetWriterInput = nil;
|
|
697
|
+
releaseFrameBuffer();
|
|
698
|
+
if (pixelBufferPool) {
|
|
699
|
+
CVPixelBufferPoolRelease(pixelBufferPool);
|
|
700
|
+
pixelBufferPool = NULL;
|
|
701
|
+
}
|
|
702
|
+
MemoryBudget::instance().release(poolMemoryToken);
|
|
703
|
+
poolMemoryToken = 0;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
} // namespace RNSkiaVideo
|