react-native-skia-webgpu-video 0.1.0-beta.2 → 0.1.0-beta.4
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 +0 -1
- package/README.md +62 -34
- package/android/BUILD_VALIDATION.md +1 -1
- package/android/CMakeLists.txt +3 -2
- package/android/cpp/ExportHintSession.cpp +114 -0
- package/android/cpp/ExportHintSession.h +35 -0
- package/android/cpp/VideoComposition.cpp +12 -0
- package/android/cpp/VideoCompositionFramesExtractor.cpp +6 -0
- package/android/cpp/VideoCompositionFramesExtractor.h +1 -0
- package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +1 -1
- package/android/cpp/VideoEncoderHostObject.cpp +76 -101
- package/android/cpp/VideoEncoderHostObject.h +14 -9
- package/android/cpp/VideoFrame.cpp +3 -5
- package/android/cpp/cpp-adapter.cpp +7 -4
- package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +3 -9
- package/android/src/main/java/com/azzapp/rnskv/PreviewFrameRateVote.java +104 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +4 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +8 -9
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +32 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +68 -28
- package/docs/FRAME_PIXEL_FORMAT.md +124 -0
- package/docs/MIGRATION.md +1 -1
- package/docs/UPSTREAM_NOTES.md +15 -2
- package/docs/VERIFICATION_0.1.0-beta.3.md +147 -0
- package/ios/NativeResourceStats.h +9 -0
- package/ios/RNSVColorSpace.h +35 -4
- package/ios/ReactNativeSkiaVideo.mm +4 -0
- package/ios/VideoComposition.h +20 -0
- package/ios/VideoCompositionFramesExtractorHostObject.h +5 -0
- package/ios/VideoCompositionFramesExtractorHostObject.mm +31 -2
- package/ios/VideoCompositionItemDecoder.mm +21 -8
- package/ios/VideoFrame.mm +6 -0
- package/lib/module/canvas.js +3 -5
- package/lib/module/canvas.js.map +1 -1
- package/lib/module/exportFrames.js +25 -20
- package/lib/module/exportFrames.js.map +1 -1
- package/lib/module/exportTarget.js +183 -40
- package/lib/module/exportTarget.js.map +1 -1
- package/lib/module/exportVideoComposition.js +53 -12
- package/lib/module/exportVideoComposition.js.map +1 -1
- package/lib/module/frameInterop.js +15 -12
- package/lib/module/frameInterop.js.map +1 -1
- package/lib/module/gpu.js +0 -2
- package/lib/module/gpu.js.map +1 -1
- package/lib/module/gpuDevice.js +12 -0
- package/lib/module/gpuDevice.js.map +1 -1
- package/lib/module/previewLifecycle.js +0 -1
- package/lib/module/previewLifecycle.js.map +1 -1
- package/lib/module/videoCompositionPlayer.js +13 -5
- package/lib/module/videoCompositionPlayer.js.map +1 -1
- package/lib/module/yuvBlit.js +98 -21
- package/lib/module/yuvBlit.js.map +1 -1
- package/lib/typescript/src/canvas.d.ts.map +1 -1
- package/lib/typescript/src/exportFrames.d.ts +11 -10
- package/lib/typescript/src/exportFrames.d.ts.map +1 -1
- package/lib/typescript/src/exportTarget.d.ts +53 -14
- package/lib/typescript/src/exportTarget.d.ts.map +1 -1
- package/lib/typescript/src/exportVideoComposition.d.ts +2 -1
- package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -1
- package/lib/typescript/src/frameInterop.d.ts.map +1 -1
- package/lib/typescript/src/gpu.d.ts.map +1 -1
- package/lib/typescript/src/gpuDevice.d.ts +5 -1
- package/lib/typescript/src/gpuDevice.d.ts.map +1 -1
- package/lib/typescript/src/previewLifecycle.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +36 -5
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -1
- package/lib/typescript/src/yuvBlit.d.ts +26 -14
- package/lib/typescript/src/yuvBlit.d.ts.map +1 -1
- package/package.json +6 -6
- package/patches/react-native-skia+3.7.4.patch +146 -0
- package/scripts/skia-canvas-patch.json +22 -258
- package/src/canvas.ts +3 -7
- package/src/exportFrames.ts +53 -36
- package/src/exportTarget.ts +217 -56
- package/src/exportVideoComposition.ts +87 -26
- package/src/frameInterop.ts +26 -11
- package/src/gpu.ts +0 -2
- package/src/gpuDevice.ts +26 -1
- package/src/previewLifecycle.ts +0 -1
- package/src/types.ts +37 -5
- package/src/videoCompositionPlayer.ts +14 -4
- package/src/yuvBlit.ts +108 -27
- package/android/cpp/EncoderTargets.cpp +0 -226
- package/android/cpp/EncoderTargets.h +0 -70
- package/patches/react-native-skia+3.7.1.patch +0 -502
|
@@ -1,49 +1,36 @@
|
|
|
1
1
|
#include "RNSVCheckedSizes.h"
|
|
2
2
|
#include "VideoEncoderHostObject.h"
|
|
3
|
-
#include
|
|
4
|
-
#include <
|
|
3
|
+
#include <android/data_space.h>
|
|
4
|
+
#include <android/native_window_jni.h>
|
|
5
|
+
#include <dlfcn.h>
|
|
5
6
|
#include <fbjni/ByteBuffer.h>
|
|
6
7
|
#include <cmath>
|
|
7
8
|
#include <limits>
|
|
8
|
-
#include <unistd.h>
|
|
9
9
|
|
|
10
10
|
namespace RNSkiaVideo {
|
|
11
11
|
using namespace facebook::jni;
|
|
12
12
|
|
|
13
13
|
namespace {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
public:
|
|
17
|
-
explicit ScopedEncoderContext(const global_ref<VideoEncoder>& encoder)
|
|
18
|
-
: display(eglGetCurrentDisplay()), context(eglGetCurrentContext()),
|
|
19
|
-
draw(eglGetCurrentSurface(EGL_DRAW)), read(eglGetCurrentSurface(EGL_READ)) {
|
|
20
|
-
encoder->makeGLContextCurrent();
|
|
21
|
-
}
|
|
22
|
-
~ScopedEncoderContext() {
|
|
23
|
-
if (context != EGL_NO_CONTEXT) {
|
|
24
|
-
eglMakeCurrent(display, draw, read, context);
|
|
25
|
-
} else if (auto current = eglGetCurrentDisplay(); current != EGL_NO_DISPLAY) {
|
|
26
|
-
eglMakeCurrent(current, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
ScopedEncoderContext(const ScopedEncoderContext&) = delete;
|
|
30
|
-
ScopedEncoderContext& operator=(const ScopedEncoderContext&) = delete;
|
|
31
|
-
|
|
32
|
-
private:
|
|
33
|
-
EGLDisplay display;
|
|
34
|
-
EGLContext context;
|
|
35
|
-
EGLSurface draw;
|
|
36
|
-
EGLSurface read;
|
|
14
|
+
struct JSurface : JavaClass<JSurface> {
|
|
15
|
+
static constexpr auto kJavaDescriptor = "Landroid/view/Surface;";
|
|
37
16
|
};
|
|
38
17
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Sets the presentation time of the next buffer queued to a window, as
|
|
20
|
+
* eglPresentationTimeANDROID does: the codec reads it as the frame's time.
|
|
21
|
+
* libnativewindow exports it on Android 10 and later, though the NDK does not
|
|
22
|
+
* declare it (it is in the LL-NDK); null where it is missing.
|
|
23
|
+
*/
|
|
24
|
+
using SetBuffersTimestamp = int32_t (*)(ANativeWindow*, int64_t);
|
|
25
|
+
SetBuffersTimestamp setBuffersTimestamp() {
|
|
26
|
+
static const auto function = []() -> SetBuffersTimestamp {
|
|
27
|
+
void* library = dlopen("libnativewindow.so", RTLD_NOW);
|
|
28
|
+
return library == nullptr ? nullptr
|
|
29
|
+
: reinterpret_cast<SetBuffersTimestamp>(dlsym(
|
|
30
|
+
library, "ANativeWindow_setBuffersTimestamp"));
|
|
31
|
+
}();
|
|
32
|
+
return function;
|
|
33
|
+
}
|
|
47
34
|
} // namespace
|
|
48
35
|
|
|
49
36
|
local_ref<VideoEncoder>
|
|
@@ -79,12 +66,6 @@ void VideoEncoder::prepare() const {
|
|
|
79
66
|
prepareMethod(self());
|
|
80
67
|
}
|
|
81
68
|
|
|
82
|
-
void VideoEncoder::makeGLContextCurrent() const {
|
|
83
|
-
static const auto makeGLContextCurrentMethod =
|
|
84
|
-
getClass()->getMethod<void()>("makeGLContextCurrent");
|
|
85
|
-
makeGLContextCurrentMethod(self());
|
|
86
|
-
}
|
|
87
|
-
|
|
88
69
|
void VideoEncoder::encodeFrameRgba(alias_ref<JByteBuffer> pixels, jint width,
|
|
89
70
|
jint height, jint bytesPerRow, jdouble time) const {
|
|
90
71
|
static const auto method =
|
|
@@ -92,9 +73,21 @@ void VideoEncoder::encodeFrameRgba(alias_ref<JByteBuffer> pixels, jint width,
|
|
|
92
73
|
method(self(), pixels, width, height, bytesPerRow, time);
|
|
93
74
|
}
|
|
94
75
|
|
|
95
|
-
|
|
96
|
-
static const auto method =
|
|
97
|
-
|
|
76
|
+
local_ref<jobject> VideoEncoder::getInputSurface() const {
|
|
77
|
+
static const auto method =
|
|
78
|
+
getClass()->getMethod<JSurface::javaobject()>("getInputSurface");
|
|
79
|
+
auto surface = method(self());
|
|
80
|
+
return make_local(static_cast<jobject>(surface.get()));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
jlong VideoEncoder::beginWindowFrame(jdouble time) const {
|
|
84
|
+
static const auto method = getClass()->getMethod<jlong(jdouble)>("beginWindowFrame");
|
|
85
|
+
return method(self(), time);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
void VideoEncoder::endWindowFrame(jlong timeUs) const {
|
|
89
|
+
static const auto method = getClass()->getMethod<void(jlong)>("endWindowFrame");
|
|
90
|
+
method(self(), timeUs);
|
|
98
91
|
}
|
|
99
92
|
|
|
100
93
|
void VideoEncoder::release() const {
|
|
@@ -112,11 +105,11 @@ VideoEncoderHostObject::VideoEncoderHostObject(
|
|
|
112
105
|
std::string& outPath, int width, int height, int frameRate, int bitRate,
|
|
113
106
|
std::optional<std::string> encoderName, std::optional<std::string> codec,
|
|
114
107
|
alias_ref<VideoComposition> composition, int audioSampleRate,
|
|
115
|
-
int audioChannelCount, int audioBitRate)
|
|
116
|
-
: width(width), height(height) {
|
|
108
|
+
int audioChannelCount, int audioBitRate) {
|
|
117
109
|
framesExtractor = make_global(VideoEncoder::create(
|
|
118
110
|
outPath, width, height, frameRate, bitRate, encoderName, codec,
|
|
119
111
|
composition, audioSampleRate, audioChannelCount, audioBitRate));
|
|
112
|
+
hints = std::make_unique<ExportHintSession>(frameRate);
|
|
120
113
|
}
|
|
121
114
|
|
|
122
115
|
VideoEncoderHostObject::~VideoEncoderHostObject() {
|
|
@@ -128,8 +121,7 @@ VideoEncoderHostObject::getPropertyNames(jsi::Runtime& rt) {
|
|
|
128
121
|
std::vector<jsi::PropNameID> result;
|
|
129
122
|
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("prepare")));
|
|
130
123
|
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("encodeFrame")));
|
|
131
|
-
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("
|
|
132
|
-
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("releaseFrameBuffer")));
|
|
124
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("acquireInputWindow")));
|
|
133
125
|
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("finishWriting")));
|
|
134
126
|
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("dispose")));
|
|
135
127
|
return result;
|
|
@@ -155,39 +147,31 @@ jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
|
|
|
155
147
|
auto frame = arguments[0].asObject(runtime);
|
|
156
148
|
const auto kindValue = frame.getProperty(runtime, "kind");
|
|
157
149
|
const auto kind = kindValue.isString() ? kindValue.asString(runtime).utf8(runtime) : "";
|
|
158
|
-
if (kind == "
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!value.isBigInt()) throw jsi::JSError(runtime, "Encoder fences must be sync-fd BigInts");
|
|
167
|
-
// WebGPU reports an already signaled fence as -1, as a uint32.
|
|
168
|
-
fences.fds.push_back(static_cast<int>(
|
|
169
|
-
static_cast<uint32_t>(value.asBigInt(runtime).getUint64(runtime))));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const auto pointerValue = frame.getProperty(runtime, "nativeBuffer");
|
|
173
|
-
if (!pointerValue.isBigInt() || !targets)
|
|
174
|
-
throw jsi::JSError(runtime, "Encoder frame buffer is not the one lent");
|
|
175
|
-
const auto* buffer = reinterpret_cast<const AHardwareBuffer*>(
|
|
176
|
-
static_cast<uintptr_t>(pointerValue.asBigInt(runtime).getUint64(runtime)));
|
|
150
|
+
if (kind == "window") {
|
|
151
|
+
// The GPU export rendered the frame into the canvas context of the
|
|
152
|
+
// input window: `present` queues it, with the time set here.
|
|
153
|
+
if (window == nullptr)
|
|
154
|
+
throw jsi::JSError(runtime, "Encoder input window was not acquired");
|
|
155
|
+
auto present = frame.getProperty(runtime, "present");
|
|
156
|
+
if (!present.isObject() || !present.asObject(runtime).isFunction(runtime))
|
|
157
|
+
throw jsi::JSError(runtime, "A window frame needs its present function");
|
|
177
158
|
const auto time = checkedMediaSeconds(arguments[1].asNumber());
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
throw;
|
|
185
|
-
|
|
186
|
-
|
|
159
|
+
const auto timeUs = framesExtractor->beginWindowFrame(time);
|
|
160
|
+
// The swapchain tags its buffers sRGB, full range, and the codec
|
|
161
|
+
// would encode and label the file so. Unknown, as EGL leaves it,
|
|
162
|
+
// lets the codec use its default: Rec.709, limited range. Set for
|
|
163
|
+
// every frame, as a recreated swapchain tags the window again.
|
|
164
|
+
if (ANativeWindow_setBuffersDataSpace(window, ADATASPACE_UNKNOWN) != 0)
|
|
165
|
+
throw jsi::JSError(runtime, "Cannot set the encoder frame dataspace");
|
|
166
|
+
if (setBuffersTimestamp()(window, static_cast<int64_t>(timeUs) * 1000) != 0)
|
|
167
|
+
throw jsi::JSError(runtime, "Cannot set the encoder frame time");
|
|
168
|
+
present.asObject(runtime).asFunction(runtime).call(runtime);
|
|
169
|
+
framesExtractor->endWindowFrame(timeUs);
|
|
170
|
+
hints->frameDone();
|
|
187
171
|
return jsi::Value::undefined();
|
|
188
172
|
}
|
|
189
173
|
if (kind != "rgba") {
|
|
190
|
-
throw jsi::JSError(runtime, "Android encoder accepts RGBA pixels or a
|
|
174
|
+
throw jsi::JSError(runtime, "Android encoder accepts RGBA pixels or a window frame");
|
|
191
175
|
}
|
|
192
176
|
const auto checkedInteger = [&](const char* name) -> int {
|
|
193
177
|
const auto value = frame.getProperty(runtime, name).asNumber();
|
|
@@ -234,7 +218,7 @@ jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
|
|
|
234
218
|
framesExtractor->encodeFrameRgba(javaPixels, width, height, stride, checkedMediaSeconds(arguments[1].asNumber()));
|
|
235
219
|
return jsi::Value::undefined();
|
|
236
220
|
});
|
|
237
|
-
} else if (propName == "
|
|
221
|
+
} else if (propName == "acquireInputWindow") {
|
|
238
222
|
return getFunction(
|
|
239
223
|
runtime, propName, 0,
|
|
240
224
|
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
@@ -243,22 +227,17 @@ jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
|
|
|
243
227
|
if (released.test() || !prepared || finished) {
|
|
244
228
|
throw jsi::JSError(runtime, "Encoder not prepared");
|
|
245
229
|
}
|
|
246
|
-
//
|
|
247
|
-
if (
|
|
248
|
-
throw jsi::JSError(runtime, "
|
|
230
|
+
// Without it, every frame would carry the time it was queued at.
|
|
231
|
+
if (setBuffersTimestamp() == nullptr) {
|
|
232
|
+
throw jsi::JSError(runtime, "Encoder frame times need Android 10 or later");
|
|
249
233
|
}
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
258
|
-
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
259
|
-
std::lock_guard<std::recursive_mutex> lock(encoderMutex);
|
|
260
|
-
if (targets) targets->giveBack();
|
|
261
|
-
return jsi::Value::undefined();
|
|
234
|
+
if (window == nullptr) {
|
|
235
|
+
auto surface = framesExtractor->getInputSurface();
|
|
236
|
+
window = ANativeWindow_fromSurface(Environment::current(), surface.get());
|
|
237
|
+
if (window == nullptr)
|
|
238
|
+
throw jsi::JSError(runtime, "Cannot get the encoder input window");
|
|
239
|
+
}
|
|
240
|
+
return jsi::BigInt::fromUint64(runtime, reinterpret_cast<uintptr_t>(window));
|
|
262
241
|
});
|
|
263
242
|
} else if (propName == "prepare") {
|
|
264
243
|
return getFunction(
|
|
@@ -300,16 +279,12 @@ jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
|
|
|
300
279
|
void VideoEncoderHostObject::release() {
|
|
301
280
|
std::lock_guard<std::recursive_mutex> lock(encoderMutex);
|
|
302
281
|
if (!released.test_and_set()) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
ScopedEncoderContext scope(framesExtractor);
|
|
308
|
-
targets->clear();
|
|
309
|
-
} catch (...) {
|
|
310
|
-
}
|
|
282
|
+
// A canvas context presenting to the window holds its own reference.
|
|
283
|
+
if (window != nullptr) {
|
|
284
|
+
ANativeWindow_release(window);
|
|
285
|
+
window = nullptr;
|
|
311
286
|
}
|
|
312
|
-
|
|
287
|
+
hints.reset();
|
|
313
288
|
framesExtractor->release();
|
|
314
289
|
framesExtractor = nullptr;
|
|
315
290
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
-
#include "
|
|
3
|
+
#include "ExportHintSession.h"
|
|
4
4
|
#include "RNSVHostObject.h"
|
|
5
5
|
#include "VideoComposition.h"
|
|
6
|
+
#include <android/native_window.h>
|
|
6
7
|
#include <memory>
|
|
7
8
|
#include <mutex>
|
|
8
9
|
#include <fbjni/ByteBuffer.h>
|
|
@@ -34,13 +35,17 @@ public:
|
|
|
34
35
|
|
|
35
36
|
void prepare() const;
|
|
36
37
|
|
|
37
|
-
void makeGLContextCurrent() const;
|
|
38
|
-
|
|
39
38
|
void encodeFrameRgba(alias_ref<JByteBuffer> pixels, jint width, jint height,
|
|
40
39
|
jint bytesPerRow, jdouble time) const;
|
|
41
40
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
41
|
+
/** The codec's input Surface (an android.view.Surface). */
|
|
42
|
+
local_ref<jobject> getInputSurface() const;
|
|
43
|
+
|
|
44
|
+
/** Before a frame is presented to the input Surface: its time in µs. */
|
|
45
|
+
jlong beginWindowFrame(jdouble time) const;
|
|
46
|
+
|
|
47
|
+
/** Once the frame of beginWindowFrame was presented. */
|
|
48
|
+
void endWindowFrame(jlong timeUs) const;
|
|
44
49
|
|
|
45
50
|
void finishWriting() const;
|
|
46
51
|
|
|
@@ -62,10 +67,10 @@ public:
|
|
|
62
67
|
|
|
63
68
|
private:
|
|
64
69
|
global_ref<VideoEncoder> framesExtractor;
|
|
65
|
-
// GPU export:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// GPU export: the codec's input Surface, which WebGPU presents to.
|
|
71
|
+
ANativeWindow* window = nullptr;
|
|
72
|
+
// GPU export: the system's hints, per presented frame.
|
|
73
|
+
std::unique_ptr<ExportHintSession> hints;
|
|
69
74
|
std::recursive_mutex encoderMutex;
|
|
70
75
|
bool prepared = false;
|
|
71
76
|
bool finished = false;
|
|
@@ -138,13 +138,11 @@ jsi::Value VideoFrame::toJSInternal(jsi::Runtime& runtime) {
|
|
|
138
138
|
if (isHardwareBuffer()) {
|
|
139
139
|
auto owner = acquireHardwareFrame(getNativeHardwareBufferHandle());
|
|
140
140
|
// An ImageReader frame is the codec's YUV buffer: its picture is the crop
|
|
141
|
-
// rectangle, turned for display
|
|
141
|
+
// rectangle, turned for display and scaled to the frame's size when
|
|
142
|
+
// WebGPU converts it.
|
|
142
143
|
const bool decodedImage = owner->image != nullptr;
|
|
143
|
-
const bool swapped = owner->rotation == 90 || owner->rotation == 270;
|
|
144
144
|
const bool valid = decodedImage
|
|
145
|
-
? owner->description.format != AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM
|
|
146
|
-
(swapped ? owner->cropHeight : owner->cropWidth) == width &&
|
|
147
|
-
(swapped ? owner->cropWidth : owner->cropHeight) == height
|
|
145
|
+
? owner->description.format != AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM
|
|
148
146
|
: owner->description.format == AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM &&
|
|
149
147
|
owner->description.width == static_cast<uint32_t>(width) &&
|
|
150
148
|
owner->description.height == static_cast<uint32_t>(height);
|
|
@@ -38,12 +38,16 @@ void install(jsi::Runtime& jsiRuntime) {
|
|
|
38
38
|
const bool hardware = NativeHardwareBuffer::isEnabled();
|
|
39
39
|
info.setProperty(runtime, "decodeTransport", jsi::String::createFromAscii(runtime,
|
|
40
40
|
hardware ? "ahardwarebuffer-rgba-egl" : "cpu-rgba-readback"));
|
|
41
|
-
// Export renders into
|
|
42
|
-
//
|
|
41
|
+
// Export renders straight into the codec's input Surface through a
|
|
42
|
+
// WebGPU canvas context; the JS export probes it at setup, and CPU
|
|
43
|
+
// readback stays the fallback. Offered with AHardwareBuffer interop,
|
|
44
|
+
// which also samples decoded frames in place: turning interop off
|
|
45
|
+
// selects the readback export, the reference of the pixel checks.
|
|
43
46
|
info.setProperty(runtime, "encodeTransport", jsi::String::createFromAscii(runtime,
|
|
44
|
-
hardware ? "
|
|
47
|
+
hardware ? "webgpu-native-window" : "cpu-rgba-upload"));
|
|
45
48
|
info.setProperty(runtime, "exportFallback", jsi::String::createFromAscii(runtime, "cpu-rgba-upload"));
|
|
46
49
|
info.setProperty(runtime, "gpuDirectExport", hardware);
|
|
50
|
+
info.setProperty(runtime, "zeroCopyEncode", hardware);
|
|
47
51
|
info.setProperty(runtime, "decodeCpuCopiesBeforeSkia", hardware ? 0 : 1);
|
|
48
52
|
info.setProperty(runtime, "cpuReadbackDecode", !hardware);
|
|
49
53
|
info.setProperty(runtime, "ownedFrameTransport", jsi::String::createFromAscii(runtime,
|
|
@@ -51,7 +55,6 @@ void install(jsi::Runtime& jsiRuntime) {
|
|
|
51
55
|
info.setProperty(runtime, "skiaImportTransport", jsi::String::createFromAscii(runtime,
|
|
52
56
|
hardware ? "native-buffer-webgpu-blit-snapshot" : "cpu-rgba-scoped-copy-webgpu-upload-snapshot"));
|
|
53
57
|
info.setProperty(runtime, "zeroCopyDecode", false);
|
|
54
|
-
info.setProperty(runtime, "zeroCopyEncode", false);
|
|
55
58
|
return info;
|
|
56
59
|
}));
|
|
57
60
|
auto createVideoPlayer = jsi::Function::createFromHostFunction(
|
|
@@ -126,8 +126,8 @@ public class GLFrameExtractor implements SurfaceTexture.OnFrameAvailableListener
|
|
|
126
126
|
/**
|
|
127
127
|
* The Surface the codec renders into. With {@code nativeImages}, on API 29+
|
|
128
128
|
* with hardware buffers, it is an ImageReader's: frames then keep the codec's
|
|
129
|
-
* YUV buffer, cropped
|
|
130
|
-
*
|
|
129
|
+
* YUV buffer, cropped, turned by {@code rotation} and scaled to the frame's
|
|
130
|
+
* size when WebGPU converts it.
|
|
131
131
|
*/
|
|
132
132
|
@android.annotation.TargetApi(Build.VERSION_CODES.Q)
|
|
133
133
|
public Surface surfaceFor(int videoWidth, int videoHeight, int rotation, boolean nativeImages) {
|
|
@@ -163,13 +163,7 @@ public class GLFrameExtractor implements SurfaceTexture.OnFrameAvailableListener
|
|
|
163
163
|
try {
|
|
164
164
|
if (Build.VERSION.SDK_INT >= 33) awaitProducer(image);
|
|
165
165
|
Rect crop = image.getCropRect();
|
|
166
|
-
|
|
167
|
-
int pictureWidth = swapped ? crop.height() : crop.width();
|
|
168
|
-
int pictureHeight = swapped ? crop.width() : crop.height();
|
|
169
|
-
if (pictureWidth != width || pictureHeight != height) {
|
|
170
|
-
throw new IllegalStateException("Decoded image " + pictureWidth + "x" + pictureHeight
|
|
171
|
-
+ " does not match the requested " + width + "x" + height);
|
|
172
|
-
}
|
|
166
|
+
if (crop.isEmpty()) throw new IllegalStateException("Decoded image has an empty crop");
|
|
173
167
|
HardwareBuffer buffer = image.getHardwareBuffer();
|
|
174
168
|
if (buffer == null) throw new IllegalStateException("Decoded image has no hardware buffer");
|
|
175
169
|
long lease;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package com.azzapp.rnskv;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.os.Build;
|
|
5
|
+
import android.os.SystemClock;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
import android.view.Display;
|
|
8
|
+
import android.view.View;
|
|
9
|
+
import android.view.ViewGroup;
|
|
10
|
+
|
|
11
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
12
|
+
|
|
13
|
+
import java.util.HashSet;
|
|
14
|
+
import java.util.Set;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Asks the display for its highest refresh rate while a preview plays, through
|
|
18
|
+
* the React Native Skia canvases of the activity (Android 15+). Main thread.
|
|
19
|
+
*
|
|
20
|
+
* Android picks the refresh rate from what the app presents. A preview that
|
|
21
|
+
* presents at 30 fps for a while (30 fps frames drawn as they come) brings a
|
|
22
|
+
* 60 Hz display down to 30 Hz, and it stays there: the next clip, at 60 or
|
|
23
|
+
* 24 fps, can only present at 30 fps and never shows the display it needs
|
|
24
|
+
* more. On a Pixel 8a, one 60 fps frame in two was never shown, and 24 fps
|
|
25
|
+
* came out irregular on a fifth of its frames. A vote tells the display.
|
|
26
|
+
*
|
|
27
|
+
* Only views that draw cast their vote: one on the window's root view is never
|
|
28
|
+
* counted. The player does not know which canvas shows it, so all of the Skia
|
|
29
|
+
* TextureView canvases vote; those that do not draw cast nothing. SurfaceView
|
|
30
|
+
* canvases vote for themselves, and the vote asks what theirs asks: the highest
|
|
31
|
+
* rate a seamless switch reaches, 120 Hz where the user allows it, so that
|
|
32
|
+
* what drawFrame animates over the video is not held to 60 Hz. Not the clips'
|
|
33
|
+
* rate: a 24 fps vote brought the display to 30 Hz, where 24 fps judders.
|
|
34
|
+
*/
|
|
35
|
+
final class PreviewFrameRateVote {
|
|
36
|
+
private static final String TAG = "PreviewFrameRateVote";
|
|
37
|
+
|
|
38
|
+
private static final String SKIA_TEXTURE_VIEW = "com.reactnative.skia.SkiaTextureView";
|
|
39
|
+
|
|
40
|
+
// Canvases mounted while playing get the vote at the next refresh.
|
|
41
|
+
private static final long REFRESH_MILLIS = 1000;
|
|
42
|
+
|
|
43
|
+
private static final Set<PreviewFrameRateVote> voting = new HashSet<>();
|
|
44
|
+
|
|
45
|
+
private static long lastAppliedMillis = 0;
|
|
46
|
+
|
|
47
|
+
/** Whether the player is playing, at each vsync it collects frames on. */
|
|
48
|
+
void update(boolean playing) {
|
|
49
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (playing ? voting.add(this) : voting.remove(this)) {
|
|
53
|
+
apply();
|
|
54
|
+
} else if (playing && SystemClock.uptimeMillis() - lastAppliedMillis >= REFRESH_MILLIS) {
|
|
55
|
+
apply();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The player is released. */
|
|
60
|
+
void withdraw() {
|
|
61
|
+
update(false);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private static void apply() {
|
|
65
|
+
lastAppliedMillis = SystemClock.uptimeMillis();
|
|
66
|
+
ReactApplicationContext context = ReactNativeSkiaVideoModule.currentReactApplicationContext();
|
|
67
|
+
Activity activity = context != null ? context.getCurrentActivity() : null;
|
|
68
|
+
if (activity == null || activity.getWindow() == null) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
float rate = voting.isEmpty() ? View.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT
|
|
72
|
+
: maxRefreshRate(activity.getWindow().getDecorView().getDisplay());
|
|
73
|
+
try {
|
|
74
|
+
vote(activity.getWindow().getDecorView(), rate);
|
|
75
|
+
} catch (RuntimeException e) {
|
|
76
|
+
Log.w(TAG, "Could not vote the refresh rate, leaving it to the display", e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The highest rate a seamless switch from the current mode reaches.
|
|
81
|
+
private static float maxRefreshRate(Display display) {
|
|
82
|
+
if (display == null) {
|
|
83
|
+
return View.REQUESTED_FRAME_RATE_CATEGORY_HIGH;
|
|
84
|
+
}
|
|
85
|
+
Display.Mode mode = display.getMode();
|
|
86
|
+
float rate = mode.getRefreshRate();
|
|
87
|
+
for (float alternative : mode.getAlternativeRefreshRates()) {
|
|
88
|
+
rate = Math.max(rate, alternative);
|
|
89
|
+
}
|
|
90
|
+
return rate;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private static void vote(View view, float rate) {
|
|
94
|
+
if (SKIA_TEXTURE_VIEW.equals(view.getClass().getName())) {
|
|
95
|
+
view.setRequestedFrameRate(rate);
|
|
96
|
+
}
|
|
97
|
+
if (view instanceof ViewGroup) {
|
|
98
|
+
ViewGroup group = (ViewGroup) view;
|
|
99
|
+
for (int i = 0; i < group.getChildCount(); i++) {
|
|
100
|
+
vote(group.getChildAt(i), rate);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -14,6 +14,10 @@ public class VideoComposition {
|
|
|
14
14
|
private boolean lazyDecoders = false;
|
|
15
15
|
private boolean hardwareBufferEnabled = false;
|
|
16
16
|
public boolean isHardwareBufferEnabled() { return hardwareBufferEnabled; }
|
|
17
|
+
// framePixelFormat: 'rgba': every frame through the GL path, none through
|
|
18
|
+
// ImageReader's YUV buffers. Set from the C++ side.
|
|
19
|
+
private boolean rgbaFrames = false;
|
|
20
|
+
public boolean isRgbaFrames() { return rgbaFrames; }
|
|
17
21
|
|
|
18
22
|
public VideoComposition(
|
|
19
23
|
double duration,
|
|
@@ -676,7 +676,7 @@ public class VideoCompositionDecoder {
|
|
|
676
676
|
try {
|
|
677
677
|
decoder.prepare();
|
|
678
678
|
decoder.setSurface(slot.extractor.surfaceFor(decoder.getVideoWidth(),
|
|
679
|
-
decoder.getVideoHeight(), decoder.getRotation(), wantsNativeImages(
|
|
679
|
+
decoder.getVideoHeight(), decoder.getRotation(), wantsNativeImages(decoder)));
|
|
680
680
|
} catch (IOException | RuntimeException e) {
|
|
681
681
|
decoder.release();
|
|
682
682
|
throw e;
|
|
@@ -685,15 +685,14 @@ public class VideoCompositionDecoder {
|
|
|
685
685
|
}
|
|
686
686
|
|
|
687
687
|
/**
|
|
688
|
-
* Hardware frames
|
|
689
|
-
*
|
|
690
|
-
*
|
|
688
|
+
* Hardware frames from an 8-bit SDR track skip the GL render pass: the
|
|
689
|
+
* codec writes into ImageReader buffers, which WebGPU converts at the
|
|
690
|
+
* frame's size, a capped or explicit one included. `framePixelFormat:
|
|
691
|
+
* 'rgba'` keeps the GL path.
|
|
691
692
|
*/
|
|
692
|
-
private boolean wantsNativeImages(
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
decoder.getVideoWidth(), decoder.getVideoHeight(), 0);
|
|
696
|
-
return size[0] == decoder.getVideoWidth() && size[1] == decoder.getVideoHeight();
|
|
693
|
+
private boolean wantsNativeImages(VideoCompositionItemDecoder decoder) {
|
|
694
|
+
return !composition.isRgbaFrames() && composition.isHardwareBufferEnabled()
|
|
695
|
+
&& decoder.isEightBitSdr();
|
|
697
696
|
}
|
|
698
697
|
|
|
699
698
|
private void reportOpenError(Exception e) {
|
|
@@ -58,6 +58,8 @@ public class VideoCompositionFramesExtractor {
|
|
|
58
58
|
private volatile boolean isPlaying = false;
|
|
59
59
|
|
|
60
60
|
private volatile long startTime = 0;
|
|
61
|
+
// The position playback last started or was sought to while playing.
|
|
62
|
+
private volatile long playStartPosition = 0;
|
|
61
63
|
private volatile long pausePosition = 0;
|
|
62
64
|
private boolean isEOS = false;
|
|
63
65
|
private boolean completeDispatched = false;
|
|
@@ -72,6 +74,7 @@ public class VideoCompositionFramesExtractor {
|
|
|
72
74
|
public void doFrame(long frameTimeNanos) {
|
|
73
75
|
if (releasing) {
|
|
74
76
|
unwatchDisplay();
|
|
77
|
+
frameRateVote.withdraw();
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
80
|
recordedVsyncNanos = frameTimeNanos;
|
|
@@ -80,8 +83,12 @@ public class VideoCompositionFramesExtractor {
|
|
|
80
83
|
};
|
|
81
84
|
// The display the preview is on; null until the first collection, or when unknown.
|
|
82
85
|
private DisplayVsyncPeriod vsyncPeriod;
|
|
86
|
+
// The display's highest refresh rate while playing.
|
|
87
|
+
private final PreviewFrameRateVote frameRateVote = new PreviewFrameRateVote();
|
|
83
88
|
// When that thread last released frames; the playback loop stands back meanwhile.
|
|
84
89
|
private volatile long lastVsyncReleaseNanos = 0;
|
|
90
|
+
// The vsync the frames collected last are shown at; 0 until the first.
|
|
91
|
+
private volatile long collectedVsyncNanos = 0;
|
|
85
92
|
// The release itself runs on the playback thread: the decoder's locks are held
|
|
86
93
|
// there for whole codec flushes on a seek, and the UI thread must never
|
|
87
94
|
// wait on them (measured: 65 ms frames when it did).
|
|
@@ -187,11 +194,13 @@ public class VideoCompositionFramesExtractor {
|
|
|
187
194
|
watchDisplay();
|
|
188
195
|
return;
|
|
189
196
|
}
|
|
197
|
+
frameRateVote.update(isPlaying);
|
|
190
198
|
long period = vsyncPeriod != null ? vsyncPeriod.getPeriodNanos() : 0;
|
|
191
199
|
if (period == 0 || recordedVsyncNanos == 0) return;
|
|
192
200
|
// This frame's vsync, whether the recorder has run in this frame yet or not.
|
|
193
201
|
long since = Math.max(0, System.nanoTime() - recordedVsyncNanos);
|
|
194
202
|
long vsyncNanos = recordedVsyncNanos + (since / period) * period;
|
|
203
|
+
collectedVsyncNanos = vsyncNanos;
|
|
195
204
|
if (vsyncNanos == lastVsyncNanos) return;
|
|
196
205
|
lastVsyncNanos = vsyncNanos;
|
|
197
206
|
long vsyncPeriodNanos = period;
|
|
@@ -243,6 +252,27 @@ public class VideoCompositionFramesExtractor {
|
|
|
243
252
|
return isPlaying ? microTime() - startTime : pausePosition;
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
/**
|
|
256
|
+
* The position the frames collected in this vsync were released for: the
|
|
257
|
+
* clock at the vsync rather than at the call, a few milliseconds later.
|
|
258
|
+
* drawFrame gets it as its time, so that the time it draws at and the frames
|
|
259
|
+
* it draws agree: read at the call, the time ran up to a frame ahead of the
|
|
260
|
+
* frames, at 60 fps on most vsyncs. Without vsync release (no display, or
|
|
261
|
+
* a stalled collector), the clock at the call.
|
|
262
|
+
*
|
|
263
|
+
* @return the position in microseconds
|
|
264
|
+
*/
|
|
265
|
+
public long getPresentedPosition() {
|
|
266
|
+
long vsyncNanos = collectedVsyncNanos;
|
|
267
|
+
if (!isPlaying || vsyncNanos == 0
|
|
268
|
+
|| System.nanoTime() - lastVsyncReleaseNanos > VSYNC_RELEASE_TIMEOUT_NANOS) {
|
|
269
|
+
return getCurrentPosition();
|
|
270
|
+
}
|
|
271
|
+
// Never before the play or seek that started this run: the vsync can
|
|
272
|
+
// precede it by up to a period.
|
|
273
|
+
return Math.max(vsyncNanos / 1000 - startTime, playStartPosition);
|
|
274
|
+
}
|
|
275
|
+
|
|
246
276
|
/**
|
|
247
277
|
* @return the version of the frames returned by {@link #decodeCompositionFrames},
|
|
248
278
|
* see {@link VideoCompositionDecoder#getFramesVersion()}
|
|
@@ -314,6 +344,7 @@ public class VideoCompositionFramesExtractor {
|
|
|
314
344
|
pausePosition = 0;
|
|
315
345
|
seekInternal(0);
|
|
316
346
|
}
|
|
347
|
+
playStartPosition = pausePosition;
|
|
317
348
|
startTime = microTime() - pausePosition;
|
|
318
349
|
isPlaying = true;
|
|
319
350
|
pausePosition = 0;
|
|
@@ -381,6 +412,7 @@ public class VideoCompositionFramesExtractor {
|
|
|
381
412
|
audioPlayer.seekTo(position);
|
|
382
413
|
}
|
|
383
414
|
if (isPlaying) {
|
|
415
|
+
playStartPosition = position;
|
|
384
416
|
startTime = microTime() - position;
|
|
385
417
|
} else {
|
|
386
418
|
pausePosition = position;
|