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.
- 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/DisplayVsyncPeriod.java +72 -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 +43 -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 +114 -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 +779 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +463 -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 +368 -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 +105 -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 +155 -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 +568 -0
- package/src/videoFrameImage.ts +205 -0
- package/src/videoPlayer.ts +318 -0
- package/src/yuvBlit.ts +331 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
#include "VideoCompositionFramesExtractorHostObject.h"
|
|
2
|
+
|
|
3
|
+
#import "AVAssetTrackUtils.h"
|
|
4
|
+
#import "AudioCompositionUtils.h"
|
|
5
|
+
#import "RNSVJSIUtils.h"
|
|
6
|
+
#include "RNSVCheckedSizes.h"
|
|
7
|
+
#import <AVFoundation/AVFoundation.h>
|
|
8
|
+
#import <Foundation/Foundation.h>
|
|
9
|
+
|
|
10
|
+
namespace RNSkiaVideo {
|
|
11
|
+
|
|
12
|
+
VideoCompositionFramesExtractorHostObject::
|
|
13
|
+
VideoCompositionFramesExtractorHostObject(
|
|
14
|
+
jsi::Runtime& runtime, std::shared_ptr<react::CallInvoker> callInvoker,
|
|
15
|
+
std::shared_ptr<VideoComposition> videoComposition)
|
|
16
|
+
: EventEmitter(runtime, callInvoker), composition(videoComposition),
|
|
17
|
+
window(videoComposition->lazyDecoders, true) {
|
|
18
|
+
lock = [[NSObject alloc] init];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
VideoCompositionFramesExtractorHostObject::
|
|
22
|
+
~VideoCompositionFramesExtractorHostObject() {
|
|
23
|
+
this->release();
|
|
24
|
+
// The queued blocks hold this, a reader being opened among them: this
|
|
25
|
+
// returns once they have all run.
|
|
26
|
+
if (decoderQueue && dispatch_get_specific(this) != this) {
|
|
27
|
+
dispatch_sync(decoderQueue, ^{
|
|
28
|
+
return;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
std::vector<jsi::PropNameID>
|
|
34
|
+
VideoCompositionFramesExtractorHostObject::getPropertyNames(jsi::Runtime& rt) {
|
|
35
|
+
std::vector<jsi::PropNameID> result;
|
|
36
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("prepare")));
|
|
37
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("play")));
|
|
38
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("pause")));
|
|
39
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("seekTo")));
|
|
40
|
+
result.push_back(
|
|
41
|
+
jsi::PropNameID::forUtf8(rt, std::string("decodeCompositionFrames")));
|
|
42
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("on")));
|
|
43
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("dispose")));
|
|
44
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("currentTime")));
|
|
45
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("framesVersion")));
|
|
46
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isLooping")));
|
|
47
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isPlaying")));
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The methods are created once per runtime (see RNSVHostObject):
|
|
52
|
+
// `decodeCompositionFrames` is read at every vsync of the UI runtime.
|
|
53
|
+
jsi::Value VideoCompositionFramesExtractorHostObject::get(
|
|
54
|
+
jsi::Runtime& runtime, const jsi::PropNameID& propNameId) {
|
|
55
|
+
@synchronized(lock) {
|
|
56
|
+
auto propName = propNameId.utf8(runtime);
|
|
57
|
+
if (propName == "prepare") {
|
|
58
|
+
return getFunction(
|
|
59
|
+
runtime, propName, 0,
|
|
60
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
61
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
62
|
+
@synchronized(lock) { if (!released.test()) {
|
|
63
|
+
prepare();
|
|
64
|
+
} }
|
|
65
|
+
return jsi::Value::undefined();
|
|
66
|
+
});
|
|
67
|
+
} else if (propName == "play") {
|
|
68
|
+
return getFunction(
|
|
69
|
+
runtime, propName, 0,
|
|
70
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
71
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
72
|
+
@synchronized(lock) { if (!released.test()) {
|
|
73
|
+
if (initialized) {
|
|
74
|
+
play();
|
|
75
|
+
} else {
|
|
76
|
+
playWhenReady = true;
|
|
77
|
+
}
|
|
78
|
+
} }
|
|
79
|
+
return jsi::Value::undefined();
|
|
80
|
+
});
|
|
81
|
+
} else if (propName == "pause") {
|
|
82
|
+
return getFunction(
|
|
83
|
+
runtime, propName, 0,
|
|
84
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
85
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
86
|
+
@synchronized(lock) { if (!released.test()) {
|
|
87
|
+
if (initialized) {
|
|
88
|
+
pause();
|
|
89
|
+
} else {
|
|
90
|
+
playWhenReady = false;
|
|
91
|
+
}
|
|
92
|
+
} }
|
|
93
|
+
return jsi::Value::undefined();
|
|
94
|
+
});
|
|
95
|
+
} else if (propName == "seekTo") {
|
|
96
|
+
return getFunction(
|
|
97
|
+
runtime, propName, 1,
|
|
98
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
99
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
100
|
+
if (count < 1 || !arguments[0].isNumber())
|
|
101
|
+
throw jsi::JSError(runtime, "seekTo expects media seconds");
|
|
102
|
+
auto seconds = checkedMediaSeconds(arguments[0].asNumber());
|
|
103
|
+
if (!released.test()) {
|
|
104
|
+
seekTo(CMTimeMakeWithSeconds(seconds, NSEC_PER_SEC));
|
|
105
|
+
}
|
|
106
|
+
return jsi::Value::undefined();
|
|
107
|
+
});
|
|
108
|
+
} else if (propName == "decodeCompositionFrames") {
|
|
109
|
+
return getFunction(
|
|
110
|
+
runtime, propName, 0,
|
|
111
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
112
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
113
|
+
scheduleDecode();
|
|
114
|
+
@synchronized(lock) {
|
|
115
|
+
if (released.test()) {
|
|
116
|
+
return jsi::Object(runtime);
|
|
117
|
+
}
|
|
118
|
+
if (initialized && lastAppliedSeekGeneration == seekGeneration) {
|
|
119
|
+
auto currentTime = getCurrentTime();
|
|
120
|
+
bool changed = false;
|
|
121
|
+
for (const auto& entry : itemDecoders) {
|
|
122
|
+
auto itemId = entry.first;
|
|
123
|
+
auto decoder = entry.second;
|
|
124
|
+
auto previousFrame = currentFrames[itemId];
|
|
125
|
+
// Forced after a seek as well as when there is nothing on
|
|
126
|
+
// screen: the frames the new reader hands back start at the
|
|
127
|
+
// keyframe before the target, so waiting for one stamped at
|
|
128
|
+
// or past it holds the pre-seek picture for up to a GOP —
|
|
129
|
+
// the scrub looks stuck, then jumps.
|
|
130
|
+
auto frame = decoder->acquireFrameForTime(
|
|
131
|
+
currentTime, !previousFrame || seekPending);
|
|
132
|
+
if (frame) {
|
|
133
|
+
currentFrames[itemId] = frame;
|
|
134
|
+
changed = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (changed) {
|
|
138
|
+
framesVersion++;
|
|
139
|
+
seekPending = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// The frames object is only rebuilt when a decoder produced a
|
|
143
|
+
// new frame. On a 120 Hz display most calls see the same frames
|
|
144
|
+
// as the previous one, and rewrapping them would be one JS
|
|
145
|
+
// allocation per item per vsync.
|
|
146
|
+
return getVersionedObject(
|
|
147
|
+
runtime, "frames", (double)framesVersion,
|
|
148
|
+
[&](jsi::Object& frames) {
|
|
149
|
+
for (const auto& entry : currentFrames) {
|
|
150
|
+
if (!entry.second) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
frames.setProperty(runtime, entry.first.c_str(),
|
|
154
|
+
jsi::Object::createFromHostObject(
|
|
155
|
+
runtime, entry.second));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
} else if (propName == "on") {
|
|
161
|
+
return getFunction(
|
|
162
|
+
runtime, propName, 2,
|
|
163
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
164
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
165
|
+
@synchronized(lock) {
|
|
166
|
+
if (released.test()) {
|
|
167
|
+
// Nothing to listen to anymore: hand out a no-op unsubscribe.
|
|
168
|
+
return jsi::Function::createFromHostFunction(
|
|
169
|
+
runtime, jsi::PropNameID::forAscii(runtime, "dispose"), 0,
|
|
170
|
+
[](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
171
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
172
|
+
return jsi::Value::undefined();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (count < 2 || !arguments[0].isString() || !arguments[1].isObject() ||
|
|
176
|
+
!arguments[1].asObject(runtime).isFunction(runtime))
|
|
177
|
+
throw jsi::JSError(runtime, "on expects an event name and listener");
|
|
178
|
+
auto name = arguments[0].asString(runtime).utf8(runtime);
|
|
179
|
+
auto handler = arguments[1].asObject(runtime).asFunction(runtime);
|
|
180
|
+
return this->on(name, std::move(handler));
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
} else if (propName == "dispose") {
|
|
184
|
+
return getFunction(
|
|
185
|
+
runtime, propName, 0,
|
|
186
|
+
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
187
|
+
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
188
|
+
this->release();
|
|
189
|
+
return jsi::Value::undefined();
|
|
190
|
+
});
|
|
191
|
+
} else if (propName == "currentTime") {
|
|
192
|
+
return jsi::Value(released.test() ? 0 : CMTimeGetSeconds(getCurrentTime()));
|
|
193
|
+
} else if (propName == "framesVersion") {
|
|
194
|
+
return jsi::Value(released.test() ? 0 : (double)framesVersion);
|
|
195
|
+
} else if (propName == "isLooping") {
|
|
196
|
+
return jsi::Value(!released.test() && isLooping);
|
|
197
|
+
} else if (propName == "isPlaying") {
|
|
198
|
+
return jsi::Value(!released.test() && isPlaying);
|
|
199
|
+
}
|
|
200
|
+
return jsi::Value::undefined();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
void VideoCompositionFramesExtractorHostObject::set(
|
|
205
|
+
jsi::Runtime& runtime, const jsi::PropNameID& propNameId,
|
|
206
|
+
const jsi::Value& value) {
|
|
207
|
+
@synchronized(lock) {
|
|
208
|
+
if (released.test()) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
auto propName = propNameId.utf8(runtime);
|
|
213
|
+
if (propName == "isLooping") {
|
|
214
|
+
isLooping = value.asBool();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
void VideoCompositionFramesExtractorHostObject::prepare() {
|
|
219
|
+
@synchronized(lock) {
|
|
220
|
+
if (released.test() || decoderQueue) return;
|
|
221
|
+
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(
|
|
222
|
+
DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
|
|
223
|
+
decoderQueue = dispatch_queue_create("ReactNativeVideoCompositionItemDecoder", attr);
|
|
224
|
+
dispatch_queue_set_specific(decoderQueue, this, this, NULL);
|
|
225
|
+
std::weak_ptr<VideoCompositionFramesExtractorHostObject> weak =
|
|
226
|
+
std::static_pointer_cast<VideoCompositionFramesExtractorHostObject>(shared_from_this());
|
|
227
|
+
dispatch_async(decoderQueue, ^{
|
|
228
|
+
if (auto owner = weak.lock()) owner->init();
|
|
229
|
+
});
|
|
230
|
+
displayLink = [[RNSVDisplayLinkWrapper alloc]
|
|
231
|
+
initWithUpdateBlock:^(CADisplayLink* displayLink) {
|
|
232
|
+
if (auto owner = weak.lock()) owner->scheduleDecode();
|
|
233
|
+
}];
|
|
234
|
+
[displayLink start];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Both display ticks and explicit seeks enter one serial decoder queue.
|
|
238
|
+
// A paused/occluded surface must still service a seek even when CADisplayLink
|
|
239
|
+
// is throttled. Coalescing bounds outstanding work to one queued tick.
|
|
240
|
+
void VideoCompositionFramesExtractorHostObject::scheduleDecode() {
|
|
241
|
+
@synchronized(lock) {
|
|
242
|
+
if (released.test() || !decoderQueue || decoderTickPending.exchange(true)) return;
|
|
243
|
+
std::weak_ptr<VideoCompositionFramesExtractorHostObject> weak =
|
|
244
|
+
std::static_pointer_cast<VideoCompositionFramesExtractorHostObject>(shared_from_this());
|
|
245
|
+
dispatch_async(decoderQueue, ^{
|
|
246
|
+
auto owner = weak.lock();
|
|
247
|
+
if (!owner) return;
|
|
248
|
+
uint64_t scheduledGeneration;
|
|
249
|
+
@synchronized(owner->lock) { scheduledGeneration = owner->seekGeneration; }
|
|
250
|
+
@autoreleasepool {
|
|
251
|
+
struct PendingGuard {
|
|
252
|
+
std::atomic<bool>& flag;
|
|
253
|
+
~PendingGuard() { flag.store(false); }
|
|
254
|
+
} pendingGuard{owner->decoderTickPending};
|
|
255
|
+
owner->decodeTick();
|
|
256
|
+
}
|
|
257
|
+
// A seek arriving during decoding was coalesced into this task. Queue
|
|
258
|
+
// its latest position after releasing the pending flag, so it cannot
|
|
259
|
+
// be lost when the display link is idle.
|
|
260
|
+
@synchronized(owner->lock) {
|
|
261
|
+
if (!owner->released.test() &&
|
|
262
|
+
scheduledGeneration != owner->seekGeneration) {
|
|
263
|
+
owner->scheduleDecode();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
void VideoCompositionFramesExtractorHostObject::decodeTick() {
|
|
271
|
+
auto* self = this;
|
|
272
|
+
std::vector<std::shared_ptr<VideoCompositionItemDecoder>> advancing;
|
|
273
|
+
std::vector<std::shared_ptr<VideoCompositionItem>> opening;
|
|
274
|
+
std::vector<std::shared_ptr<VideoCompositionItemDecoder>> closing;
|
|
275
|
+
CMTime currentTime = kCMTimeZero;
|
|
276
|
+
uint64_t generation = 0;
|
|
277
|
+
@synchronized(self->lock) {
|
|
278
|
+
if (self->released.test() || !self->initialized) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
currentTime = self->getCurrentTime();
|
|
282
|
+
if (CMTimeGetSeconds(currentTime) >= self->composition->duration) {
|
|
283
|
+
if (!self->completeEmitted) {
|
|
284
|
+
self->completeEmitted = true;
|
|
285
|
+
self->emit("complete", jsi::Value::null());
|
|
286
|
+
}
|
|
287
|
+
if (self->isLooping) {
|
|
288
|
+
currentTime = kCMTimeZero;
|
|
289
|
+
self->startDate = [NSDate date];
|
|
290
|
+
if (self->audioPlayer) {
|
|
291
|
+
[self->audioPlayer seekToTime:kCMTimeZero
|
|
292
|
+
toleranceBefore:kCMTimeZero
|
|
293
|
+
toleranceAfter:kCMTimeZero
|
|
294
|
+
completionHandler:^(BOOL){
|
|
295
|
+
}];
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// Switching to the paused clock must preserve the completed position;
|
|
299
|
+
// leaving its old value (usually zero) rewinds after completion.
|
|
300
|
+
self->pausePosition = CMTimeMakeWithSeconds(
|
|
301
|
+
self->composition->duration, NSEC_PER_SEC);
|
|
302
|
+
self->isPlaying = false;
|
|
303
|
+
if (self->audioPlayer) {
|
|
304
|
+
[self->audioPlayer pause];
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
self->completeEmitted = false;
|
|
310
|
+
}
|
|
311
|
+
generation = self->seekGeneration;
|
|
312
|
+
self->updateWindow(currentTime, opening, closing);
|
|
313
|
+
}
|
|
314
|
+
// Readers are slow to open and close: not while the UI thread waits
|
|
315
|
+
// on the self->lock for its frames.
|
|
316
|
+
for (const auto& decoder : closing) {
|
|
317
|
+
decoder->release();
|
|
318
|
+
}
|
|
319
|
+
closing.clear();
|
|
320
|
+
if (!opening.empty()) {
|
|
321
|
+
self->openDecoders(opening, currentTime, generation);
|
|
322
|
+
}
|
|
323
|
+
@synchronized(self->lock) {
|
|
324
|
+
if (self->released.test() || generation != self->seekGeneration) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// Paused where every decoder has already been advanced to: there
|
|
328
|
+
// is nothing to decode until the position moves or a seek lands.
|
|
329
|
+
if (!self->isPlaying && opening.empty() &&
|
|
330
|
+
generation == self->lastAdvancedGeneration &&
|
|
331
|
+
CMTimeCompare(currentTime, self->lastAdvancedTime) == 0) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
for (const auto& entry : self->itemDecoders) {
|
|
335
|
+
if (entry.second) {
|
|
336
|
+
advancing.push_back(entry.second);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
bool applySeek = generation != self->lastAppliedSeekGeneration;
|
|
341
|
+
// In parallel on GCD's pool, instead of a new thread per decoder
|
|
342
|
+
// per vsync.
|
|
343
|
+
struct DecodeFailure { std::mutex mutex; std::string message; };
|
|
344
|
+
auto failure = std::make_shared<DecodeFailure>();
|
|
345
|
+
dispatch_apply(advancing.size(), DISPATCH_APPLY_AUTO, ^(size_t i) {
|
|
346
|
+
try {
|
|
347
|
+
if (applySeek) advancing[i]->seekTo(currentTime);
|
|
348
|
+
advancing[i]->advanceDecoder(currentTime);
|
|
349
|
+
}
|
|
350
|
+
catch (const std::exception& error) {
|
|
351
|
+
std::lock_guard<std::mutex> guard(failure->mutex);
|
|
352
|
+
if (failure->message.empty()) failure->message = error.what();
|
|
353
|
+
} catch (NSError* error) {
|
|
354
|
+
std::lock_guard<std::mutex> guard(failure->mutex);
|
|
355
|
+
if (failure->message.empty())
|
|
356
|
+
failure->message = [[error localizedDescription] UTF8String];
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
if (!failure->message.empty()) {
|
|
360
|
+
@synchronized(self->lock) { self->pause(); }
|
|
361
|
+
auto message = failure->message;
|
|
362
|
+
self->emit("error", [message](jsi::Runtime& runtime) {
|
|
363
|
+
auto error = jsi::Object(runtime);
|
|
364
|
+
error.setProperty(runtime, "message", jsi::String::createFromUtf8(runtime, message));
|
|
365
|
+
return jsi::Value(std::move(error));
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
@synchronized(lock) {
|
|
369
|
+
if (!released.test() && generation == seekGeneration) {
|
|
370
|
+
lastAppliedSeekGeneration = generation;
|
|
371
|
+
if (failure->message.empty()) {
|
|
372
|
+
lastAdvancedTime = currentTime;
|
|
373
|
+
lastAdvancedGeneration = generation;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
void VideoCompositionFramesExtractorHostObject::init() {
|
|
380
|
+
@synchronized(lock) {
|
|
381
|
+
if (released.test()) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
// Assets are shared between the video decoders and the audio
|
|
386
|
+
// composition so a same file is never opened twice.
|
|
387
|
+
assetCache = [NSMutableDictionary dictionary];
|
|
388
|
+
for (const auto& item : composition->items) {
|
|
389
|
+
if (!item->isVideo || !opensAt(item, CMTimeGetSeconds(pausePosition))) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
itemDecoders[item->id] = std::make_shared<VideoCompositionItemDecoder>(
|
|
393
|
+
item, true, getOrCreateAsset(item->path, assetCache));
|
|
394
|
+
}
|
|
395
|
+
if (composition->hasAudio()) {
|
|
396
|
+
auto audioComposition = buildAudioComposition(composition, assetCache);
|
|
397
|
+
if (audioComposition.composition) {
|
|
398
|
+
AVPlayerItem* playerItem =
|
|
399
|
+
[AVPlayerItem playerItemWithAsset:audioComposition.composition];
|
|
400
|
+
if (audioComposition.audioMix) {
|
|
401
|
+
playerItem.audioMix = audioComposition.audioMix;
|
|
402
|
+
}
|
|
403
|
+
audioPlayer = [AVPlayer playerWithPlayerItem:playerItem];
|
|
404
|
+
audioPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
|
|
405
|
+
audioPlayer.automaticallyWaitsToMinimizeStalling = NO;
|
|
406
|
+
if (CMTimeCompare(pausePosition, kCMTimeZero) > 0) {
|
|
407
|
+
[audioPlayer seekToTime:pausePosition
|
|
408
|
+
toleranceBefore:kCMTimeZero
|
|
409
|
+
toleranceAfter:kCMTimeZero];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// A seek issued before init (a freshly created player positioned right
|
|
414
|
+
// away by the JS side) only reached `pausePosition` and, above, the
|
|
415
|
+
// audio player: the item decoders were still reading from zero and
|
|
416
|
+
// caught up with the clock by decoding every frame in between —
|
|
417
|
+
// visibly, a fast-forward from the start of the clip. Position them
|
|
418
|
+
// where the audio starts.
|
|
419
|
+
if (CMTimeCompare(pausePosition, kCMTimeZero) > 0) {
|
|
420
|
+
for (const auto& entry : itemDecoders) {
|
|
421
|
+
entry.second->seekTo(pausePosition);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch (const std::exception& error) {
|
|
425
|
+
itemDecoders.clear();
|
|
426
|
+
audioPlayer = nil;
|
|
427
|
+
assetCache = nil;
|
|
428
|
+
auto message = std::string(error.what());
|
|
429
|
+
emit("error", [message](jsi::Runtime& runtime) -> jsi::Value {
|
|
430
|
+
auto result = jsi::Object(runtime);
|
|
431
|
+
result.setProperty(runtime, "message", jsi::String::createFromUtf8(runtime, message));
|
|
432
|
+
return result;
|
|
433
|
+
});
|
|
434
|
+
return;
|
|
435
|
+
} catch (NSError* error) {
|
|
436
|
+
itemDecoders.clear();
|
|
437
|
+
audioPlayer = nil;
|
|
438
|
+
assetCache = nil;
|
|
439
|
+
emit("error", [=](jsi::Runtime& runtime) -> jsi::Value {
|
|
440
|
+
return RNSkiaVideo::NSErrorToJSI(runtime, error);
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
lastAppliedSeekGeneration = seekGeneration;
|
|
445
|
+
initialized = true;
|
|
446
|
+
if (playWhenReady) {
|
|
447
|
+
play();
|
|
448
|
+
}
|
|
449
|
+
this->emit("ready", jsi::Value::null());
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
void VideoCompositionFramesExtractorHostObject::play() {
|
|
454
|
+
@synchronized(lock) {
|
|
455
|
+
if (released.test() || isPlaying) return;
|
|
456
|
+
// A completed paused clock stays at the end until play explicitly restarts
|
|
457
|
+
// it. Rewind the readers through the same serial seek generation as seekTo.
|
|
458
|
+
if (CMTimeGetSeconds(pausePosition) >= composition->duration) {
|
|
459
|
+
seekTo(kCMTimeZero);
|
|
460
|
+
}
|
|
461
|
+
if (audioPlayer) {
|
|
462
|
+
if (CMTimeGetSeconds(audioPlayer.currentTime) >= composition->duration) {
|
|
463
|
+
[audioPlayer seekToTime:kCMTimeZero
|
|
464
|
+
toleranceBefore:kCMTimeZero
|
|
465
|
+
toleranceAfter:kCMTimeZero];
|
|
466
|
+
}
|
|
467
|
+
[audioPlayer play];
|
|
468
|
+
}
|
|
469
|
+
startDate =
|
|
470
|
+
[NSDate dateWithTimeIntervalSinceNow:-CMTimeGetSeconds(pausePosition)];
|
|
471
|
+
pausePosition = kCMTimeZero;
|
|
472
|
+
isPlaying = true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
void VideoCompositionFramesExtractorHostObject::pause() {
|
|
477
|
+
@synchronized(lock) {
|
|
478
|
+
if (!isPlaying) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
pausePosition = getCurrentTime();
|
|
482
|
+
isPlaying = false;
|
|
483
|
+
if (audioPlayer) {
|
|
484
|
+
[audioPlayer pause];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
void VideoCompositionFramesExtractorHostObject::seekTo(CMTime time) {
|
|
490
|
+
@synchronized(lock) {
|
|
491
|
+
if (released.test()) return;
|
|
492
|
+
seekPending = true;
|
|
493
|
+
if (isPlaying) {
|
|
494
|
+
startDate = [NSDate dateWithTimeIntervalSinceNow:-CMTimeGetSeconds(time)];
|
|
495
|
+
} else {
|
|
496
|
+
pausePosition = time;
|
|
497
|
+
}
|
|
498
|
+
if (audioPlayer) {
|
|
499
|
+
[audioPlayer seekToTime:time
|
|
500
|
+
toleranceBefore:kCMTimeZero
|
|
501
|
+
toleranceAfter:kCMTimeZero];
|
|
502
|
+
}
|
|
503
|
+
seekGeneration++;
|
|
504
|
+
// Reader mutation is applied by decodeTick on its serial queue.
|
|
505
|
+
scheduleDecode();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
bool VideoCompositionFramesExtractorHostObject::opensAt(
|
|
510
|
+
const std::shared_ptr<VideoCompositionItem>& item, double position) const {
|
|
511
|
+
return window.opens(item->compositionStartTime,
|
|
512
|
+
item->compositionStartTime + item->duration, position,
|
|
513
|
+
composition->duration, false);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Under the lock. Closed decoders leave the maps here and are released by the
|
|
517
|
+
// caller; opened ones are made by openDecoders.
|
|
518
|
+
void VideoCompositionFramesExtractorHostObject::updateWindow(
|
|
519
|
+
CMTime time, std::vector<std::shared_ptr<VideoCompositionItem>>& opening,
|
|
520
|
+
std::vector<std::shared_ptr<VideoCompositionItemDecoder>>& closing) {
|
|
521
|
+
if (!window.isLazy()) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
double position = CMTimeGetSeconds(time);
|
|
525
|
+
for (const auto& item : composition->items) {
|
|
526
|
+
if (!item->isVideo) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
double start = item->compositionStartTime;
|
|
530
|
+
double end = start + item->duration;
|
|
531
|
+
auto it = itemDecoders.find(item->id);
|
|
532
|
+
if (it == itemDecoders.end()) {
|
|
533
|
+
if (!failedItems.count(item->id) &&
|
|
534
|
+
window.opens(start, end, position, composition->duration,
|
|
535
|
+
isLooping)) {
|
|
536
|
+
#if defined(RNSV_TRACE_LIFECYCLE) && RNSV_TRACE_LIFECYCLE
|
|
537
|
+
NSLog(@"[rnskv] open %s at %.3fs", item->id.c_str(), position);
|
|
538
|
+
#endif
|
|
539
|
+
opening.push_back(item);
|
|
540
|
+
}
|
|
541
|
+
} else if (!window.keeps(start, end, position, composition->duration,
|
|
542
|
+
isLooping)) {
|
|
543
|
+
#if defined(RNSV_TRACE_LIFECYCLE) && RNSV_TRACE_LIFECYCLE
|
|
544
|
+
NSLog(@"[rnskv] close %s at %.3fs", item->id.c_str(), position);
|
|
545
|
+
#endif
|
|
546
|
+
closing.push_back(it->second);
|
|
547
|
+
itemDecoders.erase(it);
|
|
548
|
+
if (currentFrames.erase(item->id) > 0) {
|
|
549
|
+
framesVersion++;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// On the decoder queue, outside of the lock.
|
|
556
|
+
void VideoCompositionFramesExtractorHostObject::openDecoders(
|
|
557
|
+
const std::vector<std::shared_ptr<VideoCompositionItem>>& items,
|
|
558
|
+
CMTime time, uint64_t generation) {
|
|
559
|
+
for (const auto& item : items) {
|
|
560
|
+
std::shared_ptr<VideoCompositionItemDecoder> decoder;
|
|
561
|
+
NSError* failure = nil;
|
|
562
|
+
NSMutableDictionary<NSString*, AVURLAsset*>* cache;
|
|
563
|
+
@synchronized(lock) {
|
|
564
|
+
if (released.test()) return;
|
|
565
|
+
cache = assetCache;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
decoder = std::make_shared<VideoCompositionItemDecoder>(
|
|
569
|
+
item, true, getOrCreateAsset(item->path, cache), time);
|
|
570
|
+
} catch (const std::exception& error) {
|
|
571
|
+
failure = [NSError errorWithDomain:@"com.azzapp.rnskv" code:0
|
|
572
|
+
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithUTF8String:error.what()]}];
|
|
573
|
+
} catch (NSError* error) {
|
|
574
|
+
failure = error;
|
|
575
|
+
}
|
|
576
|
+
if (failure) {
|
|
577
|
+
// Decoder queue only, as updateWindow reads it.
|
|
578
|
+
failedItems.insert(item->id);
|
|
579
|
+
emit("error", [=](jsi::Runtime& runtime) -> jsi::Value {
|
|
580
|
+
return RNSkiaVideo::NSErrorToJSI(runtime, failure);
|
|
581
|
+
});
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
@synchronized(lock) {
|
|
585
|
+
if (released.test()) {
|
|
586
|
+
decoder->release();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (seekGeneration != generation) {
|
|
590
|
+
decoder->seekTo(getCurrentTime());
|
|
591
|
+
}
|
|
592
|
+
itemDecoders[item->id] = decoder;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
CMTime VideoCompositionFramesExtractorHostObject::getCurrentTime() {
|
|
598
|
+
@synchronized(lock) {
|
|
599
|
+
if (isPlaying) {
|
|
600
|
+
// When the composition has audio, the audio player is the master clock.
|
|
601
|
+
if (audioPlayer) {
|
|
602
|
+
CMTime time = audioPlayer.currentTime;
|
|
603
|
+
if (CMTIME_IS_NUMERIC(time)) {
|
|
604
|
+
return time;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
NSTimeInterval elapsedTime =
|
|
608
|
+
[[NSDate date] timeIntervalSinceDate:startDate];
|
|
609
|
+
return CMTimeMakeWithSeconds(elapsedTime, NSEC_PER_SEC);
|
|
610
|
+
} else {
|
|
611
|
+
return pausePosition;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
void VideoCompositionFramesExtractorHostObject::release() {
|
|
617
|
+
@synchronized(lock) {
|
|
618
|
+
if (released.test_and_set()) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
for (const auto& entry : itemDecoders) {
|
|
623
|
+
auto decoder = entry.second;
|
|
624
|
+
if (decoder) {
|
|
625
|
+
entry.second->release();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} catch (...) {
|
|
629
|
+
}
|
|
630
|
+
itemDecoders.clear();
|
|
631
|
+
currentFrames.clear();
|
|
632
|
+
assetCache = nil;
|
|
633
|
+
if (audioPlayer) {
|
|
634
|
+
[audioPlayer pause];
|
|
635
|
+
[audioPlayer replaceCurrentItemWithPlayerItem:nil];
|
|
636
|
+
audioPlayer = nil;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
removeAllListeners();
|
|
640
|
+
if (displayLink != nullptr) {
|
|
641
|
+
[displayLink invalidate];
|
|
642
|
+
displayLink = nullptr;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
} // namespace RNSkiaVideo
|
|
647
|
+
|
|
648
|
+
@implementation RNSVDisplayLinkWrapper
|
|
649
|
+
|
|
650
|
+
- (instancetype)initWithUpdateBlock:
|
|
651
|
+
(void (^)(CADisplayLink* displayLink))updateBlock {
|
|
652
|
+
self = [super init];
|
|
653
|
+
if (self) {
|
|
654
|
+
_updateBlock = [updateBlock copy];
|
|
655
|
+
_displayLink =
|
|
656
|
+
[CADisplayLink displayLinkWithTarget:self
|
|
657
|
+
selector:@selector(displayLinkFired:)];
|
|
658
|
+
}
|
|
659
|
+
return self;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
- (void)displayLinkFired:(CADisplayLink*)displayLink {
|
|
663
|
+
if (self.updateBlock) {
|
|
664
|
+
self.updateBlock(displayLink);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
- (void)start {
|
|
669
|
+
if ([NSThread isMainThread]) {
|
|
670
|
+
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
671
|
+
} else {
|
|
672
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [self start]; });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
- (void)invalidate {
|
|
677
|
+
if (![NSThread isMainThread]) {
|
|
678
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [self invalidate]; });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
[self.displayLink invalidate];
|
|
682
|
+
self.displayLink = nil;
|
|
683
|
+
self.updateBlock = nil;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
@end
|