@mieweb/pulsevault 0.4.2 → 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.
Files changed (68) hide show
  1. package/CHANGELOG.md +216 -1
  2. package/OPERATIONS.md +135 -44
  3. package/PROTOCOL.md +63 -4
  4. package/README.md +186 -34
  5. package/dist/app.d.ts +30 -5
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js +24 -5
  8. package/dist/app.js.map +1 -1
  9. package/dist/augment.d.ts +3 -0
  10. package/dist/augment.d.ts.map +1 -1
  11. package/dist/augment.js.map +1 -1
  12. package/dist/core.d.ts +107 -8
  13. package/dist/core.d.ts.map +1 -1
  14. package/dist/core.js +381 -47
  15. package/dist/core.js.map +1 -1
  16. package/dist/lib/authorize.d.ts +28 -6
  17. package/dist/lib/authorize.d.ts.map +1 -1
  18. package/dist/lib/capability-token.d.ts +26 -0
  19. package/dist/lib/capability-token.d.ts.map +1 -1
  20. package/dist/lib/capability-token.js +73 -5
  21. package/dist/lib/capability-token.js.map +1 -1
  22. package/dist/lib/completion.d.ts +138 -0
  23. package/dist/lib/completion.d.ts.map +1 -0
  24. package/dist/lib/completion.js +375 -0
  25. package/dist/lib/completion.js.map +1 -0
  26. package/dist/lib/deeplinks.d.ts +18 -2
  27. package/dist/lib/deeplinks.d.ts.map +1 -1
  28. package/dist/lib/deeplinks.js +13 -2
  29. package/dist/lib/deeplinks.js.map +1 -1
  30. package/dist/lib/magic.d.ts +41 -1
  31. package/dist/lib/magic.d.ts.map +1 -1
  32. package/dist/lib/magic.js +104 -0
  33. package/dist/lib/magic.js.map +1 -1
  34. package/dist/lib/options.d.ts +2 -0
  35. package/dist/lib/options.d.ts.map +1 -1
  36. package/dist/lib/options.js +8 -2
  37. package/dist/lib/options.js.map +1 -1
  38. package/dist/lib/protocol-version.gen.js +1 -1
  39. package/dist/lib/pulsevaultTus.d.ts +28 -26
  40. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  41. package/dist/lib/pulsevaultTus.js +149 -32
  42. package/dist/lib/pulsevaultTus.js.map +1 -1
  43. package/dist/lib/web-ready.d.ts +138 -18
  44. package/dist/lib/web-ready.d.ts.map +1 -1
  45. package/dist/lib/web-ready.js +453 -91
  46. package/dist/lib/web-ready.js.map +1 -1
  47. package/dist/routes/pulsevault.d.ts +12 -1
  48. package/dist/routes/pulsevault.d.ts.map +1 -1
  49. package/dist/routes/pulsevault.js +92 -1
  50. package/dist/routes/pulsevault.js.map +1 -1
  51. package/dist/storage/local.d.ts +7 -1
  52. package/dist/storage/local.d.ts.map +1 -1
  53. package/dist/storage/local.js +408 -52
  54. package/dist/storage/local.js.map +1 -1
  55. package/dist/storage/s3.d.ts +7 -1
  56. package/dist/storage/s3.d.ts.map +1 -1
  57. package/dist/storage/s3.js +256 -14
  58. package/dist/storage/s3.js.map +1 -1
  59. package/dist/storage/types.d.ts +126 -0
  60. package/dist/storage/types.d.ts.map +1 -1
  61. package/dist/storage/types.js +7 -0
  62. package/dist/storage/types.js.map +1 -1
  63. package/package.json +2 -2
  64. package/protocol/openapi.json +270 -1
  65. package/protocol/schemas/artifact-status.schema.json +61 -0
  66. package/protocol/schemas/capability-token.schema.json +3 -0
  67. package/protocol/schemas/deep-link.schema.json +2 -2
  68. package/scripts/web-ready-migrate.mjs +35 -11
@@ -1,8 +1,52 @@
1
1
  import type { PulseVaultLogger } from './request.js';
2
- export type WebReadyAction = 'none' | 'remuxed' | 'transcoded' | 'skipped';
2
+ /**
3
+ * Conform uploaded videos to one web-playable format: the one the Pulse app records.
4
+ *
5
+ * A video reaches PulseVault from the Pulse app (already in the target format) or from a host's
6
+ * own file picker: a screen recording, an iPhone HEVC `.mov` (often 10-bit HDR), a WebM from a
7
+ * browser recorder, a 4K clip. Browsers streaming over progressive HTTP need the `moov` atom at
8
+ * the front, a codec they all decode, and 8-bit SDR colour; a phone needs a sane size.
9
+ *
10
+ * `ensureWebReady` does the least work that gets a file there: nothing when it already
11
+ * conforms (a Pulse upload keeps its bytes), a lossless remux when only the container or the
12
+ * `moov` position is off, and otherwise one ffmpeg run that re-encodes only the streams that
13
+ * are off-target. Fail-open by design: without `ffmpeg`/`ffprobe`, or when a run fails or
14
+ * times out, the original bytes keep serving and the result says why.
15
+ */
16
+ /**
17
+ * The format every video is served in. "Already conforms" checks exactly these properties —
18
+ * never the orientation: a landscape video stays landscape and a portrait one portrait, with no
19
+ * crop, pad or stretch.
20
+ */
21
+ export declare const CONFORM_TARGET: {
22
+ /** MP4 with the `moov` atom at the front (faststart). */
23
+ readonly container: "mp4";
24
+ readonly extension: ".mp4";
25
+ /**
26
+ * H.264, 8-bit 4:2:0 in limited range, even width and height, SDR, no rotation tag. HDR (PQ,
27
+ * HLG) is tone-mapped to BT.709; an SDR source keeps its own colour tags (BT.709 or BT.601),
28
+ * which browsers honour — so a Pulse upload from any phone keeps its bytes.
29
+ */
30
+ readonly videoCodec: "h264";
31
+ readonly pixelFormat: "yuv420p";
32
+ /** Default cap on the longest edge, in pixels (`webReady.maxEdge`). */
33
+ readonly maxEdge: 1920;
34
+ /** AAC; a video without audio stays silent. */
35
+ readonly audioCodec: "aac";
36
+ };
37
+ /** The containers the conform step turns into the target: the default video `allowedExtensions`. */
38
+ export declare const CONFORM_VIDEO_EXTENSIONS: readonly string[];
39
+ /**
40
+ * - `none`: already in the target format; the bytes are untouched.
41
+ * - `remuxed`: only the `moov` position was off; rewritten losslessly, same file.
42
+ * - `transcoded`: an MP4 whose video or audio was re-encoded, same file.
43
+ * - `conformed`: a different container (WebM, MKV, MOV, …) rewritten as a new `.mp4`.
44
+ * - `skipped`: nothing was done (no ffmpeg, a failed or timed-out run); the original serves.
45
+ */
46
+ export type WebReadyAction = 'none' | 'remuxed' | 'transcoded' | 'conformed' | 'skipped';
3
47
  export type WebReadyResult = {
4
48
  action: WebReadyAction;
5
- /** Human-readable explanation (what was detected, or why nothing was done). */
49
+ /** Human-readable explanation (what was detected and done, or why nothing was done). */
6
50
  reason: string;
7
51
  };
8
52
  export type WebReadyOptions = {
@@ -11,15 +55,34 @@ export type WebReadyOptions = {
11
55
  /** Path to the ffprobe binary. Default `"ffprobe"` (resolved via PATH). */
12
56
  ffprobePath?: string;
13
57
  /**
14
- * Whether a non-web-safe codec triggers a full H.264 transcode. Default
15
- * `true`. Set `false` to only ever do the lossless faststart remux —
16
- * useful when transcode cost on the serving host is a concern.
58
+ * Whether an off-target stream is re-encoded. Default `true`. Set `false` to never re-encode:
59
+ * only lossless remuxes still run (a `moov` moved to the front, or another container whose
60
+ * streams already conform copied into an `.mp4`) — useful when transcode cost on the serving
61
+ * host is a concern.
17
62
  */
18
63
  transcode?: boolean;
64
+ /** Longest edge of the served video, in pixels; larger videos are scaled down. Default `1920`. */
65
+ maxEdge?: number;
19
66
  /** x264 CRF for the transcode path (lower = better/larger). Default `23`. */
20
67
  crf?: number;
21
68
  /** x264 preset for the transcode path. Default `"veryfast"`. */
22
69
  preset?: string;
70
+ /**
71
+ * Wall-clock limit for one ffmpeg run, in seconds; the run is killed and the original kept.
72
+ * A guard against a run that hangs, not a speed limit. Default `60 + 10 ×` the video's
73
+ * duration counted in 1080p30 seconds (a second of 4K at 120 fps counts 16), at most an hour,
74
+ * and an hour when the duration is unknown. The default comes from the upload's own metadata,
75
+ * so it's capped: a file can't claim its way to a days-long run. Set this for longer runs.
76
+ */
77
+ timeoutSeconds?: number;
78
+ /**
79
+ * `false`: leave every rewrite at a temporary `.mp4` path beside the input (the result's
80
+ * `outputPath`) instead of renaming it into place, so the caller can install it under its own
81
+ * lock and conditions. Default `true`.
82
+ */
83
+ install?: boolean;
84
+ /** Wall-clock limit for each ffprobe run over the upload, in seconds. Default `60`. */
85
+ probeTimeoutSeconds?: number;
23
86
  /** Optional logger; `error` fires when ffmpeg/ffprobe are missing or a rewrite fails. */
24
87
  logger?: PulseVaultLogger;
25
88
  };
@@ -32,23 +95,78 @@ export type MoovPosition = 'front' | 'end' | 'unknown';
32
95
  * costs microseconds regardless of file size.
33
96
  *
34
97
  * Returns `"unknown"` for non-MP4 bytes, truncated headers, or files missing
35
- * either box — callers should treat that as "leave the file alone".
98
+ * either box.
36
99
  */
37
100
  export declare function scanMoovPosition(filePath: string): Promise<MoovPosition>;
101
+ /** What ffprobe says about a video: the streams the conform step maps, and its length. */
102
+ export type VideoProbe = {
103
+ video: {
104
+ index: number;
105
+ codec: string;
106
+ pixelFormat: string;
107
+ width: number;
108
+ height: number;
109
+ /** The colour transfer (`bt709`, `arib-std-b67`, …), when tagged. */
110
+ transfer?: string;
111
+ /** `pc` for full range, `tv` for limited, when tagged. */
112
+ range?: string;
113
+ /**
114
+ * Display rotation in degrees from the stream's matrix: `0`, `90`, `-90` or `180`, or another
115
+ * angle in (-180, 180] when the matrix isn't within a degree of a quarter turn (not converted).
116
+ */
117
+ rotation: number;
118
+ /** Average frames per second, when ffprobe can tell. */
119
+ frameRate?: number;
120
+ };
121
+ /** The first audio stream, or `null` for a silent video. */
122
+ audio: {
123
+ index: number;
124
+ codec: string;
125
+ } | null;
126
+ /** Seconds, or `null` when neither the header nor the packets say. */
127
+ durationSeconds: number | null;
128
+ };
129
+ export type ProbeOptions = {
130
+ /** Path to the ffprobe binary. Default `"ffprobe"` (resolved via PATH). */
131
+ ffprobePath?: string;
132
+ /** Wall-clock limit for each ffprobe run, in seconds; past it the probe fails. Default `60`. */
133
+ timeoutSeconds?: number;
134
+ };
135
+ /** An ffprobe run past its `timeoutSeconds`: the file couldn't be read in time. */
136
+ export declare class ProbeTimeoutError extends Error {
137
+ }
138
+ /**
139
+ * ffprobe a file: its first real video stream (not cover art), its first audio stream and its
140
+ * duration. `null` when it isn't a video: ffprobe can't read it, it has no video stream, or it
141
+ * is a picture (an image file, or a stream of one frame). Each ffprobe run is bounded by
142
+ * `timeoutSeconds`; past it, rejects with `ProbeTimeoutError`.
143
+ */
144
+ export declare function probeVideo(filePath: string, options?: ProbeOptions): Promise<VideoProbe | null>;
145
+ export declare function isBinaryAvailable(binPath: string): Promise<boolean>;
146
+ /** Whether this host can conform videos: both `ffmpeg` and `ffprobe` run. */
147
+ export declare function webReadyAvailable(options?: Pick<WebReadyOptions, 'ffmpegPath' | 'ffprobePath'>): Promise<boolean>;
148
+ /** Fail at once on an option that can't work, for `ensureWebReady` and the core's `webReady`. */
149
+ export declare function validateWebReadyOptions(options: WebReadyOptions): void;
38
150
  /**
39
- * Make the MP4 at `filePath` web-playable, in place:
151
+ * Bring the video at `filePath` to `CONFORM_TARGET`:
40
152
  *
41
- * - moov at the end + web-safe codec → lossless `-c copy -movflags +faststart`
42
- * remux (sub-second, no quality change);
43
- * - non-web-safe codec (HEVC etc.) → one-time `libx264` transcode (audio
44
- * stream-copied) with faststart;
45
- * - already faststart H.264, non-MP4 bytes, unprobeable files, or missing
46
- * ffmpeg/ffprobe → untouched (`skipped`/`none`).
153
+ * - already conforming → `none`, bytes untouched;
154
+ * - an MP4 with conforming streams but the `moov` at the end → lossless faststart remux in
155
+ * place (`remuxed`);
156
+ * - any other container with conforming streams → lossless remux into a new `.mp4` beside it
157
+ * (`conformed`);
158
+ * - otherwise one ffmpeg run: the video re-encoded to H.264 when its codec, pixel format, bit
159
+ * depth, size, rotation or dynamic range is off (scaled by the longest edge, rotation
160
+ * applied, HDR tone-mapped), the audio re-encoded to AAC unless it already is — in place for
161
+ * an MP4 (`transcoded`), into a new `.mp4` otherwise (`conformed`);
162
+ * - no ffmpeg/ffprobe, a file ffprobe can't read, a failed or timed-out run → `skipped` with
163
+ * the reason; the original is untouched.
47
164
  *
48
- * The rewrite is atomic (tmp file + rename in the same directory), so a crash
49
- * mid-way never corrupts the served artifact. Never throws for pipeline
50
- * reasons — a failed ffmpeg run resolves to `skipped` with the reason, and the
51
- * original bytes keep serving.
165
+ * When the result carries `outputPath`, the conformed file was written there (`<name>.mp4`)
166
+ * and the original is left in place: the caller switches the artifact over, then removes it.
167
+ * With `install: false` nothing is renamed: every rewrite is left at a temporary path beside
168
+ * the input, returned as `outputPath`, for the caller to install (or remove).
169
+ * Never throws for pipeline reasons.
52
170
  *
53
171
  * NOTE: a rewrite changes the artifact's bytes, so any upload-time checksum
54
172
  * recorded for it (e.g. the local adapter's sidecar `checksum` from
@@ -56,5 +174,7 @@ export declare function scanMoovPosition(filePath: string): Promise<MoovPosition
56
174
  * Consumers verifying bytes against `getChecksum` must treat it as
57
175
  * upload-time provenance, not current-file integrity.
58
176
  */
59
- export declare function ensureWebReady(filePath: string, options?: WebReadyOptions): Promise<WebReadyResult>;
177
+ export declare function ensureWebReady(filePath: string, options?: WebReadyOptions): Promise<WebReadyResult & {
178
+ outputPath?: string;
179
+ }>;
60
180
  //# sourceMappingURL=web-ready.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"web-ready.d.ts","sourceRoot":"","sources":["../../src/lib/web-ready.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AA4BrD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;AAE3E,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yFAAyF;IACzF,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAEF,iEAAiE;AACjE,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,CAAC;AAEvD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA4C9E;AA4DD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CA2DzB"}
1
+ {"version":3,"file":"web-ready.d.ts","sourceRoot":"","sources":["../../src/lib/web-ready.ts"],"names":[],"mappings":"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"}