@pictor-sdk/video-android 0.1.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 +21 -0
- package/README.md +40 -0
- package/android/CMakeLists.txt +30 -0
- package/android/build.gradle +119 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/pictor_overlaystage.cpp +372 -0
- package/android/src/main/cpp/pictor_publish.cpp +87 -0
- package/android/src/main/cpp/pictor_videosurface.cpp +735 -0
- package/android/src/main/java/dev/pictor/video/AacPriming.kt +261 -0
- package/android/src/main/java/dev/pictor/video/AudioMixer.kt +1112 -0
- package/android/src/main/java/dev/pictor/video/AudioProbe.kt +162 -0
- package/android/src/main/java/dev/pictor/video/CadenceShaderProgram.kt +337 -0
- package/android/src/main/java/dev/pictor/video/CodecLedger.kt +126 -0
- package/android/src/main/java/dev/pictor/video/CompositionSpike.kt +978 -0
- package/android/src/main/java/dev/pictor/video/Diagnostics.kt +54 -0
- package/android/src/main/java/dev/pictor/video/EncoderGate.kt +476 -0
- package/android/src/main/java/dev/pictor/video/ExportGate.kt +1974 -0
- package/android/src/main/java/dev/pictor/video/ExportHeartbeat.kt +72 -0
- package/android/src/main/java/dev/pictor/video/ExportProgress.kt +103 -0
- package/android/src/main/java/dev/pictor/video/FrameClock.kt +591 -0
- package/android/src/main/java/dev/pictor/video/FramePending.kt +181 -0
- package/android/src/main/java/dev/pictor/video/GateAudio.kt +456 -0
- package/android/src/main/java/dev/pictor/video/GateDestination.kt +445 -0
- package/android/src/main/java/dev/pictor/video/GateFrames.kt +662 -0
- package/android/src/main/java/dev/pictor/video/GateMux.kt +185 -0
- package/android/src/main/java/dev/pictor/video/ManagedSources.kt +333 -0
- package/android/src/main/java/dev/pictor/video/Media3Player.kt +934 -0
- package/android/src/main/java/dev/pictor/video/MediaStoreSave.kt +419 -0
- package/android/src/main/java/dev/pictor/video/OverlayResources.kt +169 -0
- package/android/src/main/java/dev/pictor/video/PictorVideoModule.kt +1612 -0
- package/android/src/main/java/dev/pictor/video/PictorVideoPackage.kt +26 -0
- package/android/src/main/java/dev/pictor/video/PlaybackAnchor.kt +161 -0
- package/android/src/main/java/dev/pictor/video/PreviewAudioPlayer.kt +291 -0
- package/android/src/main/java/dev/pictor/video/PreviewConfig.kt +96 -0
- package/android/src/main/java/dev/pictor/video/PreviewMix.kt +396 -0
- package/android/src/main/java/dev/pictor/video/PreviewSync.kt +374 -0
- package/android/src/main/java/dev/pictor/video/PreviewTrace.kt +478 -0
- package/android/src/main/java/dev/pictor/video/PrimingTrim.kt +337 -0
- package/android/src/main/java/dev/pictor/video/RepeatingEncoderFactory.kt +97 -0
- package/android/src/main/java/dev/pictor/video/SceneProgram.kt +169 -0
- package/android/src/main/java/dev/pictor/video/SceneProgramNative.kt +299 -0
- package/android/src/main/java/dev/pictor/video/SkiaPassThroughEffect.kt +695 -0
- package/android/src/main/java/dev/pictor/video/SurfaceAttachment.kt +110 -0
- package/android/src/main/java/dev/pictor/video/ThumbnailDecoder.kt +171 -0
- package/android/src/main/java/dev/pictor/video/VideoSurface.kt +394 -0
- package/app.plugin.js +43 -0
- package/dist/index.d.mts +3622 -0
- package/dist/index.d.ts +3622 -0
- package/dist/index.js +3737 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3625 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
- package/plugin/gradleProperties.d.ts +19 -0
- package/plugin/gradleProperties.js +64 -0
- package/react-native.config.js +13 -0
- package/src/NativePictorVideo.ts +574 -0
- package/src/audioProbe.ts +72 -0
- package/src/compositionSpike.ts +305 -0
- package/src/convergence.ts +112 -0
- package/src/encoderGate.ts +66 -0
- package/src/exportDestination.ts +149 -0
- package/src/exportJob.ts +232 -0
- package/src/exportProgress.ts +391 -0
- package/src/exportProgressPump.ts +190 -0
- package/src/exporter.ts +655 -0
- package/src/index.ts +276 -0
- package/src/managedSources.ts +252 -0
- package/src/media3.ts +412 -0
- package/src/mediaStore.ts +177 -0
- package/src/memoryGuard.ts +118 -0
- package/src/mixSources.ts +82 -0
- package/src/overlayStaging.ts +216 -0
- package/src/playbackLifecycle.ts +137 -0
- package/src/player.ts +39 -0
- package/src/playlistSession.ts +125 -0
- package/src/previewAudioDiagnostics.ts +67 -0
- package/src/previewAudioSession.ts +168 -0
- package/src/previewCadence.ts +950 -0
- package/src/previewMix.ts +212 -0
- package/src/previewTrace.ts +348 -0
- package/src/spikeExcursions.ts +405 -0
- package/src/submissionScoring.ts +680 -0
- package/src/surfaceOwner.ts +264 -0
- package/src/surfaceRecovery.ts +121 -0
- package/src/syncTrace.ts +189 -0
- package/src/thumbnailProvider.ts +163 -0
- package/src/tracePhases.ts +217 -0
- package/src/useCompositionPlayback.ts +327 -0
- package/src/useMedia3Playback.ts +492 -0
- package/src/usePreviewAudio.ts +305 -0
- package/src/useVideoSurface.ts +282 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The persistent presentation surface: one external texture, one SurfaceTexture.
|
|
3
|
+
*
|
|
4
|
+
* Everything that touches GL runs on the UI runtime, because that is where
|
|
5
|
+
* Skia's context is current — established by the earlier context probe, which
|
|
6
|
+
* found the same EGL context handle before and after a Skia GPU call on that
|
|
7
|
+
* thread. A `SurfaceTexture` binds to the calling thread's current context, so
|
|
8
|
+
* it is built from here rather than from Kotlin on some other thread.
|
|
9
|
+
*
|
|
10
|
+
* The host object holds a global ref to the Java objects and nothing else. Its
|
|
11
|
+
* destructor is trivial: teardown is an explicit call, in order, on this
|
|
12
|
+
* thread. A finalizer running on a GC thread would issue `glDeleteTextures`
|
|
13
|
+
* against whatever context happened to be current there, which is how this
|
|
14
|
+
* project has crashed before.
|
|
15
|
+
*/
|
|
16
|
+
#include <EGL/egl.h>
|
|
17
|
+
#include <GLES2/gl2.h>
|
|
18
|
+
#include <GLES2/gl2ext.h>
|
|
19
|
+
#include <atomic>
|
|
20
|
+
#include <jni.h>
|
|
21
|
+
#include <mutex>
|
|
22
|
+
#include <jsi/jsi.h>
|
|
23
|
+
|
|
24
|
+
#include <ctime>
|
|
25
|
+
#include <memory>
|
|
26
|
+
|
|
27
|
+
namespace jsi = facebook::jsi;
|
|
28
|
+
|
|
29
|
+
namespace pictor {
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How many host objects have been constructed, ever.
|
|
34
|
+
*
|
|
35
|
+
* A façade is cheap and cloning one is harmless, but *constructing* one is a new
|
|
36
|
+
* backing state unless the state lives elsewhere. This counter is what
|
|
37
|
+
* distinguishes the two, and it is reported by the probe rather than inferred.
|
|
38
|
+
*/
|
|
39
|
+
std::atomic<int> g_factoryCount{0};
|
|
40
|
+
|
|
41
|
+
/** Monotonic stamp for the last mutation, so two readers can be ordered. */
|
|
42
|
+
std::atomic<long> g_mutationSeq{0};
|
|
43
|
+
/** Thread id of that mutation, as a small stable number. */
|
|
44
|
+
std::atomic<long> g_mutationThread{0};
|
|
45
|
+
|
|
46
|
+
long threadTag() {
|
|
47
|
+
static std::atomic<long> next{1};
|
|
48
|
+
thread_local long tag = next.fetch_add(1);
|
|
49
|
+
return tag;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
JavaVM *g_vm = nullptr;
|
|
53
|
+
|
|
54
|
+
/** The UI thread is a Java thread already; this only fetches its env. */
|
|
55
|
+
JNIEnv *env() {
|
|
56
|
+
if (g_vm == nullptr) return nullptr;
|
|
57
|
+
JNIEnv *e = nullptr;
|
|
58
|
+
if (g_vm->GetEnv(reinterpret_cast<void **>(&e), JNI_VERSION_1_6) != JNI_OK) {
|
|
59
|
+
return nullptr;
|
|
60
|
+
}
|
|
61
|
+
return e;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class SurfaceHostObject : public jsi::HostObject {
|
|
65
|
+
public:
|
|
66
|
+
~SurfaceHostObject() override = default; // trivial, deliberately
|
|
67
|
+
|
|
68
|
+
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override {
|
|
69
|
+
const auto prop = name.utf8(rt);
|
|
70
|
+
auto self = this;
|
|
71
|
+
|
|
72
|
+
if (prop == "create") {
|
|
73
|
+
return jsi::Function::createFromHostFunction(
|
|
74
|
+
rt, name, 2,
|
|
75
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args,
|
|
76
|
+
size_t count) -> jsi::Value {
|
|
77
|
+
const int width = count > 0 ? (int)args[0].asNumber() : 256;
|
|
78
|
+
const int height = count > 1 ? (int)args[1].asNumber() : 256;
|
|
79
|
+
jsi::Object out(rt);
|
|
80
|
+
out.setProperty(rt, "textureId", 0.0);
|
|
81
|
+
out.setProperty(rt, "ok", false);
|
|
82
|
+
|
|
83
|
+
if (eglGetCurrentContext() == EGL_NO_CONTEXT) {
|
|
84
|
+
out.setProperty(rt, "error",
|
|
85
|
+
jsi::String::createFromUtf8(
|
|
86
|
+
rt, "no EGL context current on this thread"));
|
|
87
|
+
return jsi::Value(std::move(out));
|
|
88
|
+
}
|
|
89
|
+
JNIEnv *e = env();
|
|
90
|
+
if (e == nullptr) {
|
|
91
|
+
out.setProperty(
|
|
92
|
+
rt, "error",
|
|
93
|
+
jsi::String::createFromUtf8(rt, "no JNIEnv on this thread"));
|
|
94
|
+
return jsi::Value(std::move(out));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
Retire whatever this slot already holds, before making more.
|
|
99
|
+
|
|
100
|
+
`create` used to overwrite `texture_`, `surfaceTexture_`,
|
|
101
|
+
`surface_`, `flag_` and `cls_` in place. Every field it
|
|
102
|
+
overwrote was the only reference to a live object: a GL external
|
|
103
|
+
texture, a SurfaceTexture and its buffer, a Surface, a listener
|
|
104
|
+
still watching the old texture, and four JNI global refs. None of
|
|
105
|
+
it was reachable afterwards and none of it was freed.
|
|
106
|
+
|
|
107
|
+
Measured on a Pixel 7 across eight mount cycles of the production
|
|
108
|
+
stack — no CompositionPlayer, no ring — graphics memory rose from
|
|
109
|
+
77.3 MB to 141.4 MB in steps of exactly 8,312 KB. A constant
|
|
110
|
+
increment is one object per cycle, not bloat, and 8,312 KB is a
|
|
111
|
+
1080x1920 buffer.
|
|
112
|
+
|
|
113
|
+
Explicit, on this thread, which has both an EGL context and an
|
|
114
|
+
attached JNIEnv. Not a destructor: the JSI object outlives the GL
|
|
115
|
+
thread and a GC-time free would run where neither is available.
|
|
116
|
+
*/
|
|
117
|
+
retire(self, e, true);
|
|
118
|
+
self->creates_++;
|
|
119
|
+
|
|
120
|
+
while (glGetError() != GL_NO_ERROR) {
|
|
121
|
+
}
|
|
122
|
+
glGenTextures(1, &self->texture_);
|
|
123
|
+
glBindTexture(GL_TEXTURE_EXTERNAL_OES, self->texture_);
|
|
124
|
+
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER,
|
|
125
|
+
GL_LINEAR);
|
|
126
|
+
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER,
|
|
127
|
+
GL_LINEAR);
|
|
128
|
+
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S,
|
|
129
|
+
GL_CLAMP_TO_EDGE);
|
|
130
|
+
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T,
|
|
131
|
+
GL_CLAMP_TO_EDGE);
|
|
132
|
+
const GLenum error = glGetError();
|
|
133
|
+
|
|
134
|
+
jclass cls = e->FindClass("dev/pictor/video/VideoSurface");
|
|
135
|
+
jmethodID create = e->GetStaticMethodID(
|
|
136
|
+
cls, "create", "(III)Landroid/graphics/SurfaceTexture;");
|
|
137
|
+
jobject texture = e->CallStaticObjectMethod(
|
|
138
|
+
cls, create, (jint)self->texture_, (jint)width, (jint)height);
|
|
139
|
+
jmethodID surfaceOf = e->GetStaticMethodID(
|
|
140
|
+
cls, "surfaceOf",
|
|
141
|
+
"(Landroid/graphics/SurfaceTexture;)Landroid/view/Surface;");
|
|
142
|
+
jobject surface = e->CallStaticObjectMethod(cls, surfaceOf, texture);
|
|
143
|
+
|
|
144
|
+
self->surfaceTexture_ = e->NewGlobalRef(texture);
|
|
145
|
+
self->surface_ = e->NewGlobalRef(surface);
|
|
146
|
+
self->cls_ = (jclass)e->NewGlobalRef(cls);
|
|
147
|
+
|
|
148
|
+
// Published so a player can render into it, and watched so frames
|
|
149
|
+
// raise a flag instead of doing work on the producer's thread.
|
|
150
|
+
// Returns the generation this surface was registered under. Kept
|
|
151
|
+
// so teardown can prove it is retiring its own surface: a screen's
|
|
152
|
+
// cleanup routinely runs after the next screen's setup.
|
|
153
|
+
jmethodID reg = e->GetStaticMethodID(cls, "register",
|
|
154
|
+
"(Landroid/view/Surface;)I");
|
|
155
|
+
self->generation_.store(e->CallStaticIntMethod(cls, reg, surface));
|
|
156
|
+
g_mutationSeq.fetch_add(1);
|
|
157
|
+
g_mutationThread.store(threadTag());
|
|
158
|
+
jclass boolCls = e->FindClass("java/util/concurrent/atomic/AtomicBoolean");
|
|
159
|
+
jobject flag = e->NewObject(
|
|
160
|
+
boolCls, e->GetMethodID(boolCls, "<init>", "(Z)V"), JNI_FALSE);
|
|
161
|
+
self->flag_ = e->NewGlobalRef(flag);
|
|
162
|
+
jmethodID watch = e->GetStaticMethodID(
|
|
163
|
+
cls, "watch",
|
|
164
|
+
"(Landroid/graphics/SurfaceTexture;Ljava/util/concurrent/atomic/AtomicBoolean;)V");
|
|
165
|
+
e->CallStaticVoidMethod(cls, watch, texture, flag);
|
|
166
|
+
|
|
167
|
+
out.setProperty(rt, "textureId", (double)self->texture_);
|
|
168
|
+
out.setProperty(rt, "glError", (double)error);
|
|
169
|
+
out.setProperty(rt, "ok", self->surfaceTexture_ != nullptr);
|
|
170
|
+
// Reported so readiness can be generation-aware. A caller that only
|
|
171
|
+
// knows "a surface exists" cannot tell the one it prepared against
|
|
172
|
+
// from the one that replaced it while the picker was open.
|
|
173
|
+
out.setProperty(rt, "generation", (double)self->generation_.load());
|
|
174
|
+
out.setProperty(rt, "error", jsi::String::createFromUtf8(rt, ""));
|
|
175
|
+
return jsi::Value(std::move(out));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (prop == "surfaceCounters") {
|
|
180
|
+
/*
|
|
181
|
+
Creates, retires, and the difference.
|
|
182
|
+
|
|
183
|
+
The regression this exists for: `create` used to overwrite a live
|
|
184
|
+
surface without retiring it, so `live` climbed by one per call and each
|
|
185
|
+
step was a 1080x1920 buffer nothing could reach. It is exposed rather
|
|
186
|
+
than inferred from graphics memory, which settles on its own schedule
|
|
187
|
+
and cannot say whose bytes it is holding.
|
|
188
|
+
|
|
189
|
+
`live` must never exceed one, and must return to zero after a release.
|
|
190
|
+
*/
|
|
191
|
+
return jsi::Function::createFromHostFunction(
|
|
192
|
+
rt, name, 0,
|
|
193
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
194
|
+
size_t) -> jsi::Value {
|
|
195
|
+
jsi::Object out(rt);
|
|
196
|
+
out.setProperty(rt, "creates", (double)self->creates_);
|
|
197
|
+
out.setProperty(rt, "retires", (double)self->retires_);
|
|
198
|
+
out.setProperty(rt, "live", (double)(self->creates_ - self->retires_));
|
|
199
|
+
out.setProperty(rt, "generation", (double)self->generation_.load());
|
|
200
|
+
return jsi::Value(std::move(out));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (prop == "pending") {
|
|
205
|
+
// Whether a frame is waiting, asked of the *same* `FramePending` instance
|
|
206
|
+
// the listener updates. Deliberately a call across JNI on every tick
|
|
207
|
+
// rather than a value mirrored into JS: a mirror is a copy, a copy is
|
|
208
|
+
// stale between updates, and the whole failure being fixed here was a
|
|
209
|
+
// consumer acting on its own idea of availability rather than the
|
|
210
|
+
// producer's.
|
|
211
|
+
return jsi::Function::createFromHostFunction(
|
|
212
|
+
rt, name, 0,
|
|
213
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
214
|
+
size_t) -> jsi::Value {
|
|
215
|
+
JNIEnv *e = env();
|
|
216
|
+
if (e == nullptr || self->cls_ == nullptr) return jsi::Value(false);
|
|
217
|
+
jmethodID has = e->GetStaticMethodID(self->cls_, "hasPendingFrame",
|
|
218
|
+
"(I)Z");
|
|
219
|
+
return jsi::Value(e->CallStaticBooleanMethod(
|
|
220
|
+
self->cls_, has, self->generation_.load()) == JNI_TRUE);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (prop == "identity") {
|
|
225
|
+
// Who this object is, and who it is talking to.
|
|
226
|
+
//
|
|
227
|
+
// `self` is the host object the consumer captured; `controllerId` and
|
|
228
|
+
// `pendingId` are the producer's. A consumer holding a stale object is
|
|
229
|
+
// invisible in the values — a captured `undefined` surface and a live one
|
|
230
|
+
// both report "nothing pending" — so identity is the only thing that can
|
|
231
|
+
// tell them apart, and it is recorded from inside the worklet rather than
|
|
232
|
+
// sampled from JS, which would only prove what JS can see.
|
|
233
|
+
return jsi::Function::createFromHostFunction(
|
|
234
|
+
rt, name, 0,
|
|
235
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
236
|
+
size_t) -> jsi::Value {
|
|
237
|
+
jsi::Object out(rt);
|
|
238
|
+
// The façade: this host object. May legitimately differ between
|
|
239
|
+
// runtimes — a clone is harmless.
|
|
240
|
+
out.setProperty(
|
|
241
|
+
rt, "facadeId",
|
|
242
|
+
(double)(reinterpret_cast<uintptr_t>(self) & 0x7fffffff));
|
|
243
|
+
// The backing state the façade resolves through. **Must** match
|
|
244
|
+
// across runtimes: if it does not, two independent states exist and
|
|
245
|
+
// every value read from them is separately plausible and jointly
|
|
246
|
+
// wrong.
|
|
247
|
+
out.setProperty(
|
|
248
|
+
rt, "backingId",
|
|
249
|
+
(double)(reinterpret_cast<uintptr_t>(&self->generation_) & 0x7fffffff));
|
|
250
|
+
out.setProperty(rt, "factoryCount",
|
|
251
|
+
(double)g_factoryCount.load());
|
|
252
|
+
out.setProperty(rt, "mutationSeq", (double)g_mutationSeq.load());
|
|
253
|
+
out.setProperty(rt, "mutationThread",
|
|
254
|
+
(double)g_mutationThread.load());
|
|
255
|
+
out.setProperty(rt, "readThread", (double)threadTag());
|
|
256
|
+
out.setProperty(
|
|
257
|
+
rt, "surfaceId",
|
|
258
|
+
(double)(reinterpret_cast<uintptr_t>(self) & 0x7fffffff));
|
|
259
|
+
out.setProperty(rt, "controllerId", 0.0);
|
|
260
|
+
out.setProperty(rt, "pendingId", 0.0);
|
|
261
|
+
out.setProperty(rt, "generation", (double)self->generation_.load());
|
|
262
|
+
JNIEnv *e = env();
|
|
263
|
+
if (e == nullptr || self->cls_ == nullptr) {
|
|
264
|
+
return jsi::Value(std::move(out));
|
|
265
|
+
}
|
|
266
|
+
jmethodID controller =
|
|
267
|
+
e->GetStaticMethodID(self->cls_, "getControllerId", "()I");
|
|
268
|
+
jmethodID pending =
|
|
269
|
+
e->GetStaticMethodID(self->cls_, "getPendingId", "()I");
|
|
270
|
+
if (controller != nullptr) {
|
|
271
|
+
out.setProperty(rt, "controllerId",
|
|
272
|
+
(double)e->CallStaticIntMethod(self->cls_, controller));
|
|
273
|
+
}
|
|
274
|
+
if (pending != nullptr) {
|
|
275
|
+
out.setProperty(rt, "pendingId",
|
|
276
|
+
(double)e->CallStaticIntMethod(self->cls_, pending));
|
|
277
|
+
}
|
|
278
|
+
e->ExceptionClear();
|
|
279
|
+
return jsi::Value(std::move(out));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (prop == "pendingSnapshot") {
|
|
284
|
+
// One reading of three facts. Asking separately means asking at three
|
|
285
|
+
// different instants, and a frame arriving between two of them makes the
|
|
286
|
+
// consumer look like it disagrees with the producer when both answers were
|
|
287
|
+
// true when given.
|
|
288
|
+
return jsi::Function::createFromHostFunction(
|
|
289
|
+
rt, name, 0,
|
|
290
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
291
|
+
size_t) -> jsi::Value {
|
|
292
|
+
JNIEnv *e = env();
|
|
293
|
+
if (e == nullptr || self->cls_ == nullptr) return jsi::Value(0.0);
|
|
294
|
+
jmethodID snap =
|
|
295
|
+
e->GetStaticMethodID(self->cls_, "pendingSnapshot", "(I)J");
|
|
296
|
+
if (snap == nullptr) {
|
|
297
|
+
e->ExceptionClear();
|
|
298
|
+
return jsi::Value(0.0);
|
|
299
|
+
}
|
|
300
|
+
return jsi::Value(static_cast<double>(e->CallStaticLongMethod(
|
|
301
|
+
self->cls_, snap, self->generation_.load())));
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (prop == "pendingDueNs") {
|
|
306
|
+
// The time the pending frame was asked to be presented at, so the
|
|
307
|
+
// consumer can honour it rather than latching the instant a buffer is
|
|
308
|
+
// queued. Measured, the latch ran about 34 ms ahead of it.
|
|
309
|
+
return jsi::Function::createFromHostFunction(
|
|
310
|
+
rt, name, 0,
|
|
311
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
312
|
+
size_t) -> jsi::Value {
|
|
313
|
+
JNIEnv *e = env();
|
|
314
|
+
if (e == nullptr || self->cls_ == nullptr) return jsi::Value(0.0);
|
|
315
|
+
jmethodID due = e->GetStaticMethodID(self->cls_, "pendingDueNs", "()J");
|
|
316
|
+
if (due == nullptr) {
|
|
317
|
+
e->ExceptionClear();
|
|
318
|
+
return jsi::Value(0.0);
|
|
319
|
+
}
|
|
320
|
+
return jsi::Value(
|
|
321
|
+
static_cast<double>(e->CallStaticLongMethod(self->cls_, due)));
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (prop == "pendingDepth") {
|
|
326
|
+
// How many frames are outstanding, from the producer's own counters.
|
|
327
|
+
// Needed for the strengthened invariant: depth above zero for the
|
|
328
|
+
// consumer's generation must make the consumer's own query return true,
|
|
329
|
+
// and if it does not, the two are holding different objects.
|
|
330
|
+
return jsi::Function::createFromHostFunction(
|
|
331
|
+
rt, name, 0,
|
|
332
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
333
|
+
size_t) -> jsi::Value {
|
|
334
|
+
JNIEnv *e = env();
|
|
335
|
+
if (e == nullptr || self->cls_ == nullptr) return jsi::Value(0.0);
|
|
336
|
+
jmethodID getter =
|
|
337
|
+
e->GetStaticMethodID(self->cls_, "getPendingDepth", "()J");
|
|
338
|
+
if (getter == nullptr) {
|
|
339
|
+
e->ExceptionClear();
|
|
340
|
+
return jsi::Value(0.0);
|
|
341
|
+
}
|
|
342
|
+
return jsi::Value(
|
|
343
|
+
static_cast<double>(e->CallStaticLongMethod(self->cls_, getter)));
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (prop == "pendingGeneration") {
|
|
348
|
+
// Which generation the counters are keeping for. Pending for 2 while the
|
|
349
|
+
// worklet asks about 3 is a different fault from nothing having arrived.
|
|
350
|
+
return jsi::Function::createFromHostFunction(
|
|
351
|
+
rt, name, 0,
|
|
352
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
353
|
+
size_t) -> jsi::Value {
|
|
354
|
+
JNIEnv *e = env();
|
|
355
|
+
if (e == nullptr || self->cls_ == nullptr) return jsi::Value(0.0);
|
|
356
|
+
jmethodID getter = e->GetStaticMethodID(
|
|
357
|
+
self->cls_, "getPendingGeneration", "()I");
|
|
358
|
+
if (getter == nullptr) {
|
|
359
|
+
e->ExceptionClear();
|
|
360
|
+
return jsi::Value(0.0);
|
|
361
|
+
}
|
|
362
|
+
return jsi::Value(static_cast<double>(
|
|
363
|
+
e->CallStaticIntMethod(self->cls_, getter)));
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (prop == "generation") {
|
|
368
|
+
// The generation this instance registered under, or 0 before `create`.
|
|
369
|
+
// Read-only: nothing here decides anything, it only lets a caller say
|
|
370
|
+
// which surface it means.
|
|
371
|
+
return jsi::Function::createFromHostFunction(
|
|
372
|
+
rt, name, 0,
|
|
373
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
374
|
+
size_t) -> jsi::Value {
|
|
375
|
+
return jsi::Value(static_cast<double>(self->generation_.load()));
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (prop == "alive") {
|
|
380
|
+
// Whether this instance still holds a SurfaceTexture. False before
|
|
381
|
+
// `create` and after `release`, which is what lets a host reuse a live
|
|
382
|
+
// surface instead of building a second one on every foreground.
|
|
383
|
+
return jsi::Function::createFromHostFunction(
|
|
384
|
+
rt, name, 0,
|
|
385
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
386
|
+
size_t) -> jsi::Value {
|
|
387
|
+
return jsi::Value(self->surfaceTexture_ != nullptr);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (prop == "textureId") {
|
|
392
|
+
return jsi::Function::createFromHostFunction(
|
|
393
|
+
rt, name, 0,
|
|
394
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
395
|
+
size_t) -> jsi::Value {
|
|
396
|
+
return jsi::Value(static_cast<double>(self->texture_));
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (prop == "update") {
|
|
401
|
+
// The whole per-frame path. Returns the frame's presentation time in
|
|
402
|
+
// microseconds, or -1 when nothing new has arrived — the caller then
|
|
403
|
+
// holds what is already shown rather than rewrapping for no reason.
|
|
404
|
+
return jsi::Function::createFromHostFunction(
|
|
405
|
+
rt, name, 0,
|
|
406
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
407
|
+
size_t) -> jsi::Value {
|
|
408
|
+
JNIEnv *e = env();
|
|
409
|
+
if (e == nullptr || self->surfaceTexture_ == nullptr) {
|
|
410
|
+
return jsi::Value(-1.0);
|
|
411
|
+
}
|
|
412
|
+
// Asked as a level, not read as an edge.
|
|
413
|
+
//
|
|
414
|
+
// The old path did `getAndSet(false)` on a Java AtomicBoolean: it
|
|
415
|
+
// *consumed* the signal to test it, so a tick that then failed to
|
|
416
|
+
// latch had to put it back by hand, and a consumer that was disarmed
|
|
417
|
+
// when the edge arrived never saw it at all. A frame that turned up
|
|
418
|
+
// while the app was in the system picker was therefore announced to
|
|
419
|
+
// nobody, and a paused player produces no second announcement — the
|
|
420
|
+
// preview stayed black with every producer counter healthy.
|
|
421
|
+
//
|
|
422
|
+
// `hasPendingFrame` compares two monotonic counters, so asking does
|
|
423
|
+
// not consume, and the answer is the same however long ago the frame
|
|
424
|
+
// arrived or who was listening. `VideoSurface.update` advances the
|
|
425
|
+
// consumed counter itself, and only on a latch that really happened.
|
|
426
|
+
jmethodID hasPending = e->GetStaticMethodID(self->cls_,
|
|
427
|
+
"hasPendingFrame", "(I)Z");
|
|
428
|
+
if (e->CallStaticBooleanMethod(self->cls_, hasPending,
|
|
429
|
+
self->generation_.load()) == JNI_FALSE) {
|
|
430
|
+
return jsi::Value(-1.0);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
jmethodID update = e->GetStaticMethodID(
|
|
434
|
+
self->cls_, "update", "(Landroid/graphics/SurfaceTexture;I)Z");
|
|
435
|
+
const jboolean latched = e->CallStaticBooleanMethod(
|
|
436
|
+
self->cls_, update, self->surfaceTexture_, self->generation_.load());
|
|
437
|
+
if (e->ExceptionCheck()) {
|
|
438
|
+
e->ExceptionClear();
|
|
439
|
+
return jsi::Value(-1.0);
|
|
440
|
+
}
|
|
441
|
+
if (latched == JNI_FALSE) {
|
|
442
|
+
// Nothing to put back: a failed latch consumed nothing, so the
|
|
443
|
+
// level is still raised and the next tick retries by itself.
|
|
444
|
+
return jsi::Value(-1.0);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
jmethodID transform = e->GetStaticMethodID(
|
|
448
|
+
self->cls_, "transform",
|
|
449
|
+
"(Landroid/graphics/SurfaceTexture;)[F");
|
|
450
|
+
auto array = (jfloatArray)e->CallStaticObjectMethod(
|
|
451
|
+
self->cls_, transform, self->surfaceTexture_);
|
|
452
|
+
jfloat values[16];
|
|
453
|
+
e->GetFloatArrayRegion(array, 0, 16, values);
|
|
454
|
+
self->sx_ = values[0];
|
|
455
|
+
self->sy_ = values[5];
|
|
456
|
+
self->tx_ = values[12];
|
|
457
|
+
self->ty_ = values[13];
|
|
458
|
+
// The whole matrix, not just the axis-aligned terms. A rotated
|
|
459
|
+
// source puts its rotation in [1] and [4] and leaves [0] and [5]
|
|
460
|
+
// at zero, so a transform reading only the diagonal cannot tell a
|
|
461
|
+
// rotation from an identity — and silently renders the wrong thing.
|
|
462
|
+
for (int i = 0; i < 16; i++) self->matrix_[i] = values[i];
|
|
463
|
+
e->DeleteLocalRef(array);
|
|
464
|
+
|
|
465
|
+
// One canonical sample, completed where the frame is consumed.
|
|
466
|
+
// Nothing here recomputes anything: the readout and the
|
|
467
|
+
// percentiles both read these fields, matched by sample id.
|
|
468
|
+
struct timespec now;
|
|
469
|
+
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
470
|
+
const int64_t consumedNs =
|
|
471
|
+
static_cast<int64_t>(now.tv_sec) * 1000000000LL + now.tv_nsec;
|
|
472
|
+
|
|
473
|
+
/*
|
|
474
|
+
What the texture actually holds, read after the acquisition.
|
|
475
|
+
|
|
476
|
+
The metadata deque is coalesced to its newest entry on every
|
|
477
|
+
availability callback, so "the sample we kept" and "the buffer
|
|
478
|
+
`updateTexImage` took" are only assumed to be the same frame.
|
|
479
|
+
`getTimestamp` is the acquired buffer's own presentation time and
|
|
480
|
+
is the one thing that can check it.
|
|
481
|
+
*/
|
|
482
|
+
jmethodID stamp = e->GetStaticMethodID(
|
|
483
|
+
self->cls_, "timestamp", "(Landroid/graphics/SurfaceTexture;)J");
|
|
484
|
+
const jlong texturePtsNs =
|
|
485
|
+
stamp == nullptr
|
|
486
|
+
? 0
|
|
487
|
+
: e->CallStaticLongMethod(self->cls_, stamp, self->surfaceTexture_);
|
|
488
|
+
if (e->ExceptionCheck()) e->ExceptionClear();
|
|
489
|
+
|
|
490
|
+
jclass clock = e->FindClass("dev/pictor/video/FrameClock");
|
|
491
|
+
jmethodID take = e->GetStaticMethodID(clock, "take", "(JJ)[J");
|
|
492
|
+
auto paired = (jlongArray)e->CallStaticObjectMethod(
|
|
493
|
+
clock, take, (jlong)consumedNs, texturePtsNs);
|
|
494
|
+
if (paired == nullptr) {
|
|
495
|
+
self->ptsUs_ = -1;
|
|
496
|
+
return jsi::Value(-1.0);
|
|
497
|
+
}
|
|
498
|
+
e->GetLongArrayRegion(paired, 0, 17, self->sample_);
|
|
499
|
+
e->DeleteLocalRef(paired);
|
|
500
|
+
self->ptsUs_ = self->sample_[2];
|
|
501
|
+
return jsi::Value(static_cast<double>(self->ptsUs_));
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// One accessor over the canonical sample. Field indices mirror
|
|
506
|
+
// FrameClock.Field; nothing is derived here, so a reader cannot disagree
|
|
507
|
+
// with the percentiles about what a frame's timing was.
|
|
508
|
+
if (prop == "field") {
|
|
509
|
+
return jsi::Function::createFromHostFunction(
|
|
510
|
+
rt, name, 1,
|
|
511
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args,
|
|
512
|
+
size_t count) -> jsi::Value {
|
|
513
|
+
if (count == 0) return jsi::Value(0.0);
|
|
514
|
+
const int index = static_cast<int>(args[0].asNumber());
|
|
515
|
+
if (index < 0 || index >= 17) return jsi::Value(0.0);
|
|
516
|
+
return jsi::Value(static_cast<double>(self->sample_[index]));
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (prop == "matrix") {
|
|
521
|
+
// The full 4x4, for diagnosing orientation. Diagnostics only: the render
|
|
522
|
+
// path reads the individual terms and never allocates per frame.
|
|
523
|
+
return jsi::Function::createFromHostFunction(
|
|
524
|
+
rt, name, 0,
|
|
525
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
526
|
+
size_t) -> jsi::Value {
|
|
527
|
+
jsi::Array out(rt, 16);
|
|
528
|
+
for (size_t i = 0; i < 16; i++) {
|
|
529
|
+
out.setValueAtIndex(rt, i, jsi::Value(static_cast<double>(self->matrix_[i])));
|
|
530
|
+
}
|
|
531
|
+
return out;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// `rx`/`ry` are the off-diagonal terms, m[1] and m[4]. A quarter turn puts
|
|
536
|
+
// the whole rotation there and leaves the diagonal at zero, so a renderer
|
|
537
|
+
// that reads only sx/sy/tx/ty cannot see it and silently draws unrotated.
|
|
538
|
+
if (prop == "sx" || prop == "sy" || prop == "tx" || prop == "ty" ||
|
|
539
|
+
prop == "rx" || prop == "ry") {
|
|
540
|
+
const char axis = prop[0];
|
|
541
|
+
const char which = prop[1];
|
|
542
|
+
return jsi::Function::createFromHostFunction(
|
|
543
|
+
rt, name, 0,
|
|
544
|
+
[self, axis, which](jsi::Runtime &rt, const jsi::Value &,
|
|
545
|
+
const jsi::Value *, size_t) -> jsi::Value {
|
|
546
|
+
const float value =
|
|
547
|
+
axis == 'r' ? (which == 'x' ? self->matrix_[1] : self->matrix_[4])
|
|
548
|
+
: axis == 's' ? (which == 'x' ? self->sx_ : self->sy_)
|
|
549
|
+
: (which == 'x' ? self->tx_ : self->ty_);
|
|
550
|
+
return jsi::Value(static_cast<double>(value));
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (prop == "releaseGeneration") {
|
|
555
|
+
// Release, but only if this instance still holds the generation named.
|
|
556
|
+
//
|
|
557
|
+
// A superseded create that succeeded has to free what it made, and by the
|
|
558
|
+
// time it can it may no longer be the current surface — releasing "the
|
|
559
|
+
// current one" would then tear down its replacement. Naming the
|
|
560
|
+
// generation makes that impossible: a late attempt either frees its own
|
|
561
|
+
// surface or does nothing.
|
|
562
|
+
return jsi::Function::createFromHostFunction(
|
|
563
|
+
rt, name, 1,
|
|
564
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args,
|
|
565
|
+
size_t count) -> jsi::Value {
|
|
566
|
+
const int wanted = count > 0 ? (int)args[0].asNumber() : 0;
|
|
567
|
+
if (wanted == 0 || wanted != (int)self->generation_.load()) {
|
|
568
|
+
return jsi::Value(false);
|
|
569
|
+
}
|
|
570
|
+
JNIEnv *e = env();
|
|
571
|
+
if (e == nullptr || self->surfaceTexture_ == nullptr) {
|
|
572
|
+
return jsi::Value(false);
|
|
573
|
+
}
|
|
574
|
+
// The same teardown the other two paths run. Three copies of this
|
|
575
|
+
// is how `create` came to be missing it. The class ref stays: this
|
|
576
|
+
// host object is still in use after a superseded surface retires.
|
|
577
|
+
retire(self, e, false);
|
|
578
|
+
return jsi::Value(true);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (prop == "release") {
|
|
583
|
+
// Order matters and is explicit: Surface, SurfaceTexture, then the GL
|
|
584
|
+
// texture, on the thread whose context owns it. The same teardown
|
|
585
|
+
// `create` runs before it allocates, so a replaced surface and a released
|
|
586
|
+
// one free exactly the same things.
|
|
587
|
+
return jsi::Function::createFromHostFunction(
|
|
588
|
+
rt, name, 0,
|
|
589
|
+
[self](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *,
|
|
590
|
+
size_t) -> jsi::Value {
|
|
591
|
+
JNIEnv *e = env();
|
|
592
|
+
const bool deleted = retire(self, e, true);
|
|
593
|
+
if (e != nullptr && self->clockCls_ != nullptr) {
|
|
594
|
+
e->DeleteGlobalRef(self->clockCls_);
|
|
595
|
+
self->clockCls_ = nullptr;
|
|
596
|
+
self->take_ = nullptr;
|
|
597
|
+
}
|
|
598
|
+
return jsi::Value(deleted);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return jsi::Value::undefined();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private:
|
|
606
|
+
/**
|
|
607
|
+
* Free everything one surface generation owns, in order.
|
|
608
|
+
*
|
|
609
|
+
* The Surface and SurfaceTexture first, then the GL texture, then the class
|
|
610
|
+
* ref that was needed to make the call — each on the thread that owns it,
|
|
611
|
+
* with an attached JNIEnv and a current EGL context. Shared by `release` and
|
|
612
|
+
* by `create`, so replacing a surface and releasing one free exactly the same
|
|
613
|
+
* set; they diverged once, and the half `create` skipped was all of it.
|
|
614
|
+
*
|
|
615
|
+
* Safe to call on an empty slot: every field is checked and cleared, so a
|
|
616
|
+
* second call frees nothing rather than freeing twice.
|
|
617
|
+
*
|
|
618
|
+
* Returns whether a GL texture was actually deleted.
|
|
619
|
+
*/
|
|
620
|
+
static bool retire(SurfaceHostObject *self, JNIEnv *e, bool dropClass) {
|
|
621
|
+
if (e != nullptr && self->surfaceTexture_ != nullptr) {
|
|
622
|
+
jmethodID release = e->GetStaticMethodID(
|
|
623
|
+
self->cls_, "release",
|
|
624
|
+
"(Landroid/graphics/SurfaceTexture;Landroid/view/Surface;I)V");
|
|
625
|
+
e->CallStaticVoidMethod(self->cls_, release, self->surfaceTexture_,
|
|
626
|
+
self->surface_, self->generation_.load());
|
|
627
|
+
e->DeleteGlobalRef(self->surfaceTexture_);
|
|
628
|
+
e->DeleteGlobalRef(self->surface_);
|
|
629
|
+
self->surfaceTexture_ = nullptr;
|
|
630
|
+
self->surface_ = nullptr;
|
|
631
|
+
// Counted only when something was actually freed, so `live` is a true count
|
|
632
|
+
// rather than a count of calls. A no-op retire must not read as a free.
|
|
633
|
+
self->retires_++;
|
|
634
|
+
}
|
|
635
|
+
if (e != nullptr && self->flag_ != nullptr) {
|
|
636
|
+
// The listener watching the retired SurfaceTexture goes with it; leaving
|
|
637
|
+
// the ref alive kept a callback pointed at a texture nobody could read.
|
|
638
|
+
e->DeleteGlobalRef(self->flag_);
|
|
639
|
+
self->flag_ = nullptr;
|
|
640
|
+
}
|
|
641
|
+
bool deleted = false;
|
|
642
|
+
if (self->texture_ != 0 && eglGetCurrentContext() != EGL_NO_CONTEXT) {
|
|
643
|
+
glDeleteTextures(1, &self->texture_);
|
|
644
|
+
deleted = glGetError() == GL_NO_ERROR;
|
|
645
|
+
self->texture_ = 0;
|
|
646
|
+
}
|
|
647
|
+
/*
|
|
648
|
+
The class ref goes only when nothing will ask for it again.
|
|
649
|
+
|
|
650
|
+
`create` replaces it on the next line and `release` is the end of the
|
|
651
|
+
object, so both drop it. `releaseGeneration` retires a superseded surface
|
|
652
|
+
while the host object stays in use, and every other query on it guards on
|
|
653
|
+
a null class by returning a default — dropping it there would turn a
|
|
654
|
+
surface swap into a stretch of silent zeroes.
|
|
655
|
+
*/
|
|
656
|
+
if (dropClass && e != nullptr && self->cls_ != nullptr) {
|
|
657
|
+
e->DeleteGlobalRef(self->cls_);
|
|
658
|
+
self->cls_ = nullptr;
|
|
659
|
+
}
|
|
660
|
+
self->generation_.store(0);
|
|
661
|
+
return deleted;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* The slot's mutable target, guarded because two runtimes read it.
|
|
666
|
+
*
|
|
667
|
+
* This object is created once, at install, **before any surface exists** and
|
|
668
|
+
* before anything captures it. `create` fills the fields in place; retirement
|
|
669
|
+
* clears them, and only for its own generation. So a consumer captures this
|
|
670
|
+
* object once and every call resolves through whatever the slot is pointing at
|
|
671
|
+
* now — there is nothing replaceable to capture, which is the whole point.
|
|
672
|
+
*
|
|
673
|
+
* `generation_` is atomic because the JS thread reads it (`generation()`)
|
|
674
|
+
* while the UI runtime writes it (`create`, `releaseGeneration`). The JNI
|
|
675
|
+
* handles beside it are only ever touched on the UI runtime, where the GL
|
|
676
|
+
* context lives.
|
|
677
|
+
*/
|
|
678
|
+
std::mutex slot_;
|
|
679
|
+
GLuint texture_ = 0;
|
|
680
|
+
/** Surfaces made, and surfaces actually freed. @see surfaceCounters */
|
|
681
|
+
long creates_ = 0;
|
|
682
|
+
long retires_ = 0;
|
|
683
|
+
/** Raised by the frame listener, cleared when the UI runtime consumes it. */
|
|
684
|
+
jobject flag_ = nullptr;
|
|
685
|
+
float sx_ = 1.f, sy_ = 1.f, tx_ = 0.f, ty_ = 0.f;
|
|
686
|
+
/** Clip time of the frame on screen, and how late it got there. */
|
|
687
|
+
int64_t ptsUs_ = -1;
|
|
688
|
+
/** The canonical sample for the frame on screen. See FrameClock.Field. */
|
|
689
|
+
jlong sample_[17] = {0};
|
|
690
|
+
jfloat matrix_[16] = {0};
|
|
691
|
+
/** The surface generation this instance registered under. */
|
|
692
|
+
std::atomic<jint> generation_{0};
|
|
693
|
+
jobject surfaceTexture_ = nullptr;
|
|
694
|
+
jobject surface_ = nullptr;
|
|
695
|
+
jclass cls_ = nullptr;
|
|
696
|
+
/** Held across frames so the per-frame path never calls `FindClass`. */
|
|
697
|
+
jclass clockCls_ = nullptr;
|
|
698
|
+
jmethodID take_ = nullptr;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
} // namespace
|
|
702
|
+
} // namespace pictor
|
|
703
|
+
|
|
704
|
+
extern "C" JNIEXPORT jboolean JNICALL
|
|
705
|
+
Java_dev_pictor_video_PictorVideoModule_nativeInstallVideoSurface(
|
|
706
|
+
JNIEnv *e, jclass, jlong runtimePtr) {
|
|
707
|
+
auto *runtime = reinterpret_cast<jsi::Runtime *>(runtimePtr);
|
|
708
|
+
if (runtime == nullptr) return JNI_FALSE;
|
|
709
|
+
e->GetJavaVM(&pictor::g_vm);
|
|
710
|
+
|
|
711
|
+
// Idempotent, and that is the whole point.
|
|
712
|
+
//
|
|
713
|
+
// This used to construct a new `SurfaceHostObject` on every call and
|
|
714
|
+
// overwrite the global with it. A caller that installed once during render
|
|
715
|
+
// and again from an effect — which is exactly what the hook did — ended up
|
|
716
|
+
// with two objects: the consumer captured the first, `create` ran on the
|
|
717
|
+
// second, and the two reported different generations forever. Neither was
|
|
718
|
+
// wrong about itself. Reinstalling is not a way to obtain the slot; asking
|
|
719
|
+
// for it is.
|
|
720
|
+
auto existing =
|
|
721
|
+
runtime->global().getProperty(*runtime, "__pictorVideoSurface");
|
|
722
|
+
if (existing.isObject()) return JNI_TRUE;
|
|
723
|
+
|
|
724
|
+
pictor::g_factoryCount.fetch_add(1);
|
|
725
|
+
runtime->global().setProperty(
|
|
726
|
+
*runtime, "__pictorVideoSurface",
|
|
727
|
+
jsi::Object::createFromHostObject(
|
|
728
|
+
*runtime, std::make_shared<pictor::SurfaceHostObject>()));
|
|
729
|
+
// Installed once and never replaced. A consumer that captures this object at
|
|
730
|
+
// any point — including before a surface has ever been created — holds a
|
|
731
|
+
// stable slot, not a snapshot, so its calls resolve through whatever target
|
|
732
|
+
// the slot has *now*. Replacing this object on surface creation is what would
|
|
733
|
+
// put a consumer back in the position of holding something stale.
|
|
734
|
+
return JNI_TRUE;
|
|
735
|
+
}
|