@mieweb/pulsevault 0.5.0 → 0.6.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/CHANGELOG.md +127 -1
- package/OPERATIONS.md +123 -46
- package/PROTOCOL.md +27 -5
- package/README.md +28 -8
- package/dist/app.d.ts +7 -6
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +4 -3
- package/dist/app.js.map +1 -1
- package/dist/core.d.ts +22 -8
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +90 -17
- package/dist/core.js.map +1 -1
- package/dist/lib/completion.d.ts +8 -5
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +121 -34
- package/dist/lib/completion.js.map +1 -1
- package/dist/lib/deeplinks.d.ts +18 -2
- package/dist/lib/deeplinks.d.ts.map +1 -1
- package/dist/lib/deeplinks.js +13 -2
- package/dist/lib/deeplinks.js.map +1 -1
- package/dist/lib/magic.d.ts +41 -1
- package/dist/lib/magic.d.ts.map +1 -1
- package/dist/lib/magic.js +104 -0
- package/dist/lib/magic.js.map +1 -1
- package/dist/lib/options.d.ts +2 -0
- package/dist/lib/options.d.ts.map +1 -1
- package/dist/lib/options.js +8 -4
- package/dist/lib/options.js.map +1 -1
- package/dist/lib/protocol-version.gen.js +1 -1
- package/dist/lib/pulsevaultTus.d.ts.map +1 -1
- package/dist/lib/pulsevaultTus.js +35 -1
- package/dist/lib/pulsevaultTus.js.map +1 -1
- package/dist/lib/web-ready.d.ts +138 -18
- package/dist/lib/web-ready.d.ts.map +1 -1
- package/dist/lib/web-ready.js +453 -91
- package/dist/lib/web-ready.js.map +1 -1
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +241 -50
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/s3.d.ts.map +1 -1
- package/dist/storage/s3.js +21 -1
- package/dist/storage/s3.js.map +1 -1
- package/dist/storage/types.d.ts +57 -4
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +7 -0
- package/dist/storage/types.js.map +1 -1
- package/package.json +2 -2
- package/protocol/openapi.json +26 -3
- package/protocol/schemas/artifact-status.schema.json +13 -1
- package/protocol/schemas/deep-link.schema.json +2 -2
- package/scripts/web-ready-migrate.mjs +35 -11
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,130 @@ breaking changes, called out explicitly below.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-10-12
|
|
11
|
+
|
|
12
|
+
Protocol 2.5 (`PROTOCOL.md` §7.4): 2.4 adds the https pairing link form (#83), 2.5 the conform
|
|
13
|
+
step's `webReady` result. `webReady` becomes a **conform** step: whatever video a
|
|
14
|
+
person uploads, PulseVault serves it in the one format the Pulse app records, and a Pulse upload
|
|
15
|
+
is left untouched (#84).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **https pairing links** (#83): `buildUploadLink({ …, form: 'https' })`
|
|
20
|
+
returns `https://mieweb.github.io/pulse/open#v=1&artifactId=…&server=…&token=…`,
|
|
21
|
+
the same parameters as the `pulsecam://` form, in the fragment. It is a
|
|
22
|
+
Universal Link / App Link: a phone camera opens it, Pulse opens straight
|
|
23
|
+
from it when installed, and the page sends the person to the store when it
|
|
24
|
+
isn't. Browsers never send the fragment, so the link host never receives the
|
|
25
|
+
token. `PULSE_OPEN_URL` is exported. Without `form` the output is
|
|
26
|
+
unchanged, `pulsecam://`, because Pulse releases from before the link host
|
|
27
|
+
was claimed open the https form only through the page's button.
|
|
28
|
+
- **Conform target** (`CONFORM_TARGET`): faststart MP4, H.264 8-bit `yuv420p` (limited range)
|
|
29
|
+
with even dimensions, SDR (HDR tone-mapped to BT.709; SDR colour tags kept), the longest edge at most `webReady.maxEdge` (default 1920; aspect ratio and
|
|
30
|
+
orientation kept), rotation applied, AAC or no audio. `ensureWebReady` leaves a file already
|
|
31
|
+
in it byte-for-byte (`none`), remuxes losslessly when only the container or `moov` position is
|
|
32
|
+
off, and otherwise runs ffmpeg once, re-encoding only the off-target streams: HEVC and other
|
|
33
|
+
codecs to H.264, 10-bit HDR (PQ/HLG) tone-mapped to SDR (FFmpeg 8's `scale`, or `zscale` on
|
|
34
|
+
older builds), full-range video (iPhone screen recordings) to limited range, oversized video
|
|
35
|
+
scaled down, Opus/PCM and other audio to AAC.
|
|
36
|
+
- **Container change**: a `.mov`, `.m4v`, `.webm`, `.mkv`, `.3gp` or `.avi` becomes a new
|
|
37
|
+
`<id>.mp4` (`WebReadyAction` `conformed`). The artifact's stored file, `ext` and served
|
|
38
|
+
`Content-Type` follow it; the artifact id and its URLs don't change. The sidecar switches
|
|
39
|
+
first, then the original is deleted (`sourceExt` remembers it so `remove` cleans up after a
|
|
40
|
+
crash). New patch fields `ext` and `webReady` (local adapter; the S3 adapter refuses them).
|
|
41
|
+
`describeArtifact` reports the uploaded extension as `sourceExt` once it differs, and
|
|
42
|
+
`uploadIdOf(meta)` names tus's upload by it, so a replayed hook's `ext` and `uploadId` still
|
|
43
|
+
match `filename`.
|
|
44
|
+
- `webReady.timeoutSeconds` (default `60 + 10 ×` the duration in 1080p30 seconds, so 4K or high
|
|
45
|
+
frame rates get proportionally longer, at most an hour since it comes from the upload's own
|
|
46
|
+
metadata; set it explicitly for longer runs): a longer run is killed, the original
|
|
47
|
+
kept, and `skipped` recorded with the reason.
|
|
48
|
+
- The conversion's result is recorded on the artifact: `GET /artifacts/:id/status` and
|
|
49
|
+
`getStatus` report `webReady: { action, reason }` (protocol 2.5), and `describeArtifact`
|
|
50
|
+
returns it.
|
|
51
|
+
- `core.conformAvailable()` (also on `fastify.pulseVaultCore`): `false` when `webReady` is off or
|
|
52
|
+
ffmpeg/ffprobe are missing, for a host's health check.
|
|
53
|
+
- `createVideoValidator({ maxDurationSeconds? })`: a `validatePayload` hook that checks a video
|
|
54
|
+
by its content with ffprobe (a video stream with a duration above zero, including a browser
|
|
55
|
+
WebM without one in its header; a picture — PNG, JPEG, GIF, HEIC, a one-frame clip, counted
|
|
56
|
+
from the first two packets when the container has no frame count — is not a video), refusing with `422 That file isn't a video.` or `That video is
|
|
57
|
+
longer than the limit of …` (the configured limit exactly: "1 minute 1 second"); falls back to `sniffVideo` (`ftyp`, EBML or RIFF AVI) without
|
|
58
|
+
ffprobe. Also exported: `probeVideo`, `webReadyAvailable`, `CONFORM_VIDEO_EXTENSIONS`.
|
|
59
|
+
- An upload over `maxUploadSize` is refused with `413 That file is larger than 500 MB.` instead
|
|
60
|
+
of tus's "Maximum size exceeded".
|
|
61
|
+
- `webReady.probeTimeoutSeconds` and `createVideoValidator({ probeTimeoutSeconds })` (default 60):
|
|
62
|
+
every ffprobe run over an upload is bounded; the validator refuses with `422 That video
|
|
63
|
+
couldn't be checked in time.`
|
|
64
|
+
- Content types for `.webm`, `.mkv`, `.3gp` and `.avi`.
|
|
65
|
+
- While a video is being converted, `GET /artifacts/:id` is served with `max-age=0` instead of
|
|
66
|
+
the configured `cache` (which may be `immutable`), so no client keeps the original bytes once
|
|
67
|
+
the converted file replaces them.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- The default video `allowedExtensions` are `.mp4`, `.mov`, `.m4v`, `.webm`, `.mkv`, `.3gp` and
|
|
72
|
+
`.avi` (was `.mp4`). A host's own `allowedExtensions` still overrides them.
|
|
73
|
+
- A failed conversion is now recorded (`webReady: { action: "skipped", reason }`) as well as
|
|
74
|
+
logged, and still not retried; a conversion that never recorded a result is resumed by the
|
|
75
|
+
replay, as before.
|
|
76
|
+
- The local adapter's `remove` reads the sidecar itself (not its cache) and deletes the
|
|
77
|
+
converted `<id>.mp4` and the uploaded file as well as the stored one. A converted video's bytes
|
|
78
|
+
are always at `<id>.mp4`, and `resolve`/`getLocalPath` fall back to it when the sidecar's `ext`
|
|
79
|
+
file is gone: across instances sharing a workspace, where the last sidecar write wins (as
|
|
80
|
+
before; there is no cross-instance lock), a write from a copy older than a conversion still
|
|
81
|
+
serves the converted file.
|
|
82
|
+
- A client that re-sends the final `PATCH` of a finished upload whose file a conversion replaced
|
|
83
|
+
(a `.webm` conformed to `<id>.mp4`) gets `204`, as without a conversion: the local adapter's tus
|
|
84
|
+
store reads such an upload as complete and takes its empty tail without a write.
|
|
85
|
+
- A `webReady` conversion needs the adapter's `generation`: an adapter with `getLocalPath` but no
|
|
86
|
+
generation has its videos recorded `skipped` (`the storage adapter keeps no generation for this
|
|
87
|
+
upload`). The local adapter gives one at `markReady` to an upload reserved before this release.
|
|
88
|
+
- Reservations run under the in-process sidecar lock, and `remove` deletes the sidecar last, so a new upload
|
|
89
|
+
of a removed id never has its bytes deleted by the old removal. Each reservation gets a
|
|
90
|
+
`generation` (`describeArtifact`), new every time the id is reserved.
|
|
91
|
+
- A conversion is installed in one patch under the sidecar lock: the runner converts into a temporary
|
|
92
|
+
`.mp4` (`ensureWebReady(…, { install: false })`), then `patchArtifact` with `file` renames it
|
|
93
|
+
onto the stored file, switches `ext`, records the result and deletes the original — only for
|
|
94
|
+
the reservation the conversion began on (`generation`) and only while no other pass has
|
|
95
|
+
recorded one (`unlessConverted`). When a condition doesn't hold, `patchArtifact` resolves
|
|
96
|
+
`false`, changing nothing, and the runner drops its file.
|
|
97
|
+
- Completion work belongs to one reservation: a queued conversion checks, when its turn comes,
|
|
98
|
+
that the id still names the upload it was queued for (and that it's finished); the
|
|
99
|
+
acknowledgement is recorded only on that reservation; and a new upload of a removed id is never
|
|
100
|
+
joined to the old one's work in progress.
|
|
101
|
+
- `GET /artifacts/:id` opens the file before sending headers, checks it's the file `send`
|
|
102
|
+
described (inode, size, mtime), and resolves once more when it's gone or another file was
|
|
103
|
+
renamed onto the path (a conversion just replaced it), instead of a 404, a cut-off response or
|
|
104
|
+
headers that don't match the bytes; bytes this
|
|
105
|
+
instance resolved under an extension storage no longer names are served with `max-age=0`.
|
|
106
|
+
- A failed run's recorded reason is `ffmpeg exited with code N`, never ffmpeg's output (which
|
|
107
|
+
names server paths); an HDR video on an ffmpeg without tone mapping is left as uploaded
|
|
108
|
+
(`skipped`, saying what's missing) rather than converted with wrong colours.
|
|
109
|
+
- The local adapter's `remove` decides from the sidecar alone, never its cache: an artifact
|
|
110
|
+
another reader had cached is reported removed once, not twice.
|
|
111
|
+
- `webReady` needs an adapter with `describeArtifact` too (both built-in adapters have it).
|
|
112
|
+
- The local adapter's `getLocalPath` reads the sidecar, not its cache, so it names the current
|
|
113
|
+
file after another instance removed the id and reserved it again.
|
|
114
|
+
- ffmpeg's capability queries (`-version`, `-h filter=scale`, `-filters`) are bounded (30 s).
|
|
115
|
+
- An `onArtifactEvent` observer that throws on `processed` is logged as its own failure; the
|
|
116
|
+
recorded result still reaches `completeAfter`'s hook as `ctx.webReady`.
|
|
117
|
+
- A replayed `onUploadComplete` (the hook threw, or the process stopped) gets the recorded
|
|
118
|
+
conversion result as `ctx.webReady` too, so a host naming the file from it (an `.mp4` once a
|
|
119
|
+
WebM was conformed) gets it right on the retry.
|
|
120
|
+
- `web-ready-migrate.mjs` rerun after an interruption deletes an original the sidecar no longer
|
|
121
|
+
names.
|
|
122
|
+
- `scripts/web-ready-migrate.mjs` conforms existing artifacts the same way, recording the result
|
|
123
|
+
on each sidecar and switching a changed container to its `.mp4`. Run it with the server
|
|
124
|
+
stopped.
|
|
125
|
+
- `transcode: false` records a file it leaves off-target (HEVC, oversized, non-AAC audio) as
|
|
126
|
+
`skipped`, not `none`: `none` now means only that the file already conforms. (Was `none`.)
|
|
127
|
+
- A browser WebM's missing duration is found by streaming ffprobe's packet list and keeping
|
|
128
|
+
only the latest end, never buffering it.
|
|
129
|
+
- An upload of deferred length that grows past `maxUploadSize` (a chunk with a length, or a
|
|
130
|
+
chunked body cut off mid-stream) gets the same `413 That file is larger than …` message.
|
|
131
|
+
- An MP4-family `.mov`/`.m4v` that already conformed used to be served as is (`video/quicktime`,
|
|
132
|
+
a type not every browser takes); it is now remuxed into an `.mp4`.
|
|
133
|
+
|
|
10
134
|
## [0.5.0] - 2026-10-05
|
|
11
135
|
|
|
12
136
|
Protocol 2.3 (`PROTOCOL.md` §7.4). What every host rebuilt around PulseVault
|
|
@@ -500,7 +624,9 @@ if you've evaluated against an intermediate build.
|
|
|
500
624
|
lock, so this is now at least surfaced: a one-time `console.warn` fires
|
|
501
625
|
per process the first time a deployment falls into this degraded mode.
|
|
502
626
|
|
|
503
|
-
[Unreleased]: https://github.com/mieweb/pulsevault/compare/v0.
|
|
627
|
+
[Unreleased]: https://github.com/mieweb/pulsevault/compare/v0.6.0...HEAD
|
|
628
|
+
[0.6.0]: https://github.com/mieweb/pulsevault/compare/v0.5.0...v0.6.0
|
|
629
|
+
[0.5.0]: https://github.com/mieweb/pulsevault/compare/v0.4.2...v0.5.0
|
|
504
630
|
[0.4.2]: https://github.com/mieweb/pulsevault/compare/v0.4.1...v0.4.2
|
|
505
631
|
[0.4.1]: https://github.com/mieweb/pulsevault/compare/v0.4.0...v0.4.1
|
|
506
632
|
[0.4.0]: https://github.com/mieweb/pulsevault/compare/v0.3.0...v0.4.0
|
package/OPERATIONS.md
CHANGED
|
@@ -54,7 +54,15 @@ Two ways to actually support multiple instances:
|
|
|
54
54
|
2. **Shared filesystem**: point every instance's `workspaceDir` at the same
|
|
55
55
|
NFS/EFS mount. Verify your mount's durability/consistency guarantees
|
|
56
56
|
under concurrent writes from multiple instances before relying on this in
|
|
57
|
-
production.
|
|
57
|
+
production. Each instance changes an artifact's metadata (`.pulsevault/
|
|
58
|
+
<id>.json`) one write at a time, but no lock spans instances: when two
|
|
59
|
+
write the same artifact's metadata at once, the last write wins. The
|
|
60
|
+
completion bookkeeping recovers: the replay redoes a lost acknowledgement
|
|
61
|
+
or conversion record, and a converted video's bytes are always at
|
|
62
|
+
`<id>.mp4`, which is served even when the metadata that won still names
|
|
63
|
+
the uploaded file. A host's `recordOutcome` lost that way is not redone. A
|
|
64
|
+
removal racing another instance's write to the same artifact can leave its
|
|
65
|
+
metadata behind; for strict guarantees across instances, use S3.
|
|
58
66
|
|
|
59
67
|
**The S3/R2 adapter (`createS3Storage`) has no such requirement** — every
|
|
60
68
|
instance talks to the same bucket, so it scales horizontally with zero
|
|
@@ -160,56 +168,124 @@ backup time will resume correctly via the normal TUS `HEAD`-then-resume path
|
|
|
160
168
|
once the client retries, or will sit as abandoned partial uploads until
|
|
161
169
|
`retention` removes them (see "Retention" below) if the client never retries.
|
|
162
170
|
|
|
163
|
-
## Web-ready playback (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
171
|
+
## Web-ready playback (conform to one format)
|
|
172
|
+
|
|
173
|
+
Videos reach PulseVault from the Pulse app, which already uploads the format
|
|
174
|
+
below, and from a host's own file picker: a screen recording, an iPhone HEVC
|
|
175
|
+
`.mov` (often 10-bit HDR), a WebM from a browser recorder, a 4K clip. Browsers
|
|
176
|
+
streaming over progressive HTTP stall on a `moov` atom at the end of the file,
|
|
177
|
+
Firefox never decodes HEVC, Chrome usually can't without hardware support,
|
|
178
|
+
10-bit HDR shown as SDR looks washed out, and a 4K file is far more than a
|
|
179
|
+
phone needs.
|
|
180
|
+
|
|
181
|
+
With the `webReady` option on, every finished video is **conformed** to one
|
|
182
|
+
target (`CONFORM_TARGET`, exported from the package root):
|
|
183
|
+
|
|
184
|
+
| Property | Target |
|
|
185
|
+
|---|---|
|
|
186
|
+
| Container | MP4, `moov` at the front (faststart) |
|
|
187
|
+
| Video | H.264, 8-bit `yuv420p` (limited range), even width and height |
|
|
188
|
+
| Colour | SDR: HDR (PQ or HLG) is tone-mapped to BT.709; an SDR source keeps its own colour tags (BT.709 or BT.601), which browsers honour |
|
|
189
|
+
| Size | longest edge at most 1920 (`maxEdge`); aspect ratio and orientation kept — no crop, pad or stretch |
|
|
190
|
+
| Rotation | applied to the pixels, so every player shows it the way it was held |
|
|
191
|
+
| Audio | AAC; a video with no audio stays silent |
|
|
192
|
+
|
|
193
|
+
`ensureWebReady` does the least work that gets a file there:
|
|
194
|
+
|
|
195
|
+
| The upload | What happens | `action` |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| Already in the target (a Pulse upload) | nothing; bytes untouched | `none` |
|
|
198
|
+
| MP4 with the `moov` at the end | lossless `-c copy` faststart remux | `remuxed` |
|
|
199
|
+
| MP4 with an off-target stream | one ffmpeg run, in place | `transcoded` |
|
|
200
|
+
| Another container (`.mov`, `.m4v`, `.webm`, `.mkv`, `.3gp`, `.avi`) | one ffmpeg run into a new `<id>.mp4`: a stream copy when the streams already conform, otherwise only the off-target streams re-encoded | `conformed` |
|
|
201
|
+
| No ffmpeg, unreadable, failed or timed out | nothing; the original serves | `skipped` |
|
|
202
|
+
|
|
203
|
+
A re-encode scales by the longest edge, applies the rotation, tone-maps HDR
|
|
204
|
+
(PQ or HLG) to SDR, and converts non-AAC audio (Opus, PCM, …) to AAC. After a
|
|
205
|
+
container change the artifact's stored file, extension and served
|
|
206
|
+
`Content-Type` (`video/mp4`) follow the new file; the artifact id, and so every
|
|
207
|
+
URL a host stored, stay the same. The new file is written beside the original,
|
|
208
|
+
the artifact's sidecar switches to it, and only then is the original deleted:
|
|
209
|
+
a crash at any point leaves either the original serving (the conversion is
|
|
210
|
+
redone by the replay) or an unused original that is deleted with the
|
|
211
|
+
artifact.
|
|
212
|
+
|
|
213
|
+
While a video is being converted, its URL serves the original bytes with
|
|
214
|
+
`max-age=0` instead of the configured `cache`, so a browser that fetched it
|
|
215
|
+
early revalidates and gets the converted file (with a new ETag) afterwards.
|
|
216
|
+
|
|
217
|
+
A failed run is recorded as `conversion failed: ffmpeg exited with code N`;
|
|
218
|
+
ffmpeg's own output, which names server paths, goes only to the server log.
|
|
219
|
+
|
|
220
|
+
The result is recorded on the artifact: the status route reports it as
|
|
221
|
+
`webReady: { action, reason }` (a host can say "Your video is ready", or "We
|
|
222
|
+
couldn't convert this video, so it may not play in every browser"),
|
|
223
|
+
`onArtifactEvent` fires `processed` with it, and `completeAfter` hands it to
|
|
224
|
+
`onUploadComplete` as `ctx.webReady`.
|
|
225
|
+
|
|
226
|
+
**Prerequisite:** install ffmpeg (which includes ffprobe) on the serving host
|
|
227
|
+
— `apt install ffmpeg` (Debian/Ubuntu), `dnf install ffmpeg` (Fedora/EL + RPM
|
|
228
|
+
Fusion), or `brew install ffmpeg` (macOS). It is detected at first use; without
|
|
229
|
+
it every upload is still accepted and served exactly as uploaded, one warning
|
|
230
|
+
is logged, and `core.conformAvailable()` (also `fastify.pulseVaultCore`)
|
|
231
|
+
resolves `false` for a health check. HDR tone mapping uses the `scale` filter
|
|
232
|
+
on FFmpeg 8 and later, or `zscale` (libzimg, included in the Debian and Ubuntu
|
|
233
|
+
packages) on older builds. A build with neither leaves an HDR video as
|
|
234
|
+
uploaded and records `skipped` with what's missing: converting it without tone
|
|
235
|
+
mapping would give wrong colours.
|
|
191
236
|
|
|
192
237
|
```js
|
|
193
238
|
await app.register(pulseVault, {
|
|
194
239
|
storage: createLocalStorage({ workspaceDir: dataDir }),
|
|
240
|
+
validatePayload: createVideoValidator({ maxDurationSeconds: 600 }),
|
|
195
241
|
webReady: { concurrency: 1, completeAfter: true },
|
|
196
242
|
});
|
|
197
243
|
```
|
|
198
244
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
`
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
for
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
245
|
+
Settings:
|
|
246
|
+
|
|
247
|
+
- `concurrency` (default `1`): conversions run one at a time per process; the
|
|
248
|
+
rest wait in a queue. A conversion a restart interrupted is resumed by the
|
|
249
|
+
completion replay (`replayCompletions`): an artifact isn't marked converted
|
|
250
|
+
until its result is recorded.
|
|
251
|
+
- `timeoutSeconds` (default `60 + 10 ×` the video's duration counted in
|
|
252
|
+
1080p30 seconds — a second of 4K at 120 fps counts 16 — at most an hour,
|
|
253
|
+
and an hour when the duration is unknown; the default comes from the
|
|
254
|
+
upload's own metadata, so set this for longer runs): a run past it is
|
|
255
|
+
killed, the original kept, and
|
|
256
|
+
`skipped` recorded with the reason.
|
|
257
|
+
- `probeTimeoutSeconds` (default `60`): the same guard for each ffprobe run
|
|
258
|
+
over the upload, so a malformed file can't hold the queue.
|
|
259
|
+
- `maxEdge` (default `1920`): the longest edge of the served video.
|
|
260
|
+
- `transcode: false`: never re-encode; only lossless remuxes run (a `moov`
|
|
261
|
+
moved to the front, or another container whose streams already conform copied
|
|
262
|
+
into an `.mp4`), so no CPU cost beyond a file rewrite. A file that would need
|
|
263
|
+
a re-encode is left as uploaded and recorded as `skipped` (`none` means only
|
|
264
|
+
"already conforms").
|
|
265
|
+
- `crf`/`preset` (defaults `23`/`veryfast`) tune the H.264 encode;
|
|
266
|
+
`ffmpegPath`/`ffprobePath` point at binaries off `PATH`.
|
|
267
|
+
|
|
268
|
+
**Accepted containers.** The default video `allowedExtensions` are `.mp4`,
|
|
269
|
+
`.mov`, `.m4v`, `.webm`, `.mkv`, `.3gp` and `.avi` (`CONFORM_VIDEO_EXTENSIONS`);
|
|
270
|
+
a host's own `allowedExtensions` still overrides them. The extension only
|
|
271
|
+
decides what's accepted at `create`; `createVideoValidator()` checks the
|
|
272
|
+
received bytes by what they are: ffprobe must find a video stream with a
|
|
273
|
+
duration above zero that isn't a picture (a PNG, JPEG, GIF, HEIC photo or
|
|
274
|
+
one-frame clip is refused), and, with `maxDurationSeconds`, not longer, or the
|
|
275
|
+
upload is refused with `422 That file isn't a video.` / `That video is longer
|
|
276
|
+
than the limit of 10 minutes.` Each ffprobe run is bounded by
|
|
277
|
+
`probeTimeoutSeconds` (default `60`), so a malformed file can't hold the final
|
|
278
|
+
`PATCH` open: past it the upload is refused with `422 That video couldn't be
|
|
279
|
+
checked in time.` Without ffprobe it falls back to sniffing the
|
|
280
|
+
container's first bytes (`ftyp`, EBML or RIFF AVI). An upload over
|
|
281
|
+
`maxUploadSize` is refused at `create` with `413 That file is larger than
|
|
282
|
+
500 MB.` Without `webReady`, a WebM or MKV is served as uploaded, with its own
|
|
283
|
+
content type.
|
|
284
|
+
|
|
285
|
+
**Existing artifacts** — uploads that landed before `webReady` was on are
|
|
286
|
+
conformed once with the bundled migration script. Stop the server while it
|
|
287
|
+
runs (the server caches each artifact's extension, which a container change
|
|
288
|
+
rewrites). It is idempotent; interrupt and rerun freely:
|
|
213
289
|
|
|
214
290
|
```sh
|
|
215
291
|
node node_modules/@mieweb/pulsevault/scripts/web-ready-migrate.mjs /path/to/workspaceDir
|
|
@@ -219,11 +295,12 @@ node node_modules/@mieweb/pulsevault/scripts/web-ready-migrate.mjs /path/to/work
|
|
|
219
295
|
node node_modules/@mieweb/pulsevault/scripts/web-ready-migrate.mjs /path/to/workspaceDir --no-transcode
|
|
220
296
|
```
|
|
221
297
|
|
|
222
|
-
A
|
|
223
|
-
|
|
298
|
+
A re-encode is one-time, quality-preserving at the default CRF but not
|
|
299
|
+
bit-identical, and a conformed file replaces the upload (the original is not
|
|
300
|
+
kept). Take a backup first if that matters to
|
|
224
301
|
your deployment (see "Backup and restore" above).
|
|
225
302
|
|
|
226
|
-
Note on checksums: a
|
|
303
|
+
Note on checksums: a conversion changes the artifact's bytes, so an
|
|
227
304
|
upload-time checksum recorded for it (the sidecar `checksum` from
|
|
228
305
|
`Upload-Metadata`) describes the original upload, not the rewritten file.
|
|
229
306
|
Treat `getChecksum` as upload-time provenance rather than current-file
|
package/PROTOCOL.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<!-- BEGIN GENERATED: protocol-version (from package.json pulseProtocol — edit that, then run `npm run protocol`) -->
|
|
4
4
|
| | |
|
|
5
5
|
|---|---|
|
|
6
|
-
| Spec revision | `2.
|
|
6
|
+
| Spec revision | `2.5` |
|
|
7
7
|
| Protocol majors accepted | 2 |
|
|
8
8
|
<!-- END GENERATED: protocol-version -->
|
|
9
9
|
|
|
@@ -69,12 +69,30 @@ the integer from `protocolVersion`.
|
|
|
69
69
|
|
|
70
70
|
## 3. Pairing (deep link)
|
|
71
71
|
|
|
72
|
-
A server presents a pairing link or QR code of
|
|
72
|
+
A server presents a pairing link or QR code in one of two equivalent forms
|
|
73
|
+
(since 2.4), carrying the same parameters:
|
|
73
74
|
|
|
74
75
|
```
|
|
75
76
|
pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>
|
|
77
|
+
https://mieweb.github.io/pulse/open#v=1&artifactId=<uuid>&server=<origin>&token=<opaque>
|
|
76
78
|
```
|
|
77
79
|
|
|
80
|
+
- **`pulsecam://`**: the parameters are the query string. Every Pulse release
|
|
81
|
+
opens it, but phone cameras and browsers don't open custom schemes reliably.
|
|
82
|
+
- **`https://`**: a Universal Link (iOS) / App Link (Android) on the link host
|
|
83
|
+
the Pulse app claims. When Pulse is installed the OS opens it straight from
|
|
84
|
+
the link; when it isn't, the link opens a static page that sends the person
|
|
85
|
+
to the store and offers a `pulsecam://` fallback. The parameters are the
|
|
86
|
+
**fragment**, which is never sent in an HTTP request, so the link host never
|
|
87
|
+
receives the token. The link host is fixed (an app can only claim domains it
|
|
88
|
+
was built with); `server` still points at the deployment's own server, so
|
|
89
|
+
uploads never touch the link host.
|
|
90
|
+
|
|
91
|
+
A client that accepts the `https://` form MUST treat it exactly like the
|
|
92
|
+
`pulsecam://` form with the same parameters. A server that wants every Pulse
|
|
93
|
+
release to open its links directly keeps the `pulsecam://` form until the
|
|
94
|
+
releases that claim the link host are installed.
|
|
95
|
+
|
|
78
96
|
- `v` (REQUIRED): the deep-link schema version. A client MUST refuse and
|
|
79
97
|
explain (not silently misparse) a `v` it doesn't recognize.
|
|
80
98
|
- `artifactId` (REQUIRED): a UUID minted by the server. The client uses this
|
|
@@ -95,7 +113,7 @@ pulsecam://?v=1&artifactId=<uuid>&server=<origin>&token=<opaque>
|
|
|
95
113
|
credential — see §5.4 for the recommended (but not required) capability-
|
|
96
114
|
token shape.
|
|
97
115
|
|
|
98
|
-
A client SHOULD ignore
|
|
116
|
+
A client SHOULD ignore parameters it does not recognize, so links minted by
|
|
99
117
|
an older or newer server stay valid. The parameters are defined by
|
|
100
118
|
[`protocol/schemas/deep-link.schema.json`](protocol/schemas/deep-link.schema.json).
|
|
101
119
|
|
|
@@ -334,7 +352,9 @@ body is defined by
|
|
|
334
352
|
`state` is `unknown`, `uploading`, `processing` (the server is converting the
|
|
335
353
|
bytes for the web) or `ready`, with the bytes received against the declared
|
|
336
354
|
length while uploading, and whatever outcome the host recorded once finished.
|
|
337
|
-
|
|
355
|
+
A server that converts videos for the web also reports what the conversion did
|
|
356
|
+
(`webReady`, protocol 2.5): nothing, a remux, a transcode, a new container, or
|
|
357
|
+
why it was skipped (the original then serves). The response MUST NOT be cached.
|
|
338
358
|
|
|
339
359
|
A server MAY also expose `GET {prefix}/artifacts/<videoId>/poster`: the finished
|
|
340
360
|
thumbnail `relatedTo` the video (§8), served exactly as §6.2 serves an artifact
|
|
@@ -410,7 +430,7 @@ a server it paired with earlier: a server can be upgraded in between.
|
|
|
410
430
|
| [`beat-manifest.schema.json`](protocol/schemas/beat-manifest.schema.json) | Beat manifest |
|
|
411
431
|
| [`capabilities.schema.json`](protocol/schemas/capabilities.schema.json) | GET /capabilities response |
|
|
412
432
|
| [`capability-token.schema.json`](protocol/schemas/capability-token.schema.json) | Capability token claims |
|
|
413
|
-
| [`deep-link.schema.json`](protocol/schemas/deep-link.schema.json) | Pairing link
|
|
433
|
+
| [`deep-link.schema.json`](protocol/schemas/deep-link.schema.json) | Pairing link parameters |
|
|
414
434
|
| [`pulse-client.schema.json`](protocol/schemas/pulse-client.schema.json) | Pulse-Client request header |
|
|
415
435
|
| [`upload-metadata.schema.json`](protocol/schemas/upload-metadata.schema.json) | TUS Upload-Metadata keys |
|
|
416
436
|
| [`view-link.schema.json`](protocol/schemas/view-link.schema.json) | POST /artifacts/<id>/view-link response |
|
|
@@ -425,6 +445,8 @@ a server it paired with earlier: a server can be upgraded in between.
|
|
|
425
445
|
| 2.1 | Added `protocolRevision` to `/capabilities`, the `Pulse-Client` header with `426 Upgrade Required` (§7.2), and `Upload-Metadata.appVersion` (§4.1). |
|
|
426
446
|
| 2.2 | Added read-only view links: `viewLinks` in `/capabilities`, `POST {prefix}/artifacts/<id>/view-link` (§6.4), and the view token's `use` claim (§5.5). |
|
|
427
447
|
| 2.3 | Added `GET {prefix}/artifacts/<id>/status` and `GET {prefix}/artifacts/<id>/poster` (§6.5), the capability token's `ctx` claim (§5.4), and the pulse-shape rule for creates (§5.4). |
|
|
448
|
+
| 2.4 | Added the `https://mieweb.github.io/pulse/open#…` pairing link form (§3), with the parameters in the fragment. |
|
|
449
|
+
| 2.5 | Added `webReady` to the status response (§6.5): what the server's conversion did. |
|
|
428
450
|
|
|
429
451
|
## 8. Artifact relationships (`relatedTo`)
|
|
430
452
|
|
package/README.md
CHANGED
|
@@ -269,7 +269,7 @@ type PulseVaultPluginOptions = {
|
|
|
269
269
|
lockWhenReady?: boolean; // default false: a finished pulse can't be deleted through the routes
|
|
270
270
|
webReady?: PulseVaultWebReadyOptions | boolean; // default off: background faststart remux / H.264 transcode
|
|
271
271
|
replayCompletions?: { intervalSeconds?: number } | false; // default every 300 s: re-fire unacknowledged completions
|
|
272
|
-
coreDecoratorName?: string; // default "pulseVaultCore": getStatus, getPulse, recordOutcome, replayCompletions
|
|
272
|
+
coreDecoratorName?: string; // default "pulseVaultCore": getStatus, getPulse, recordOutcome, replayCompletions, conformAvailable
|
|
273
273
|
/** @deprecated use validatePayload + ctx.kind === "project" */
|
|
274
274
|
validateProjectPayload?: PulseVaultValidatePayload;
|
|
275
275
|
/** @deprecated use onUploadComplete + ctx.kind === "project" */
|
|
@@ -306,8 +306,9 @@ import "@mieweb/pulsevault/augment";
|
|
|
306
306
|
File extensions accepted per artifact kind. Three accepted forms:
|
|
307
307
|
|
|
308
308
|
```ts
|
|
309
|
-
// 1. Omit entirely — uses
|
|
310
|
-
// video: [".mp4"
|
|
309
|
+
// 1. Omit entirely — uses the defaults:
|
|
310
|
+
// video: [".mp4", ".mov", ".m4v", ".webm", ".mkv", ".3gp", ".avi"] project: [".pulse", ".zip"]
|
|
311
|
+
// captions: [".vtt"] thumbnail: [".jpg", ".jpeg", ".png"]
|
|
311
312
|
|
|
312
313
|
// 2. Flat array (legacy) — video-only; project/captions keep their defaults:
|
|
313
314
|
allowedExtensions: [".mp4"]
|
|
@@ -316,7 +317,7 @@ allowedExtensions: [".mp4"]
|
|
|
316
317
|
allowedExtensions: { video: [".mp4"], project: [".pulse"], captions: [".vtt"] }
|
|
317
318
|
```
|
|
318
319
|
|
|
319
|
-
All extensions must include the leading dot and are matched case-insensitively. The `kind` field in `Upload-Metadata` determines which list is checked.
|
|
320
|
+
All extensions must include the leading dot and are matched case-insensitively. The `kind` field in `Upload-Metadata` determines which list is checked. The default video list is the containers [`webReady`](#webready) conforms to MP4 (`CONFORM_VIDEO_EXTENSIONS`); the extension only decides what's accepted at `create`, so pair it with `createVideoValidator()` to check the bytes themselves.
|
|
320
321
|
|
|
321
322
|
### `cache`
|
|
322
323
|
|
|
@@ -416,6 +417,8 @@ await app.register(pulseVault, {
|
|
|
416
417
|
});
|
|
417
418
|
```
|
|
418
419
|
|
|
420
|
+
For videos from anywhere (a WebM from a browser recorder, an MKV, an AVI), check them by what they are with **`createVideoValidator({ maxDurationSeconds?, probeTimeoutSeconds? })`**: ffprobe must find a video stream with a duration above zero that isn't a picture (PNG, JPEG, GIF, HEIC, a one-frame clip), or the upload is refused with `422` and a message a host can show as it is (`That file isn't a video.`, `That video is longer than the limit of 10 minutes.`, or `That video couldn't be checked in time.` when ffprobe runs past `probeTimeoutSeconds`, default 60). It checks `kind: "video"` only and lets other kinds through; without ffprobe it falls back to `sniffVideo(path)` (the container's first bytes: `ftyp`, EBML or RIFF AVI) and logs once. Local storage only.
|
|
421
|
+
|
|
419
422
|
`createMp4Sniffer` reads the first 12 bytes and verifies the ISOBMFF `ftyp` header (MP4, MOV, M4V, 3GP). Uploads that pass the extension check but contain non-video bytes are rejected with 422 and the disk is cleaned up. The lower-level `sniffMp4(path)` is also exported if you want to drive your own validator.
|
|
420
423
|
|
|
421
424
|
`createChecksumValidator`/`createS3ChecksumValidator` parse `ctx.checksum` with the also-exported `parseChecksumMetadata(raw)`, which returns `{ algorithm, digest }` or `null` for a missing/malformed value — use it directly if you're writing your own checksum check instead of the ship-ready validators.
|
|
@@ -542,11 +545,14 @@ Off by default. `true`, or `ensureWebReady`'s options plus:
|
|
|
542
545
|
webReady: {
|
|
543
546
|
concurrency: 1, // conversions at once; a transcode is CPU-bound
|
|
544
547
|
completeAfter: false, // true: run onUploadComplete only once the conversion has finished
|
|
548
|
+
maxEdge: 1920, // longest edge of the served video
|
|
549
|
+
timeoutSeconds: undefined, // default 60 + 10 × duration in 1080p30 seconds (4K120 counts 16×), at most 1 h: a hang guard
|
|
550
|
+
probeTimeoutSeconds: 60, // the same guard for each ffprobe run over the upload
|
|
545
551
|
transcode: true, crf: 23, preset: "veryfast", ffmpegPath: "ffmpeg", ffprobePath: "ffprobe",
|
|
546
552
|
},
|
|
547
553
|
```
|
|
548
554
|
|
|
549
|
-
Every finished video is
|
|
555
|
+
Every finished video is conformed in the background, after the final `PATCH` is answered, to the one format the Pulse app records (`CONFORM_TARGET`: faststart MP4, H.264 8-bit SDR, longest edge at most `maxEdge`, rotation applied, AAC or silent; orientation kept). A file already in it — every Pulse upload — keeps its bytes (`none`); a `moov` at the end is a lossless remux (`remuxed`); an off-target stream is re-encoded (`transcoded`: HEVC, 10-bit HDR tone-mapped to SDR, 4K scaled, Opus/PCM to AAC); another container (`.mov`, `.webm`, `.mkv`, …) becomes a new `.mp4` served as `video/mp4` at the same artifact URL (`conformed`). See `OPERATIONS.md` for the table. While it runs the artifact's status is `processing` (the original bytes serve meanwhile, revalidated on every request instead of the configured `cache`; every rewrite is atomic), and when it's done the status reports `webReady: { action, reason }` — `skipped` with the reason when ffmpeg is missing, a run failed or timed out, and the original keeps serving — and `onArtifactEvent` fires `processed` with the same result. With `completeAfter: true`, `onUploadComplete` waits for the conversion and receives `ctx.webReady`, so a host that publishes from the hook never publishes a video that's still being rewritten. A conversion a restart interrupted is resumed by the replay below. Needs the local storage adapter and ffmpeg on the host; without ffmpeg it logs once and serves the original bytes.
|
|
550
556
|
|
|
551
557
|
### `replayCompletions`
|
|
552
558
|
|
|
@@ -560,12 +566,15 @@ The core behind the routes is `fastify.pulseVaultCore` (rename it with `coreDeco
|
|
|
560
566
|
// Where an upload is — what GET /artifacts/:id/status reports — for a host that polls server-side.
|
|
561
567
|
await app.pulseVaultCore.getStatus(videoId);
|
|
562
568
|
// → { artifactId, state: "unknown" | "uploading" | "processing" | "ready", kind, relatedTo?, name?,
|
|
563
|
-
// bytesReceived?, size?, acknowledged, outcome?, context? }
|
|
569
|
+
// bytesReceived?, size?, acknowledged, webReady?: { action, reason }, outcome?, context? }
|
|
564
570
|
// `context` (the token's) is for the host — an owner check is a comparison on it; the route leaves it out.
|
|
565
571
|
|
|
566
572
|
// Where it went, or why it didn't: any JSON, reported as `outcome` by the status route. `null` clears it.
|
|
567
573
|
await app.pulseVaultCore.recordOutcome(videoId, { state: "done", note: "Posted to Huddle" });
|
|
568
574
|
|
|
575
|
+
// Whether videos are conformed here (`webReady` on, ffmpeg and ffprobe installed) — for a health check.
|
|
576
|
+
await app.pulseVaultCore.conformAvailable();
|
|
577
|
+
|
|
569
578
|
// A video and the finished files relatedTo it, by kind — from the adapters' relation index, never a scan.
|
|
570
579
|
const { video, thumbnail, captions, manifest } = await app.pulseVaultCore.getPulse(videoId);
|
|
571
580
|
// One per kind; when a file was re-sent, the one that finished last (`readyAt`) wins.
|
|
@@ -598,7 +607,7 @@ When the final PATCH lands the plugin runs the following steps in order, for eve
|
|
|
598
607
|
"minSupportedVersion": 2,
|
|
599
608
|
"maxSupportedVersion": 2,
|
|
600
609
|
"kinds": ["video", "project", "captions", "thumbnail"],
|
|
601
|
-
"allowedExtensions": { "video": [".mp4"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
|
|
610
|
+
"allowedExtensions": { "video": [".mp4", ".mov", ".m4v", ".webm", ".mkv", ".3gp", ".avi"], "project": [".pulse", ".zip"], "captions": [".vtt"], "thumbnail": [".jpg", ".jpeg", ".png"] },
|
|
602
611
|
"maxUploadSize": 5368709120,
|
|
603
612
|
"checksum": { "algorithms": ["sha256", "sha1", "md5"] }
|
|
604
613
|
}
|
|
@@ -937,7 +946,7 @@ const storage: PulseVaultStorage = {
|
|
|
937
946
|
|
|
938
947
|
## Deep link helper
|
|
939
948
|
|
|
940
|
-
Use this to generate a
|
|
949
|
+
Use this to generate a pairing link for the Pulse mobile app and your server. Typically encoded as a QR code on a pairing page.
|
|
941
950
|
|
|
942
951
|
```ts
|
|
943
952
|
import { buildUploadLink } from "@mieweb/pulsevault";
|
|
@@ -952,8 +961,19 @@ const uploadLink = buildUploadLink({
|
|
|
952
961
|
token: "secret", // optional — forwarded to your authorize hook; see Capability tokens
|
|
953
962
|
});
|
|
954
963
|
// pulsecam://?v=1&artifactId=...&server=https%3A%2F%2Fexample.com%2Fpulsevault&token=secret
|
|
964
|
+
|
|
965
|
+
// The https form, a Universal Link / App Link (PROTOCOL.md §3):
|
|
966
|
+
const httpsLink = buildUploadLink({
|
|
967
|
+
server: "https://example.com/pulsevault",
|
|
968
|
+
artifactId: randomUUID(),
|
|
969
|
+
token: "secret",
|
|
970
|
+
form: "https",
|
|
971
|
+
});
|
|
972
|
+
// https://mieweb.github.io/pulse/open#v=1&artifactId=...&server=https%3A%2F%2Fexample.com%2Fpulsevault&token=secret
|
|
955
973
|
```
|
|
956
974
|
|
|
975
|
+
The `https` form is what a phone camera opens: Pulse opens straight from it when installed, and its page sends the person to the store when not. The parameters are after `#`, which browsers never send, so the link host never sees the token. Pulse releases from before the link host was claimed land on the page instead and open Pulse from its button, so `pulsecam://` stays the default.
|
|
976
|
+
|
|
957
977
|
## Pulse logo and store badges
|
|
958
978
|
|
|
959
979
|
The package ships the Pulse logo for the button that opens Pulse
|
package/dist/app.d.ts
CHANGED
|
@@ -157,7 +157,7 @@ export type PulseVaultPluginOptions = {
|
|
|
157
157
|
replayCompletions?: PulseVaultReplayOptions | false;
|
|
158
158
|
/**
|
|
159
159
|
* Name of the decorator that exposes the core (`fastify.<name>.getStatus`, `.getPulse`,
|
|
160
|
-
* `.recordOutcome`, `.replayCompletions`). Defaults to `"pulseVaultCore"`.
|
|
160
|
+
* `.recordOutcome`, `.replayCompletions`, `.conformAvailable`). Defaults to `"pulseVaultCore"`.
|
|
161
161
|
*/
|
|
162
162
|
coreDecoratorName?: string;
|
|
163
163
|
/**
|
|
@@ -180,17 +180,18 @@ export type PulseVaultPluginOptions = {
|
|
|
180
180
|
declare const _default: FastifyPluginAsync<PulseVaultPluginOptions>;
|
|
181
181
|
export default _default;
|
|
182
182
|
export { createLocalStorage } from './storage/local.js';
|
|
183
|
+
export { uploadIdOf } from './storage/types.js';
|
|
183
184
|
export type { LocalStorage, LocalStorageOptions } from './storage/local.js';
|
|
184
185
|
export { createS3Storage } from './storage/s3.js';
|
|
185
186
|
export type { S3Storage, S3StorageOptions } from './storage/s3.js';
|
|
186
187
|
export type { PulseVaultArtifactRecord, PulseVaultResolution, PulseVaultStorage, ReserveUploadParams, UploadKind, } from './storage/types.js';
|
|
187
188
|
export type { PulseVaultAuthorize, PulseVaultAuthorizeContext, PulseVaultAuthorizePhase, } from './routes/pulsevault.js';
|
|
188
189
|
export type { PulseVaultOnUploadComplete, PulseVaultOnArtifactEvent, PulseVaultArtifactEvent, } from './lib/pulsevaultTus.js';
|
|
189
|
-
export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer } from './lib/magic.js';
|
|
190
|
-
export type { PulseVaultValidatePayload } from './lib/magic.js';
|
|
191
|
-
export { ensureWebReady, scanMoovPosition } from './lib/web-ready.js';
|
|
192
|
-
export type { WebReadyAction, WebReadyOptions, WebReadyResult, MoovPosition } from './lib/web-ready.js';
|
|
193
|
-
export { buildUploadLink } from './lib/deeplinks.js';
|
|
190
|
+
export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer, sniffVideo, createVideoValidator } from './lib/magic.js';
|
|
191
|
+
export type { PulseVaultValidatePayload, VideoValidatorOptions } from './lib/magic.js';
|
|
192
|
+
export { ensureWebReady, scanMoovPosition, probeVideo, webReadyAvailable, CONFORM_TARGET, CONFORM_VIDEO_EXTENSIONS, } from './lib/web-ready.js';
|
|
193
|
+
export type { WebReadyAction, WebReadyOptions, WebReadyResult, MoovPosition, VideoProbe } from './lib/web-ready.js';
|
|
194
|
+
export { buildUploadLink, PULSE_OPEN_URL } from './lib/deeplinks.js';
|
|
194
195
|
export type { UploadLinkOptions } from './lib/deeplinks.js';
|
|
195
196
|
export { issueCapabilityToken, verifyCapabilityToken, issueViewToken, verifyViewToken, createCapabilityAuthorize, } from './lib/capability-token.js';
|
|
196
197
|
export type { CapabilityTokenClaims, IssueCapabilityTokenOptions, IssueViewTokenOptions, VerifyCapabilityTokenOptions, LookupSecret, } from './lib/capability-token.js';
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
package/dist/app.js
CHANGED
|
@@ -64,10 +64,11 @@ export default fp(app, {
|
|
|
64
64
|
fastify: '5.x',
|
|
65
65
|
});
|
|
66
66
|
export { createLocalStorage } from './storage/local.js';
|
|
67
|
+
export { uploadIdOf } from './storage/types.js';
|
|
67
68
|
export { createS3Storage } from './storage/s3.js';
|
|
68
|
-
export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer } from './lib/magic.js';
|
|
69
|
-
export { ensureWebReady, scanMoovPosition } from './lib/web-ready.js';
|
|
70
|
-
export { buildUploadLink } from './lib/deeplinks.js';
|
|
69
|
+
export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer, sniffVideo, createVideoValidator } from './lib/magic.js';
|
|
70
|
+
export { ensureWebReady, scanMoovPosition, probeVideo, webReadyAvailable, CONFORM_TARGET, CONFORM_VIDEO_EXTENSIONS, } from './lib/web-ready.js';
|
|
71
|
+
export { buildUploadLink, PULSE_OPEN_URL } from './lib/deeplinks.js';
|
|
71
72
|
export { issueCapabilityToken, verifyCapabilityToken, issueViewToken, verifyViewToken, createCapabilityAuthorize, } from './lib/capability-token.js';
|
|
72
73
|
export { createViewLinkIssuer } from './lib/view-links.js';
|
|
73
74
|
export { sweepAbandonedUploads } from './lib/retention.js';
|