@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pictor contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @pictor-sdk/video-android
|
|
2
|
+
|
|
3
|
+
Optional Android video playback and export adapter for Pictor. Requires Android API 29+.
|
|
4
|
+
|
|
5
|
+
Part of [Pictor](https://github.com/zahidhasanaunto/pictor), an MIT-licensed, composable
|
|
6
|
+
photo and video editor SDK for React Native. Public APIs are pre-1.0 and experimental.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @pictor-sdk/video-android@0.1.0
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Install the package's declared peer dependencies in your app. Keep React, React Native,
|
|
15
|
+
Skia, Reanimated and Worklets single-instance; never install a second renderer stack.
|
|
16
|
+
|
|
17
|
+
The reference host uses Expo SDK 54, React Native 0.81, React 19.1,
|
|
18
|
+
Skia 2.2.12, Reanimated 4.1 and Worklets 0.5.1 (New Architecture).
|
|
19
|
+
Device acceptance was performed on a Pixel 7; broader peer ranges are not a claim
|
|
20
|
+
of device verification on every supported version/platform.
|
|
21
|
+
|
|
22
|
+
## Android setup
|
|
23
|
+
|
|
24
|
+
Android only, API 29+. Requires a native development/release build; Expo Go is not supported.
|
|
25
|
+
Add "@pictor-sdk/video-android" to the Expo app config's plugins array, then run Expo prebuild
|
|
26
|
+
and rebuild your app. Bare React Native hosts must set minSdkVersion to at least 29.
|
|
27
|
+
The package includes its autolinking config, TurboModule specs, Kotlin/C++ sources and CMake build.
|
|
28
|
+
Media3 is pinned to 1.9.4. iOS video playback/export is not provided.
|
|
29
|
+
|
|
30
|
+
Diagnostics are disabled in ordinary release builds. Do not set the pictorDiagnostics Gradle
|
|
31
|
+
property in a shipped build. The host owns source permissions, durable ingestion and export files.
|
|
32
|
+
|
|
33
|
+
## Documentation and contributing
|
|
34
|
+
|
|
35
|
+
- [Repository and example app](https://github.com/zahidhasanaunto/pictor)
|
|
36
|
+
- [Public API inventory](https://github.com/zahidhasanaunto/pictor/blob/main/docs/API-SURFACE.md)
|
|
37
|
+
- [Release and contributor guide](https://github.com/zahidhasanaunto/pictor/blob/main/docs/RELEASING.md)
|
|
38
|
+
|
|
39
|
+
The original development scope was @pictor/*; public packages use @pictor-sdk/*.
|
|
40
|
+
Use the public scope in all imports and app configuration.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.13)
|
|
2
|
+
project(pictor_video)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
|
|
6
|
+
add_library(pictor_video SHARED
|
|
7
|
+
src/main/cpp/pictor_videosurface.cpp
|
|
8
|
+
src/main/cpp/pictor_publish.cpp
|
|
9
|
+
src/main/cpp/pictor_overlaystage.cpp
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# JSI comes from React Native's prefab. Linking it is what lets this library
|
|
13
|
+
# expose a jsi::HostObject, which is the supported route into a worklet runtime.
|
|
14
|
+
find_package(ReactAndroid REQUIRED CONFIG)
|
|
15
|
+
find_library(log-lib log)
|
|
16
|
+
find_library(android-lib android)
|
|
17
|
+
# The presentation surface creates a GL texture in Skia's context.
|
|
18
|
+
find_library(egl-lib EGL)
|
|
19
|
+
find_library(gles-lib GLESv2)
|
|
20
|
+
# CRC32 for the overlay sidecars, the same checksum `OverlayResources.kt` reads.
|
|
21
|
+
find_library(z-lib z)
|
|
22
|
+
|
|
23
|
+
target_link_libraries(pictor_video
|
|
24
|
+
ReactAndroid::jsi
|
|
25
|
+
${log-lib}
|
|
26
|
+
${android-lib}
|
|
27
|
+
${egl-lib}
|
|
28
|
+
${gles-lib}
|
|
29
|
+
${z-lib}
|
|
30
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
repositories { google(); mavenCentral() }
|
|
3
|
+
dependencies {
|
|
4
|
+
classpath "com.android.tools.build:gradle:8.6.0"
|
|
5
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
apply plugin: "com.android.library"
|
|
10
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
11
|
+
apply plugin: "com.facebook.react"
|
|
12
|
+
|
|
13
|
+
android {
|
|
14
|
+
namespace "dev.pictor.video"
|
|
15
|
+
compileSdk 35
|
|
16
|
+
|
|
17
|
+
defaultConfig {
|
|
18
|
+
// 29 is now conservative, and deliberately so.
|
|
19
|
+
//
|
|
20
|
+
// It was forced by the retired custom backend: ImageReader.newInstance with
|
|
21
|
+
// USAGE_GPU_SAMPLED_IMAGE (29), Image.getHardwareBuffer() (28), HardwareBuffer
|
|
22
|
+
// itself (26). None of that code exists any more. What the Media3 + external-OES
|
|
23
|
+
// path actually needs is far lower — SurfaceTexture and GL_TEXTURE_EXTERNAL_OES
|
|
24
|
+
// have been present since API 11 and 8, Media3 1.9.4's own floor is 23, and
|
|
25
|
+
// React Native 0.76 requires 24 — so 24 is the real requirement.
|
|
26
|
+
//
|
|
27
|
+
// It stays at 29 because nothing below it has been *run*. The only device here
|
|
28
|
+
// is API 36 and the one installed system image is incomplete, so 24 would be a
|
|
29
|
+
// support claim resting on reading API levels rather than on a passing test.
|
|
30
|
+
// Lower it when an older device or a complete emulator image can prove it.
|
|
31
|
+
// Installing this package raises a host's minSdk; not installing it leaves
|
|
32
|
+
// image-only Pictor consumers exactly where they were.
|
|
33
|
+
minSdk 29
|
|
34
|
+
externalNativeBuild {
|
|
35
|
+
cmake {
|
|
36
|
+
cppFlags "-std=c++17"
|
|
37
|
+
// React Native's prefab libraries are built against the shared STL, and
|
|
38
|
+
// linking JSI against a static one fails configuration outright.
|
|
39
|
+
arguments "-DANDROID_STL=c++_shared"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
externalNativeBuild {
|
|
45
|
+
cmake {
|
|
46
|
+
path "CMakeLists.txt"
|
|
47
|
+
version "3.22.1"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
The diagnostics boundary is compile-time, not a runtime flag.
|
|
53
|
+
|
|
54
|
+
Fault injection, artificial stalls, timing holds, provenance hashing and
|
|
55
|
+
synthetic heartbeats exist for conformance gates. A shipped build must not
|
|
56
|
+
be able to reach them by any sequence of calls, so whether they exist at all
|
|
57
|
+
is decided here and baked into `BuildConfig` — not stored in a mutable field
|
|
58
|
+
that arbitrary JavaScript could set.
|
|
59
|
+
|
|
60
|
+
Off unless a build explicitly asks:
|
|
61
|
+
|
|
62
|
+
./gradlew :app:assembleRelease -PpictorDiagnostics=true
|
|
63
|
+
|
|
64
|
+
A debug build gets it for free, because a debug build is already a
|
|
65
|
+
development artefact. A release build without the property has the constant
|
|
66
|
+
false, every mutating gate method refuses synchronously, and every site that
|
|
67
|
+
would act on an injected fault is dead.
|
|
68
|
+
*/
|
|
69
|
+
buildTypes {
|
|
70
|
+
debug {
|
|
71
|
+
buildConfigField "boolean", "PICTOR_DIAGNOSTICS", "true"
|
|
72
|
+
}
|
|
73
|
+
release {
|
|
74
|
+
buildConfigField "boolean", "PICTOR_DIAGNOSTICS",
|
|
75
|
+
project.hasProperty("pictorDiagnostics") ? "true" : "false"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
buildFeatures {
|
|
80
|
+
buildConfig true
|
|
81
|
+
// Required to consume React Native's JSI headers and library.
|
|
82
|
+
prefab true
|
|
83
|
+
}
|
|
84
|
+
compileOptions {
|
|
85
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
86
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
87
|
+
}
|
|
88
|
+
kotlinOptions { jvmTarget = "17" }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
repositories { google(); mavenCentral() }
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One Media3 version, everywhere.
|
|
95
|
+
*
|
|
96
|
+
* 1.9.4 is the last release that builds against compileSdk 35; 1.10.0 raised
|
|
97
|
+
* its floor to 36. Revisit when Pictor targets an Expo SDK that compiles
|
|
98
|
+
* against 36 — see docs/research/media3-mapping.md.
|
|
99
|
+
*
|
|
100
|
+
* `media3-transformer` and `media3-effect` arrive here at V3.3, for the
|
|
101
|
+
* Transformer–Skia GL interoperability gate. They are still weight an optional
|
|
102
|
+
* adapter's consumers pay for; they are here because the gate needs them, not
|
|
103
|
+
* in anticipation of a slice that has not started.
|
|
104
|
+
*/
|
|
105
|
+
ext.media3Version = "1.9.4"
|
|
106
|
+
|
|
107
|
+
dependencies {
|
|
108
|
+
implementation "com.facebook.react:react-android"
|
|
109
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib"
|
|
110
|
+
implementation "androidx.media3:media3-exoplayer:${project.ext.media3Version}"
|
|
111
|
+
implementation "androidx.media3:media3-transformer:${project.ext.media3Version}"
|
|
112
|
+
implementation "androidx.media3:media3-effect:${project.ext.media3Version}"
|
|
113
|
+
implementation "androidx.media3:media3-common:${project.ext.media3Version}"
|
|
114
|
+
|
|
115
|
+
// FrameClock's pairing rules are Kotlin and have no Android dependencies, so
|
|
116
|
+
// they are tested on the JVM rather than reimplemented in TS to suit the
|
|
117
|
+
// Node harness. Run with `pnpm test:android`.
|
|
118
|
+
testImplementation "junit:junit:4.13.2"
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Staging overlay rasters, synchronously, without a filesystem package in JS.
|
|
3
|
+
|
|
4
|
+
## Why this exists in the adapter
|
|
5
|
+
|
|
6
|
+
The export reader that consumes these sidecars is `OverlayResources.kt`, in
|
|
7
|
+
this package. The writer belongs beside it: a host that had to produce the
|
|
8
|
+
bytes *and* choose where they live would need a filesystem dependency and a
|
|
9
|
+
path convention, and the two ends of one format would then be maintained in
|
|
10
|
+
different repositories.
|
|
11
|
+
|
|
12
|
+
## Why JSI rather than a TurboModule method
|
|
13
|
+
|
|
14
|
+
The payload is a raster — several megabytes at export scale. Codegen carries
|
|
15
|
+
numbers, strings and arrays; the only way to move raw bytes through it is
|
|
16
|
+
base64, which costs a 4/3 expansion, an encode on the JS thread and a decode
|
|
17
|
+
on the native side, for data that is already contiguous memory. A JSI host
|
|
18
|
+
function reads the `ArrayBuffer` in place.
|
|
19
|
+
|
|
20
|
+
## Why JS cannot name the destination
|
|
21
|
+
|
|
22
|
+
`stage` takes bytes and dimensions and returns a token. The directory is
|
|
23
|
+
chosen here from the app's own cache path, and the file name is a counter and
|
|
24
|
+
a random suffix — never anything derived from a document string. An overlay id
|
|
25
|
+
is author-controlled text; letting it reach a path is how `../` arrives in a
|
|
26
|
+
filename.
|
|
27
|
+
|
|
28
|
+
## Publication
|
|
29
|
+
|
|
30
|
+
Bytes go to `<dir>/<name>.part`, are flushed and fsynced, and only then
|
|
31
|
+
renamed onto the final name. A reader therefore never opens a file that is
|
|
32
|
+
still being written: the sidecar either does not exist or is complete, with no
|
|
33
|
+
state in between for a crash to land in.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
#include <jni.h>
|
|
37
|
+
#include <jsi/jsi.h>
|
|
38
|
+
|
|
39
|
+
#include <errno.h>
|
|
40
|
+
#include <dirent.h>
|
|
41
|
+
#include <fcntl.h>
|
|
42
|
+
#include <string.h>
|
|
43
|
+
#include <sys/stat.h>
|
|
44
|
+
#include <unistd.h>
|
|
45
|
+
#include <zlib.h>
|
|
46
|
+
|
|
47
|
+
#include <memory>
|
|
48
|
+
#include <mutex>
|
|
49
|
+
#include <string>
|
|
50
|
+
#include <unordered_map>
|
|
51
|
+
#include <vector>
|
|
52
|
+
|
|
53
|
+
using namespace facebook;
|
|
54
|
+
|
|
55
|
+
namespace {
|
|
56
|
+
|
|
57
|
+
/** The subdirectory this module owns. Nothing outside it is ever swept. */
|
|
58
|
+
constexpr const char *kDirName = "pictor-overlays";
|
|
59
|
+
/** The suffix a published sidecar carries. */
|
|
60
|
+
constexpr const char *kSuffix = ".pictorraster";
|
|
61
|
+
/** The suffix an in-progress write carries. */
|
|
62
|
+
constexpr const char *kPartSuffix = ".part";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The largest raster that will be accepted, in bytes.
|
|
66
|
+
*
|
|
67
|
+
* 256 MB is far above any plausible text overlay — a 4096×4096 RGBA raster is
|
|
68
|
+
* 64 MB — and far below a size that would threaten the process. The point is
|
|
69
|
+
* not to guess the right maximum but to have one: without it a corrupted or
|
|
70
|
+
* hostile descriptor turns into an allocation the device cannot satisfy, and
|
|
71
|
+
* the failure is an abort rather than a refusal.
|
|
72
|
+
*/
|
|
73
|
+
constexpr uint64_t kMaxBytes = 256ull * 1024ull * 1024ull;
|
|
74
|
+
|
|
75
|
+
/** Bytes per pixel for the one format this stages. */
|
|
76
|
+
constexpr uint64_t kBytesPerPixel = 4;
|
|
77
|
+
|
|
78
|
+
struct Staged {
|
|
79
|
+
std::string path;
|
|
80
|
+
uint64_t byteLength;
|
|
81
|
+
uint32_t checksum;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
class Staging {
|
|
85
|
+
public:
|
|
86
|
+
explicit Staging(std::string dir) : dir_(std::move(dir)) {
|
|
87
|
+
::mkdir(dir_.c_str(), 0700);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Write one raster and publish it.
|
|
92
|
+
*
|
|
93
|
+
* Every failure path removes whatever it created. A half-written `.part` left
|
|
94
|
+
* behind would be swept later, but "later" is the next cold start, and an
|
|
95
|
+
* export that failed is exactly the run nobody comes back to tidy.
|
|
96
|
+
*/
|
|
97
|
+
bool stage(const uint8_t *bytes, uint64_t length, uint32_t *outChecksum,
|
|
98
|
+
std::string *outToken, std::string *outPath, std::string *outError) {
|
|
99
|
+
const uint64_t id = ++counter_;
|
|
100
|
+
// Never derived from a document string: an overlay id is author-controlled
|
|
101
|
+
// text, and a path built from one is how `../` arrives in a filename.
|
|
102
|
+
const std::string name = "ov" + std::to_string(id) + "-" + std::to_string(::getpid()) +
|
|
103
|
+
"-" + std::to_string(::rand());
|
|
104
|
+
const std::string finalPath = dir_ + "/" + name + kSuffix;
|
|
105
|
+
const std::string partPath = finalPath + kPartSuffix;
|
|
106
|
+
|
|
107
|
+
int fd = ::open(partPath.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0600);
|
|
108
|
+
if (fd < 0) {
|
|
109
|
+
*outError = "could not create a staging file";
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
uint64_t written = 0;
|
|
114
|
+
bool ok = true;
|
|
115
|
+
while (written < length) {
|
|
116
|
+
const ssize_t n = ::write(fd, bytes + written, static_cast<size_t>(length - written));
|
|
117
|
+
if (n <= 0) {
|
|
118
|
+
if (errno == EINTR) continue;
|
|
119
|
+
ok = false;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
written += static_cast<uint64_t>(n);
|
|
123
|
+
}
|
|
124
|
+
// Durable before the rename: publishing a name that points at unflushed
|
|
125
|
+
// bytes is the same hazard as publishing a partial file.
|
|
126
|
+
if (ok && ::fsync(fd) != 0) ok = false;
|
|
127
|
+
::close(fd);
|
|
128
|
+
|
|
129
|
+
if (!ok || written != length) {
|
|
130
|
+
::unlink(partPath.c_str());
|
|
131
|
+
*outError = "the raster could not be written in full";
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (::rename(partPath.c_str(), finalPath.c_str()) != 0) {
|
|
136
|
+
::unlink(partPath.c_str());
|
|
137
|
+
*outError = "the raster could not be published";
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
uLong crc = ::crc32(0L, Z_NULL, 0);
|
|
142
|
+
crc = ::crc32(crc, bytes, static_cast<uInt>(length));
|
|
143
|
+
|
|
144
|
+
const std::string token = "stg" + std::to_string(id);
|
|
145
|
+
{
|
|
146
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
147
|
+
live_.emplace(token, Staged{finalPath, length, static_cast<uint32_t>(crc)});
|
|
148
|
+
created_ += 1;
|
|
149
|
+
}
|
|
150
|
+
*outChecksum = static_cast<uint32_t>(crc);
|
|
151
|
+
*outToken = token;
|
|
152
|
+
*outPath = finalPath;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Exact and idempotent: releasing twice is not an error and frees once. */
|
|
157
|
+
bool release(const std::string &token) {
|
|
158
|
+
std::string path;
|
|
159
|
+
{
|
|
160
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
161
|
+
auto it = live_.find(token);
|
|
162
|
+
if (it == live_.end()) return false;
|
|
163
|
+
path = it->second.path;
|
|
164
|
+
live_.erase(it);
|
|
165
|
+
released_ += 1;
|
|
166
|
+
}
|
|
167
|
+
::unlink(path.c_str());
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
int releaseAll() {
|
|
172
|
+
std::vector<std::string> tokens;
|
|
173
|
+
{
|
|
174
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
175
|
+
tokens.reserve(live_.size());
|
|
176
|
+
for (const auto &entry : live_) tokens.push_back(entry.first);
|
|
177
|
+
}
|
|
178
|
+
int n = 0;
|
|
179
|
+
for (const auto &t : tokens) {
|
|
180
|
+
if (release(t)) n += 1;
|
|
181
|
+
}
|
|
182
|
+
return n;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Remove orphans left by a previous process.
|
|
187
|
+
*
|
|
188
|
+
* Scoped to this module's own directory and to its own suffixes, so it can
|
|
189
|
+
* never take a cache file belonging to anything else. Anything currently live
|
|
190
|
+
* in *this* process is skipped, which is what makes it safe to call at any
|
|
191
|
+
* time rather than only before the first export.
|
|
192
|
+
*/
|
|
193
|
+
int sweep() {
|
|
194
|
+
std::vector<std::string> livePaths;
|
|
195
|
+
{
|
|
196
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
197
|
+
for (const auto &entry : live_) livePaths.push_back(entry.second.path);
|
|
198
|
+
}
|
|
199
|
+
DIR *d = ::opendir(dir_.c_str());
|
|
200
|
+
if (d == nullptr) return 0;
|
|
201
|
+
int removed = 0;
|
|
202
|
+
while (dirent *e = ::readdir(d)) {
|
|
203
|
+
const std::string name = e->d_name;
|
|
204
|
+
if (name == "." || name == "..") continue;
|
|
205
|
+
const bool ours = name.size() > 3 && name.compare(0, 2, "ov") == 0 &&
|
|
206
|
+
(name.find(kSuffix) != std::string::npos);
|
|
207
|
+
if (!ours) continue;
|
|
208
|
+
const std::string path = dir_ + "/" + name;
|
|
209
|
+
bool isLive = false;
|
|
210
|
+
for (const auto &p : livePaths) {
|
|
211
|
+
if (p == path) { isLive = true; break; }
|
|
212
|
+
}
|
|
213
|
+
if (isLive) continue;
|
|
214
|
+
if (::unlink(path.c_str()) == 0) removed += 1;
|
|
215
|
+
}
|
|
216
|
+
::closedir(d);
|
|
217
|
+
return removed;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
void ledger(uint64_t *created, uint64_t *released, uint64_t *live) {
|
|
221
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
222
|
+
*created = created_;
|
|
223
|
+
*released = released_;
|
|
224
|
+
*live = live_.size();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private:
|
|
228
|
+
std::string dir_;
|
|
229
|
+
std::mutex mutex_;
|
|
230
|
+
std::unordered_map<std::string, Staged> live_;
|
|
231
|
+
uint64_t counter_ = 0;
|
|
232
|
+
uint64_t created_ = 0;
|
|
233
|
+
uint64_t released_ = 0;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
std::shared_ptr<Staging> gStaging;
|
|
237
|
+
|
|
238
|
+
/** Is `v` a whole number that fits a positive 32-bit extent? */
|
|
239
|
+
bool wholeExtent(double v, uint64_t *out) {
|
|
240
|
+
if (!(v > 0) || v != v) return false; // zero, negative, or NaN
|
|
241
|
+
if (v > 65536.0) return false; // far past any raster
|
|
242
|
+
const uint64_t n = static_cast<uint64_t>(v);
|
|
243
|
+
if (static_cast<double>(n) != v) return false; // not an integer
|
|
244
|
+
*out = n;
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
class StagingHostObject : public jsi::HostObject {
|
|
249
|
+
public:
|
|
250
|
+
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override {
|
|
251
|
+
const std::string prop = name.utf8(rt);
|
|
252
|
+
|
|
253
|
+
if (prop == "stage") {
|
|
254
|
+
return jsi::Function::createFromHostFunction(
|
|
255
|
+
rt, name, 3,
|
|
256
|
+
[](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args,
|
|
257
|
+
size_t count) -> jsi::Value {
|
|
258
|
+
if (gStaging == nullptr) throw jsi::JSError(rt, "overlay staging is not installed");
|
|
259
|
+
if (count < 3) throw jsi::JSError(rt, "stage(bytes, width, height)");
|
|
260
|
+
|
|
261
|
+
if (!args[0].isObject() || !args[0].asObject(rt).isArrayBuffer(rt)) {
|
|
262
|
+
throw jsi::JSError(rt, "stage expects an ArrayBuffer");
|
|
263
|
+
}
|
|
264
|
+
uint64_t w = 0, h = 0;
|
|
265
|
+
if (!wholeExtent(args[1].asNumber(), &w) || !wholeExtent(args[2].asNumber(), &h)) {
|
|
266
|
+
throw jsi::JSError(rt, "raster dimensions must be whole positive numbers");
|
|
267
|
+
}
|
|
268
|
+
/*
|
|
269
|
+
Overflow-safe before multiplying, not after.
|
|
270
|
+
|
|
271
|
+
`w * h * 4` in 64 bits cannot overflow for extents capped at
|
|
272
|
+
65536, but the check is written as a division rather than relying
|
|
273
|
+
on that cap staying where it is.
|
|
274
|
+
*/
|
|
275
|
+
if (w > kMaxBytes / kBytesPerPixel / h) {
|
|
276
|
+
throw jsi::JSError(rt, "raster is larger than staging accepts");
|
|
277
|
+
}
|
|
278
|
+
const uint64_t expected = w * h * kBytesPerPixel;
|
|
279
|
+
if (expected == 0 || expected > kMaxBytes) {
|
|
280
|
+
throw jsi::JSError(rt, "raster is larger than staging accepts");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
auto buffer = args[0].asObject(rt).getArrayBuffer(rt);
|
|
284
|
+
const uint64_t length = static_cast<uint64_t>(buffer.size(rt));
|
|
285
|
+
if (length != expected) {
|
|
286
|
+
// The descriptor and the payload disagree; writing either would
|
|
287
|
+
// publish a sidecar the reader must then reject.
|
|
288
|
+
throw jsi::JSError(rt, "raster byte length does not match its dimensions");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
uint32_t checksum = 0;
|
|
292
|
+
std::string token, path, error;
|
|
293
|
+
if (!gStaging->stage(buffer.data(rt), length, &checksum, &token, &path, &error)) {
|
|
294
|
+
// No path in the message: an app-private location is not
|
|
295
|
+
// something a user-facing error should carry.
|
|
296
|
+
throw jsi::JSError(rt, error);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
jsi::Object out(rt);
|
|
300
|
+
out.setProperty(rt, "token", jsi::String::createFromUtf8(rt, token));
|
|
301
|
+
out.setProperty(rt, "file", jsi::String::createFromUtf8(rt, path));
|
|
302
|
+
out.setProperty(rt, "byteLength", jsi::Value(static_cast<double>(length)));
|
|
303
|
+
out.setProperty(rt, "checksum", jsi::Value(static_cast<double>(checksum)));
|
|
304
|
+
return out;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (prop == "release") {
|
|
309
|
+
return jsi::Function::createFromHostFunction(
|
|
310
|
+
rt, name, 1,
|
|
311
|
+
[](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args,
|
|
312
|
+
size_t count) -> jsi::Value {
|
|
313
|
+
if (gStaging == nullptr || count < 1 || !args[0].isString()) return jsi::Value(false);
|
|
314
|
+
return jsi::Value(gStaging->release(args[0].asString(rt).utf8(rt)));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (prop == "releaseAll") {
|
|
319
|
+
return jsi::Function::createFromHostFunction(
|
|
320
|
+
rt, name, 0,
|
|
321
|
+
[](jsi::Runtime &, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value {
|
|
322
|
+
if (gStaging == nullptr) return jsi::Value(0);
|
|
323
|
+
return jsi::Value(gStaging->releaseAll());
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (prop == "sweep") {
|
|
328
|
+
return jsi::Function::createFromHostFunction(
|
|
329
|
+
rt, name, 0,
|
|
330
|
+
[](jsi::Runtime &, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value {
|
|
331
|
+
if (gStaging == nullptr) return jsi::Value(0);
|
|
332
|
+
return jsi::Value(gStaging->sweep());
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (prop == "ledger") {
|
|
337
|
+
return jsi::Function::createFromHostFunction(
|
|
338
|
+
rt, name, 0,
|
|
339
|
+
[](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value {
|
|
340
|
+
uint64_t created = 0, released = 0, live = 0;
|
|
341
|
+
if (gStaging != nullptr) gStaging->ledger(&created, &released, &live);
|
|
342
|
+
jsi::Object out(rt);
|
|
343
|
+
out.setProperty(rt, "created", jsi::Value(static_cast<double>(created)));
|
|
344
|
+
out.setProperty(rt, "released", jsi::Value(static_cast<double>(released)));
|
|
345
|
+
out.setProperty(rt, "live", jsi::Value(static_cast<double>(live)));
|
|
346
|
+
return out;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return jsi::Value::undefined();
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
} // namespace
|
|
355
|
+
|
|
356
|
+
extern "C" JNIEXPORT jboolean JNICALL
|
|
357
|
+
Java_dev_pictor_video_PictorVideoModule_nativeInstallOverlayStaging(
|
|
358
|
+
JNIEnv *env, jobject, jlong runtimePtr, jstring cacheDir) {
|
|
359
|
+
if (runtimePtr == 0 || cacheDir == nullptr) return JNI_FALSE;
|
|
360
|
+
auto *rt = reinterpret_cast<jsi::Runtime *>(runtimePtr);
|
|
361
|
+
|
|
362
|
+
const char *chars = env->GetStringUTFChars(cacheDir, nullptr);
|
|
363
|
+
if (chars == nullptr) return JNI_FALSE;
|
|
364
|
+
const std::string dir = std::string(chars) + "/" + kDirName;
|
|
365
|
+
env->ReleaseStringUTFChars(cacheDir, chars);
|
|
366
|
+
|
|
367
|
+
gStaging = std::make_shared<Staging>(dir);
|
|
368
|
+
rt->global().setProperty(
|
|
369
|
+
*rt, "__pictorOverlayStaging",
|
|
370
|
+
jsi::Object::createFromHostObject(*rt, std::make_shared<StagingHostObject>()));
|
|
371
|
+
return JNI_TRUE;
|
|
372
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
One syscall, because publication has to survive a power cut.
|
|
3
|
+
|
|
4
|
+
`open(O_CREAT|O_EXCL)` then `rename` is race-safe — the kernel decides who
|
|
5
|
+
claims the name — but it is two operations, and a process that dies between
|
|
6
|
+
them leaves an empty file that looks claimed. Nothing afterwards can tell that
|
|
7
|
+
from a destination someone meant to create, so it blocks every later
|
|
8
|
+
`overwrite: false` export to the same path until a human deletes it.
|
|
9
|
+
|
|
10
|
+
`renameat2` with `RENAME_NOREPLACE` is the primitive that does not have that
|
|
11
|
+
shape: it either publishes the complete staging file or reports `EEXIST`, and
|
|
12
|
+
there is no state in between for a crash to land in.
|
|
13
|
+
|
|
14
|
+
It is reached through `syscall(2)` rather than the libc wrapper. Bionic only
|
|
15
|
+
exposes `renameat2` from API 30, this module supports older devices, and
|
|
16
|
+
whether the *kernel* has it is a different question from whether the *headers*
|
|
17
|
+
do — which is exactly the question that has to be answered on the device
|
|
18
|
+
rather than at build time.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
#include <jni.h>
|
|
22
|
+
|
|
23
|
+
#include <errno.h>
|
|
24
|
+
#include <fcntl.h>
|
|
25
|
+
#include <sys/syscall.h>
|
|
26
|
+
#include <unistd.h>
|
|
27
|
+
|
|
28
|
+
#ifndef RENAME_NOREPLACE
|
|
29
|
+
#define RENAME_NOREPLACE (1 << 0)
|
|
30
|
+
#endif
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
The syscall numbers, for toolchains whose headers predate the call.
|
|
34
|
+
|
|
35
|
+
Stable ABI: a syscall number is part of the kernel's contract with userspace
|
|
36
|
+
and cannot be renumbered, so hard-coding them is safe in a way that
|
|
37
|
+
hard-coding almost nothing else is.
|
|
38
|
+
*/
|
|
39
|
+
#ifndef __NR_renameat2
|
|
40
|
+
#if defined(__aarch64__)
|
|
41
|
+
#define __NR_renameat2 276
|
|
42
|
+
#elif defined(__arm__)
|
|
43
|
+
#define __NR_renameat2 382
|
|
44
|
+
#elif defined(__i386__)
|
|
45
|
+
#define __NR_renameat2 353
|
|
46
|
+
#elif defined(__x86_64__)
|
|
47
|
+
#define __NR_renameat2 316
|
|
48
|
+
#endif
|
|
49
|
+
#endif
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Publish `from` as `to`, refusing to replace anything.
|
|
53
|
+
*
|
|
54
|
+
* Returns 0 on success, or the errno. `ENOSYS` means the kernel has no such
|
|
55
|
+
* call; `EINVAL` means it has the call and this filesystem will not take the
|
|
56
|
+
* flag. Both are answers about the destination, not about the caller, and the
|
|
57
|
+
* Kotlin side reads them as "this filesystem cannot do an atomic no-replace".
|
|
58
|
+
*/
|
|
59
|
+
extern "C" JNIEXPORT jint JNICALL
|
|
60
|
+
Java_dev_pictor_video_GateDestination_nativeRenameNoReplace(
|
|
61
|
+
JNIEnv *env, jobject /* self */, jstring from, jstring to) {
|
|
62
|
+
#ifndef __NR_renameat2
|
|
63
|
+
(void)env;
|
|
64
|
+
(void)from;
|
|
65
|
+
(void)to;
|
|
66
|
+
return ENOSYS;
|
|
67
|
+
#else
|
|
68
|
+
const char *fromPath = env->GetStringUTFChars(from, nullptr);
|
|
69
|
+
if (fromPath == nullptr) {
|
|
70
|
+
return EINVAL;
|
|
71
|
+
}
|
|
72
|
+
const char *toPath = env->GetStringUTFChars(to, nullptr);
|
|
73
|
+
if (toPath == nullptr) {
|
|
74
|
+
env->ReleaseStringUTFChars(from, fromPath);
|
|
75
|
+
return EINVAL;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
errno = 0;
|
|
79
|
+
long result =
|
|
80
|
+
syscall(__NR_renameat2, AT_FDCWD, fromPath, AT_FDCWD, toPath, RENAME_NOREPLACE);
|
|
81
|
+
int failure = (result == 0) ? 0 : errno;
|
|
82
|
+
|
|
83
|
+
env->ReleaseStringUTFChars(from, fromPath);
|
|
84
|
+
env->ReleaseStringUTFChars(to, toPath);
|
|
85
|
+
return failure;
|
|
86
|
+
#endif
|
|
87
|
+
}
|