@mediaway/container 0.1.6 → 0.2.1
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/README.md +38 -0
- package/dist/index.d.ts +44 -2
- package/dist/index.js +67 -5
- package/dist/replay.d.ts +176 -0
- package/dist/replay.js +227 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,6 +89,44 @@ demuxer.close();
|
|
|
89
89
|
WebM works the same way — pass `"webm"` to the `Muxer`/`Demuxer` constructor:
|
|
90
90
|
`new Muxer("webm")` / `new Demuxer("webm")`.
|
|
91
91
|
|
|
92
|
+
## Replay ring
|
|
93
|
+
|
|
94
|
+
`ReplayRing` keeps the last N milliseconds of encoded packets and cuts "the last M
|
|
95
|
+
milliseconds" at a keyframe of an anchor stream, in decode order, as a packet sequence
|
|
96
|
+
rebased to zero that a fresh `Muxer` writes as a standalone file. Other streams (audio) are
|
|
97
|
+
cut by time to match. Times are milliseconds.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { ReplayRing, Muxer } from "@mediaway/container";
|
|
101
|
+
|
|
102
|
+
const ring = ReplayRing.create({
|
|
103
|
+
anchorStreamId: 1, anchorTimeBase: { num: 1, den: 30 }, windowMs: 30_000, payload: "bytes",
|
|
104
|
+
});
|
|
105
|
+
ring.addStream(2, { num: 1, den: 48_000 });
|
|
106
|
+
ring.push({ streamId: 1, pts, dts, duration: 1, key, data }); // copies `data` in
|
|
107
|
+
|
|
108
|
+
const clip = ring.clipLast(10_000); // null until the anchor's first keyframe
|
|
109
|
+
// clip.entries(): { streamId, pts, dts (rebased), duration, isKeyframe, kind, payload }
|
|
110
|
+
// -> muxer.addVideoTrack / addAudioTrack once, then muxer.push({ ..., dts }) per entry
|
|
111
|
+
clip.close();
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- A **clip is an owned snapshot**: it stays valid while the ring keeps taking packets and
|
|
115
|
+
after the ring is closed. A `"bytes"` entry's `payload` is a **copy** out of the clip.
|
|
116
|
+
- It can start up to one keyframe interval **earlier** than asked, never later.
|
|
117
|
+
- `payload: "stored"` keeps only where the bytes are: `pushStored(meta, { file, offset,
|
|
118
|
+
length })`, with locations from `Muxer.createWithPlacements()` + `pollPlacements()` (the
|
|
119
|
+
offsets are file offsets when every `pollBytes` chunk is written sequentially from 0).
|
|
120
|
+
- Errors are distinct classes: `ReplayOutOfOrderError` (a stream's `dts` went backwards; the
|
|
121
|
+
packet was not added, drop it and carry on), `ReplayUnknownStreamError`,
|
|
122
|
+
`ReplayPayloadKindError` (`push` on a stored ring or `pushStored` on a bytes ring).
|
|
123
|
+
- **Limit:** the C ABI has no video-packet source (`EncodeSession` muxes its encoder's packets
|
|
124
|
+
internally; packet-level output exists only for the audio encoder). Feed the ring `Demuxer`
|
|
125
|
+
packets, audio-encoder packets, or packets from an encoder you drive yourself.
|
|
126
|
+
|
|
127
|
+
`Packet` now carries an optional `dts` (defaults to `pts` when pushing; always set on demuxed
|
|
128
|
+
packets), which a clip with B-frames needs.
|
|
129
|
+
|
|
92
130
|
## Other formats
|
|
93
131
|
|
|
94
132
|
Ogg/ADTS/FLV/MPEG-TS/MP3/WAV are exported from the same package but have their own
|
package/dist/index.d.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* structs out; the muxer never touches files — the caller owns byte I/O.
|
|
7
7
|
* `pts`/`duration` are integer ticks of each track's `timeBase`.
|
|
8
8
|
*/
|
|
9
|
+
import type { Placement } from "./replay.js";
|
|
9
10
|
export interface Rational {
|
|
10
11
|
num: number;
|
|
11
12
|
den: number;
|
|
12
13
|
}
|
|
13
|
-
export type VideoCodec = "h264" | "hevc" | "av1" | "vp9" | "vp8";
|
|
14
|
+
export type VideoCodec = "h264" | "hevc" | "av1" | "vp9" | "vp8" | "prores_422_proxy" | "prores_422_lt" | "prores_422" | "prores_422_hq" | "prores_4444" | "prores_4444_xq";
|
|
14
15
|
export type AudioCodec = "aac" | "opus" | "mp3" | "raw_audio";
|
|
15
16
|
export type PixelFormat = "nv12" | "bgra8" | "rgba8" | "i420" | "yuy2";
|
|
16
17
|
/** Which format `Muxer`/`Demuxer` open — mirrors mediaway_container_format_t.
|
|
@@ -46,6 +47,11 @@ export interface Packet {
|
|
|
46
47
|
data: Buffer;
|
|
47
48
|
/** Ticks of the track's timeBase. */
|
|
48
49
|
pts: number;
|
|
50
|
+
/**
|
|
51
|
+
* Decode timestamp, ticks of the track's timeBase. Optional on input (`Muxer.push` defaults it
|
|
52
|
+
* to `pts`, i.e. no B-frames); always set on demuxed packets.
|
|
53
|
+
*/
|
|
54
|
+
dts?: number;
|
|
49
55
|
/** Ticks of the track's timeBase. */
|
|
50
56
|
duration: number;
|
|
51
57
|
/** Sync sample / keyframe. */
|
|
@@ -56,6 +62,22 @@ export declare class MediawayError extends Error {
|
|
|
56
62
|
readonly status: number;
|
|
57
63
|
constructor(status: number, message?: string);
|
|
58
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* A replay ring refused a packet whose decode timestamp went backwards within its stream
|
|
67
|
+
* (status INVALID_PACKET). The packet was NOT added and the ring is still usable: drop it and
|
|
68
|
+
* carry on.
|
|
69
|
+
*/
|
|
70
|
+
export declare class ReplayOutOfOrderError extends MediawayError {
|
|
71
|
+
constructor();
|
|
72
|
+
}
|
|
73
|
+
/** A packet for a stream that was never added to the replay ring (status UNKNOWN_STREAM). */
|
|
74
|
+
export declare class ReplayUnknownStreamError extends MediawayError {
|
|
75
|
+
constructor();
|
|
76
|
+
}
|
|
77
|
+
/** `push` on a `"stored"` ring, or `pushStored` on a `"bytes"` ring (status INVALID_STATE). */
|
|
78
|
+
export declare class ReplayPayloadKindError extends MediawayError {
|
|
79
|
+
constructor();
|
|
80
|
+
}
|
|
59
81
|
export declare const CODEC_TO_ABI: Record<string, number>;
|
|
60
82
|
export declare const ABI_TO_CODEC: Record<number, string>;
|
|
61
83
|
export declare function check(status: number): void;
|
|
@@ -69,7 +91,26 @@ export declare function check(status: number): void;
|
|
|
69
91
|
export declare class Muxer {
|
|
70
92
|
private handle;
|
|
71
93
|
private nextIndex;
|
|
72
|
-
|
|
94
|
+
/**
|
|
95
|
+
* @param options.placements MP4 only: also record where every sample's payload lands in the
|
|
96
|
+
* output ({@link Muxer.pollPlacements}). The output bytes are identical; the only cost is
|
|
97
|
+
* one small record per sample.
|
|
98
|
+
*/
|
|
99
|
+
constructor(format?: ContainerFormat, options?: {
|
|
100
|
+
placements?: boolean;
|
|
101
|
+
});
|
|
102
|
+
/** An MP4 muxer that records payload placements — see {@link Muxer.pollPlacements}. */
|
|
103
|
+
static createWithPlacements(): Muxer;
|
|
104
|
+
/**
|
|
105
|
+
* Take the placements recorded since the last call, in write order: where each sample's
|
|
106
|
+
* payload landed in the bytes {@link pollBytes} returned. Feed them to
|
|
107
|
+
* {@link ReplayRing.pushStored}. A placement's bytes are available from `pollBytes` by the
|
|
108
|
+
* time the placement is.
|
|
109
|
+
*
|
|
110
|
+
* @throws {MediawayError} status 2 (INVALID_STATE) before `begin()`, and on a muxer that
|
|
111
|
+
* records none (any not built with `placements`, WebM included).
|
|
112
|
+
*/
|
|
113
|
+
pollPlacements(): Placement[];
|
|
73
114
|
addVideoTrack(info: VideoTrackInfo): number;
|
|
74
115
|
addAudioTrack(info: AudioTrackInfo): number;
|
|
75
116
|
/**
|
|
@@ -101,6 +142,7 @@ export declare class Demuxer {
|
|
|
101
142
|
pollPacket(): Packet | null;
|
|
102
143
|
close(): void;
|
|
103
144
|
}
|
|
145
|
+
export * from "./replay.js";
|
|
104
146
|
export * from "./ogg.js";
|
|
105
147
|
export * from "./adts.js";
|
|
106
148
|
export * from "./flv.js";
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* structs out; the muxer never touches files — the caller owns byte I/O.
|
|
7
7
|
* `pts`/`duration` are integer ticks of each track's `timeBase`.
|
|
8
8
|
*/
|
|
9
|
-
import { MwPacket, MwPacketView, MwRational, MwStreamInfo, MwVideoTrackInfo, MwAudioTrackInfo, container, copyBytes, } from "@mediaway/ffi";
|
|
9
|
+
import { MwPacket, MwPacketView, MwRational, MwStreamInfo, MwVideoTrackInfo, MwAudioTrackInfo, container, copyBytes, replay, } from "@mediaway/ffi";
|
|
10
|
+
import { takePlacements } from "./replay.js";
|
|
10
11
|
const FORMAT_TO_ABI = { mp4: 0, webm: 1 };
|
|
11
12
|
/** Raw per-crate ABI status from mediaway-ffi (mediaway_status_t). */
|
|
12
13
|
export class MediawayError extends Error {
|
|
@@ -16,6 +17,31 @@ export class MediawayError extends Error {
|
|
|
16
17
|
this.status = status;
|
|
17
18
|
}
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A replay ring refused a packet whose decode timestamp went backwards within its stream
|
|
22
|
+
* (status INVALID_PACKET). The packet was NOT added and the ring is still usable: drop it and
|
|
23
|
+
* carry on.
|
|
24
|
+
*/
|
|
25
|
+
export class ReplayOutOfOrderError extends MediawayError {
|
|
26
|
+
constructor() {
|
|
27
|
+
super(4, "replay ring: dts went backwards on this stream; packet not added");
|
|
28
|
+
this.name = "ReplayOutOfOrderError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** A packet for a stream that was never added to the replay ring (status UNKNOWN_STREAM). */
|
|
32
|
+
export class ReplayUnknownStreamError extends MediawayError {
|
|
33
|
+
constructor() {
|
|
34
|
+
super(10, "replay ring: stream was never added");
|
|
35
|
+
this.name = "ReplayUnknownStreamError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** `push` on a `"stored"` ring, or `pushStored` on a `"bytes"` ring (status INVALID_STATE). */
|
|
39
|
+
export class ReplayPayloadKindError extends MediawayError {
|
|
40
|
+
constructor() {
|
|
41
|
+
super(2, "replay ring: wrong payload kind for this call (push vs pushStored)");
|
|
42
|
+
this.name = "ReplayPayloadKindError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
19
45
|
export const CODEC_TO_ABI = {
|
|
20
46
|
h264: 0,
|
|
21
47
|
hevc: 1,
|
|
@@ -27,6 +53,12 @@ export const CODEC_TO_ABI = {
|
|
|
27
53
|
vorbis: 7,
|
|
28
54
|
raw_audio: 11,
|
|
29
55
|
vp8: 12,
|
|
56
|
+
prores_422_proxy: 13,
|
|
57
|
+
prores_422_lt: 14,
|
|
58
|
+
prores_422: 15,
|
|
59
|
+
prores_422_hq: 16,
|
|
60
|
+
prores_4444: 17,
|
|
61
|
+
prores_4444_xq: 18,
|
|
30
62
|
};
|
|
31
63
|
export const ABI_TO_CODEC = Object.fromEntries(Object.entries(CODEC_TO_ABI).map(([k, v]) => [v, k]));
|
|
32
64
|
export function check(status) {
|
|
@@ -57,12 +89,40 @@ export function check(status) {
|
|
|
57
89
|
export class Muxer {
|
|
58
90
|
handle;
|
|
59
91
|
nextIndex = 1;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
92
|
+
/**
|
|
93
|
+
* @param options.placements MP4 only: also record where every sample's payload lands in the
|
|
94
|
+
* output ({@link Muxer.pollPlacements}). The output bytes are identical; the only cost is
|
|
95
|
+
* one small record per sample.
|
|
96
|
+
*/
|
|
97
|
+
constructor(format = "mp4", options = {}) {
|
|
98
|
+
if (options.placements) {
|
|
99
|
+
if (format !== "mp4")
|
|
100
|
+
throw new MediawayError(1, "placements are MP4-only");
|
|
101
|
+
this.handle = replay.muxerCreateWithPlacements();
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.handle =
|
|
105
|
+
format === "mp4" ? container.muxerCreate() : container.muxerCreateForFormat(FORMAT_TO_ABI[format]);
|
|
106
|
+
}
|
|
63
107
|
if (!this.handle)
|
|
64
108
|
throw new MediawayError(7, "muxer creation panicked");
|
|
65
109
|
}
|
|
110
|
+
/** An MP4 muxer that records payload placements — see {@link Muxer.pollPlacements}. */
|
|
111
|
+
static createWithPlacements() {
|
|
112
|
+
return new Muxer("mp4", { placements: true });
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Take the placements recorded since the last call, in write order: where each sample's
|
|
116
|
+
* payload landed in the bytes {@link pollBytes} returned. Feed them to
|
|
117
|
+
* {@link ReplayRing.pushStored}. A placement's bytes are available from `pollBytes` by the
|
|
118
|
+
* time the placement is.
|
|
119
|
+
*
|
|
120
|
+
* @throws {MediawayError} status 2 (INVALID_STATE) before `begin()`, and on a muxer that
|
|
121
|
+
* records none (any not built with `placements`, WebM included).
|
|
122
|
+
*/
|
|
123
|
+
pollPlacements() {
|
|
124
|
+
return takePlacements(this.handle);
|
|
125
|
+
}
|
|
66
126
|
addVideoTrack(info) {
|
|
67
127
|
const index = this.nextIndex++;
|
|
68
128
|
const raw = {
|
|
@@ -105,7 +165,7 @@ export class Muxer {
|
|
|
105
165
|
const raw = {
|
|
106
166
|
stream_id: packet.trackIndex,
|
|
107
167
|
pts: BigInt(packet.pts),
|
|
108
|
-
dts: BigInt(packet.pts), //
|
|
168
|
+
dts: BigInt(packet.dts ?? packet.pts), // defaults to pts (no B-frames)
|
|
109
169
|
duration: BigInt(packet.duration),
|
|
110
170
|
is_keyframe: packet.key ?? false,
|
|
111
171
|
is_discard: false,
|
|
@@ -203,6 +263,7 @@ export class Demuxer {
|
|
|
203
263
|
trackIndex,
|
|
204
264
|
data: payload,
|
|
205
265
|
pts: Number(raw.pts),
|
|
266
|
+
dts: Number(raw.dts),
|
|
206
267
|
duration: Number(raw.duration),
|
|
207
268
|
key: raw.is_keyframe,
|
|
208
269
|
};
|
|
@@ -218,6 +279,7 @@ export class Demuxer {
|
|
|
218
279
|
// Ogg/ADTS/FLV/MPEG-TS/MP3/WAV each need a genuinely different C ABI shape
|
|
219
280
|
// from MP4/WebM's Muxer/Demuxer (no track registration, out-buffer-per-call
|
|
220
281
|
// mux, or a construction-time stream list) — see each module's own top comment.
|
|
282
|
+
export * from "./replay.js";
|
|
221
283
|
export * from "./ogg.js";
|
|
222
284
|
export * from "./adts.js";
|
|
223
285
|
export * from "./flv.js";
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay ring — keep the last N milliseconds of encoded packets in memory (or just their
|
|
3
|
+
* locations on disk) and cut "the last M milliseconds" at a keyframe, as a packet sequence
|
|
4
|
+
* rebased to zero that a fresh {@link Muxer} writes as a standalone file.
|
|
5
|
+
* (adr/container/0009-replay-ring-c-abi.md)
|
|
6
|
+
*
|
|
7
|
+
* **Where the packets come from:** the C ABI has no video-packet source. `EncodeSession` muxes
|
|
8
|
+
* its encoder's packets internally, and packet-level output exists only for the audio encoder.
|
|
9
|
+
* Feed the ring packets from a {@link Demuxer}, from the audio encoder, or from an encoder you
|
|
10
|
+
* drive yourself.
|
|
11
|
+
*
|
|
12
|
+
* Ring timestamps are ticks of each stream's own `timeBase`; the anchor stream's keyframes
|
|
13
|
+
* decide where a clip may start, and other streams (audio) are cut by time to match.
|
|
14
|
+
*/
|
|
15
|
+
import { type Rational } from "./index.js";
|
|
16
|
+
/** What a ring holds for each packet. */
|
|
17
|
+
export type ReplayPayloadKind = "bytes" | "stored";
|
|
18
|
+
export interface ReplayRingOptions {
|
|
19
|
+
/** The stream whose keyframes decide where a clip may start (normally video). */
|
|
20
|
+
anchorStreamId: number;
|
|
21
|
+
/** The anchor's timebase. */
|
|
22
|
+
anchorTimeBase: Rational;
|
|
23
|
+
/** How much history to keep, in milliseconds. */
|
|
24
|
+
windowMs: number;
|
|
25
|
+
/**
|
|
26
|
+
* Evict oldest-GOP-first while more than this many payload bytes are held; 0 or omitted = no
|
|
27
|
+
* ceiling. The newest GOP is always kept, so one larger GOP can exceed it.
|
|
28
|
+
*/
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
/**
|
|
31
|
+
* `"bytes"`: the ring copies each payload in ({@link ReplayRing.push}). `"stored"`: the ring
|
|
32
|
+
* keeps only where the bytes are ({@link ReplayRing.pushStored}), for a caller that already
|
|
33
|
+
* writes packets to a file.
|
|
34
|
+
*/
|
|
35
|
+
payload: ReplayPayloadKind;
|
|
36
|
+
}
|
|
37
|
+
/** A packet to push into a `"bytes"` ring. Timestamps are ticks of the stream's timebase. */
|
|
38
|
+
export interface ReplayPacket {
|
|
39
|
+
streamId: number;
|
|
40
|
+
pts: number;
|
|
41
|
+
/** Decode timestamp. Must not go backwards within a stream. Defaults to `pts` (no B-frames). */
|
|
42
|
+
dts?: number;
|
|
43
|
+
duration: number;
|
|
44
|
+
key?: boolean;
|
|
45
|
+
data: Buffer;
|
|
46
|
+
}
|
|
47
|
+
/** A packet's metadata without its payload — input to {@link ReplayRing.pushStored}. */
|
|
48
|
+
export interface ReplayPacketMeta {
|
|
49
|
+
streamId: number;
|
|
50
|
+
pts: number;
|
|
51
|
+
dts?: number;
|
|
52
|
+
duration: number;
|
|
53
|
+
key?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** Where a packet's payload was stored on the caller's disk. */
|
|
56
|
+
export interface StoredLocation {
|
|
57
|
+
/** The caller's own id for the file. */
|
|
58
|
+
file: number;
|
|
59
|
+
/** Byte offset of the payload in that file (exact up to 2^53). */
|
|
60
|
+
offset: number;
|
|
61
|
+
/** Payload length in bytes. */
|
|
62
|
+
length: number;
|
|
63
|
+
}
|
|
64
|
+
interface ReplayEntryBase {
|
|
65
|
+
streamId: number;
|
|
66
|
+
/** Rebased so the cut keyframe decodes at zero. */
|
|
67
|
+
pts: number;
|
|
68
|
+
dts: number;
|
|
69
|
+
duration: number;
|
|
70
|
+
isKeyframe: boolean;
|
|
71
|
+
isDiscard: boolean;
|
|
72
|
+
}
|
|
73
|
+
/** One packet of a clip cut from a `"bytes"` ring. */
|
|
74
|
+
export interface ReplayBytesEntry extends ReplayEntryBase {
|
|
75
|
+
kind: "bytes";
|
|
76
|
+
/** A COPY of the payload: the clip's own pointer is borrowed and never leaves this package. */
|
|
77
|
+
payload: Buffer;
|
|
78
|
+
}
|
|
79
|
+
/** One packet of a clip cut from a `"stored"` ring. */
|
|
80
|
+
export interface ReplayStoredEntry extends ReplayEntryBase {
|
|
81
|
+
kind: "stored";
|
|
82
|
+
stored: StoredLocation;
|
|
83
|
+
}
|
|
84
|
+
export type ReplayEntry = ReplayBytesEntry | ReplayStoredEntry;
|
|
85
|
+
/**
|
|
86
|
+
* A clip: an OWNED SNAPSHOT of the ring at the moment {@link ReplayRing.clipLast} was called.
|
|
87
|
+
* Unlike the Rust `Clip` it does not borrow the ring, so it stays valid while the ring keeps
|
|
88
|
+
* taking packets and after the ring is closed. Release it with {@link close}.
|
|
89
|
+
*/
|
|
90
|
+
export declare class ReplayClip {
|
|
91
|
+
private handle;
|
|
92
|
+
/** @internal Use {@link ReplayRing.clipLast}. */
|
|
93
|
+
constructor(handle: unknown);
|
|
94
|
+
private live;
|
|
95
|
+
/** Number of packets. */
|
|
96
|
+
get length(): number;
|
|
97
|
+
/** From the cut keyframe to the newest packet pushed when the clip was taken, in ms. */
|
|
98
|
+
get durationMs(): number;
|
|
99
|
+
/**
|
|
100
|
+
* Packet `index` in decode order across streams, timestamps rebased so the cut keyframe
|
|
101
|
+
* decodes at zero. A `"bytes"` entry's `payload` is a copy.
|
|
102
|
+
*/
|
|
103
|
+
entry(index: number): ReplayEntry;
|
|
104
|
+
/** Every entry, in order. Copies each `"bytes"` payload once. */
|
|
105
|
+
entries(): ReplayEntry[];
|
|
106
|
+
[Symbol.iterator](): IterableIterator<ReplayEntry>;
|
|
107
|
+
/** Free the snapshot. Idempotent. */
|
|
108
|
+
close(): void;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* A rolling buffer of the last `windowMs` of encoded packets.
|
|
112
|
+
*
|
|
113
|
+
* Push packets in decode order per stream; {@link clipLast} cuts at a keyframe of the anchor
|
|
114
|
+
* stream. Anchor packets before the anchor's first keyframe are dropped without error.
|
|
115
|
+
*/
|
|
116
|
+
export declare class ReplayRing {
|
|
117
|
+
private handle;
|
|
118
|
+
private readonly kind;
|
|
119
|
+
private constructor();
|
|
120
|
+
/** Create a ring. */
|
|
121
|
+
static create(options: ReplayRingOptions): ReplayRing;
|
|
122
|
+
private live;
|
|
123
|
+
/** Carry another stream (e.g. audio), cut by time to match the anchor. */
|
|
124
|
+
addStream(streamId: number, timeBase: Rational): void;
|
|
125
|
+
/**
|
|
126
|
+
* Add a packet to a `"bytes"` ring, then evict whatever fell out of the window.
|
|
127
|
+
*
|
|
128
|
+
* The payload is COPIED into the ring (the native call only borrows it), and every clip then
|
|
129
|
+
* shares that copy.
|
|
130
|
+
*
|
|
131
|
+
* @throws {ReplayOutOfOrderError} this stream's `dts` went backwards; the packet was not added
|
|
132
|
+
* and the ring is still usable, so drop it and carry on.
|
|
133
|
+
* @throws {ReplayUnknownStreamError} the stream was never added.
|
|
134
|
+
* @throws {ReplayPayloadKindError} this is a `"stored"` ring.
|
|
135
|
+
*/
|
|
136
|
+
push(packet: ReplayPacket): void;
|
|
137
|
+
/**
|
|
138
|
+
* Add a packet to a `"stored"` ring: its metadata and where its bytes are. The ring never
|
|
139
|
+
* reads the file and holds a few dozen bytes per packet. Take the location from
|
|
140
|
+
* {@link Muxer.pollPlacements} when every polled muxer byte is written to a file.
|
|
141
|
+
* Errors as {@link push}, with {@link ReplayPayloadKindError} on a `"bytes"` ring.
|
|
142
|
+
*/
|
|
143
|
+
pushStored(meta: ReplayPacketMeta, stored: StoredLocation): void;
|
|
144
|
+
/** The longest clip {@link clipLast} can return now, in ms; 0 before the first keyframe. */
|
|
145
|
+
spanMs(): number;
|
|
146
|
+
/**
|
|
147
|
+
* Cut the last `spanMs` of every stream at a keyframe. The clip starts at the latest anchor
|
|
148
|
+
* keyframe at or before `newest - spanMs`: up to one keyframe interval EARLIER than asked,
|
|
149
|
+
* never later; with less than `spanMs` held it starts at the oldest keyframe.
|
|
150
|
+
*
|
|
151
|
+
* @returns the clip, or `null` until the anchor's first keyframe has been pushed.
|
|
152
|
+
*/
|
|
153
|
+
clipLast(spanMs: number): ReplayClip | null;
|
|
154
|
+
/** Whether this ring holds payload bytes or locations. */
|
|
155
|
+
get payload(): ReplayPayloadKind;
|
|
156
|
+
/** Close the ring. Clips already taken stay valid. Idempotent. */
|
|
157
|
+
close(): void;
|
|
158
|
+
}
|
|
159
|
+
/** Where one muxed sample's payload landed in the output stream. */
|
|
160
|
+
export interface Placement {
|
|
161
|
+
/** The packet's track index (as passed to `Muxer.push`). */
|
|
162
|
+
trackIndex: number;
|
|
163
|
+
/** The sample's decode timestamp, as pushed. */
|
|
164
|
+
dts: number;
|
|
165
|
+
/**
|
|
166
|
+
* Absolute byte offset of the payload in the muxer's output: counted from the first byte
|
|
167
|
+
* `pollBytes` ever returned, so it is the file offset when every polled byte is written
|
|
168
|
+
* sequentially from 0. Exact up to 2^53.
|
|
169
|
+
*/
|
|
170
|
+
offset: number;
|
|
171
|
+
/** Payload length as written: Annex-B becomes length-prefixed and ADTS is stripped. */
|
|
172
|
+
length: number;
|
|
173
|
+
}
|
|
174
|
+
/** @internal Take the muxer's placements; used by `Muxer.pollPlacements`. */
|
|
175
|
+
export declare function takePlacements(muxer: unknown): Placement[];
|
|
176
|
+
export {};
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay ring — keep the last N milliseconds of encoded packets in memory (or just their
|
|
3
|
+
* locations on disk) and cut "the last M milliseconds" at a keyframe, as a packet sequence
|
|
4
|
+
* rebased to zero that a fresh {@link Muxer} writes as a standalone file.
|
|
5
|
+
* (adr/container/0009-replay-ring-c-abi.md)
|
|
6
|
+
*
|
|
7
|
+
* **Where the packets come from:** the C ABI has no video-packet source. `EncodeSession` muxes
|
|
8
|
+
* its encoder's packets internally, and packet-level output exists only for the audio encoder.
|
|
9
|
+
* Feed the ring packets from a {@link Demuxer}, from the audio encoder, or from an encoder you
|
|
10
|
+
* drive yourself.
|
|
11
|
+
*
|
|
12
|
+
* Ring timestamps are ticks of each stream's own `timeBase`; the anchor stream's keyframes
|
|
13
|
+
* decide where a clip may start, and other streams (audio) are cut by time to match.
|
|
14
|
+
*/
|
|
15
|
+
import { copyBytes, decodeArray, replay, } from "@mediaway/ffi";
|
|
16
|
+
import { MediawayError, ReplayOutOfOrderError, ReplayPayloadKindError, ReplayUnknownStreamError, check, } from "./index.js";
|
|
17
|
+
const PAYLOAD_BYTES = 0;
|
|
18
|
+
const PAYLOAD_STORED = 1;
|
|
19
|
+
/** Map the ring's statuses to distinct classes; everything else is the shared container error. */
|
|
20
|
+
function checkReplay(status) {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 0:
|
|
23
|
+
return;
|
|
24
|
+
case 2:
|
|
25
|
+
throw new ReplayPayloadKindError();
|
|
26
|
+
case 4:
|
|
27
|
+
throw new ReplayOutOfOrderError();
|
|
28
|
+
case 10:
|
|
29
|
+
throw new ReplayUnknownStreamError();
|
|
30
|
+
default:
|
|
31
|
+
check(status);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A clip: an OWNED SNAPSHOT of the ring at the moment {@link ReplayRing.clipLast} was called.
|
|
36
|
+
* Unlike the Rust `Clip` it does not borrow the ring, so it stays valid while the ring keeps
|
|
37
|
+
* taking packets and after the ring is closed. Release it with {@link close}.
|
|
38
|
+
*/
|
|
39
|
+
export class ReplayClip {
|
|
40
|
+
handle;
|
|
41
|
+
/** @internal Use {@link ReplayRing.clipLast}. */
|
|
42
|
+
constructor(handle) {
|
|
43
|
+
this.handle = handle;
|
|
44
|
+
}
|
|
45
|
+
live() {
|
|
46
|
+
if (!this.handle)
|
|
47
|
+
throw new MediawayError(2, "replay clip is closed");
|
|
48
|
+
return this.handle;
|
|
49
|
+
}
|
|
50
|
+
/** Number of packets. */
|
|
51
|
+
get length() {
|
|
52
|
+
return Number(replay.clipPacketCount(this.live()));
|
|
53
|
+
}
|
|
54
|
+
/** From the cut keyframe to the newest packet pushed when the clip was taken, in ms. */
|
|
55
|
+
get durationMs() {
|
|
56
|
+
const out = [0n];
|
|
57
|
+
checkReplay(replay.clipDurationMs(this.live(), out));
|
|
58
|
+
return Number(out[0]);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Packet `index` in decode order across streams, timestamps rebased so the cut keyframe
|
|
62
|
+
* decodes at zero. A `"bytes"` entry's `payload` is a copy.
|
|
63
|
+
*/
|
|
64
|
+
entry(index) {
|
|
65
|
+
const raw = {};
|
|
66
|
+
checkReplay(replay.clipPacketAt(this.live(), index, raw));
|
|
67
|
+
const base = {
|
|
68
|
+
streamId: raw.stream_id,
|
|
69
|
+
pts: Number(raw.pts),
|
|
70
|
+
dts: Number(raw.dts),
|
|
71
|
+
duration: Number(raw.duration),
|
|
72
|
+
isKeyframe: raw.is_keyframe,
|
|
73
|
+
isDiscard: raw.is_discard,
|
|
74
|
+
};
|
|
75
|
+
if (raw.payload_kind === PAYLOAD_STORED) {
|
|
76
|
+
return {
|
|
77
|
+
...base,
|
|
78
|
+
kind: "stored",
|
|
79
|
+
stored: { file: raw.stored_file, offset: Number(raw.stored_offset), length: raw.stored_len },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { ...base, kind: "bytes", payload: copyBytes(raw.payload, raw.payload_len) };
|
|
83
|
+
}
|
|
84
|
+
/** Every entry, in order. Copies each `"bytes"` payload once. */
|
|
85
|
+
entries() {
|
|
86
|
+
const n = this.length;
|
|
87
|
+
const out = [];
|
|
88
|
+
for (let i = 0; i < n; i++)
|
|
89
|
+
out.push(this.entry(i));
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
*[Symbol.iterator]() {
|
|
93
|
+
const n = this.length;
|
|
94
|
+
for (let i = 0; i < n; i++)
|
|
95
|
+
yield this.entry(i);
|
|
96
|
+
}
|
|
97
|
+
/** Free the snapshot. Idempotent. */
|
|
98
|
+
close() {
|
|
99
|
+
if (this.handle) {
|
|
100
|
+
replay.clipFree(this.handle);
|
|
101
|
+
this.handle = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A rolling buffer of the last `windowMs` of encoded packets.
|
|
107
|
+
*
|
|
108
|
+
* Push packets in decode order per stream; {@link clipLast} cuts at a keyframe of the anchor
|
|
109
|
+
* stream. Anchor packets before the anchor's first keyframe are dropped without error.
|
|
110
|
+
*/
|
|
111
|
+
export class ReplayRing {
|
|
112
|
+
handle;
|
|
113
|
+
kind;
|
|
114
|
+
constructor(handle, kind) {
|
|
115
|
+
this.handle = handle;
|
|
116
|
+
this.kind = kind;
|
|
117
|
+
}
|
|
118
|
+
/** Create a ring. */
|
|
119
|
+
static create(options) {
|
|
120
|
+
const config = {
|
|
121
|
+
anchor_stream_id: options.anchorStreamId,
|
|
122
|
+
anchor_time_base: { num: BigInt(options.anchorTimeBase.num), den: options.anchorTimeBase.den },
|
|
123
|
+
window_ms: BigInt(options.windowMs),
|
|
124
|
+
max_bytes: BigInt(options.maxBytes ?? 0),
|
|
125
|
+
payload_kind: options.payload === "stored" ? PAYLOAD_STORED : PAYLOAD_BYTES,
|
|
126
|
+
};
|
|
127
|
+
const out = [null];
|
|
128
|
+
checkReplay(replay.ringCreate(config, out));
|
|
129
|
+
return new ReplayRing(out[0], options.payload);
|
|
130
|
+
}
|
|
131
|
+
live() {
|
|
132
|
+
if (!this.handle)
|
|
133
|
+
throw new MediawayError(2, "replay ring is closed");
|
|
134
|
+
return this.handle;
|
|
135
|
+
}
|
|
136
|
+
/** Carry another stream (e.g. audio), cut by time to match the anchor. */
|
|
137
|
+
addStream(streamId, timeBase) {
|
|
138
|
+
checkReplay(replay.ringAddStream(this.live(), streamId, { num: BigInt(timeBase.num), den: timeBase.den }));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Add a packet to a `"bytes"` ring, then evict whatever fell out of the window.
|
|
142
|
+
*
|
|
143
|
+
* The payload is COPIED into the ring (the native call only borrows it), and every clip then
|
|
144
|
+
* shares that copy.
|
|
145
|
+
*
|
|
146
|
+
* @throws {ReplayOutOfOrderError} this stream's `dts` went backwards; the packet was not added
|
|
147
|
+
* and the ring is still usable, so drop it and carry on.
|
|
148
|
+
* @throws {ReplayUnknownStreamError} the stream was never added.
|
|
149
|
+
* @throws {ReplayPayloadKindError} this is a `"stored"` ring.
|
|
150
|
+
*/
|
|
151
|
+
push(packet) {
|
|
152
|
+
const raw = {
|
|
153
|
+
stream_id: packet.streamId,
|
|
154
|
+
pts: BigInt(packet.pts),
|
|
155
|
+
dts: BigInt(packet.dts ?? packet.pts),
|
|
156
|
+
duration: BigInt(packet.duration),
|
|
157
|
+
is_keyframe: packet.key ?? false,
|
|
158
|
+
is_discard: false,
|
|
159
|
+
payload: packet.data,
|
|
160
|
+
payload_len: packet.data.length,
|
|
161
|
+
};
|
|
162
|
+
checkReplay(replay.ringPush(this.live(), raw));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Add a packet to a `"stored"` ring: its metadata and where its bytes are. The ring never
|
|
166
|
+
* reads the file and holds a few dozen bytes per packet. Take the location from
|
|
167
|
+
* {@link Muxer.pollPlacements} when every polled muxer byte is written to a file.
|
|
168
|
+
* Errors as {@link push}, with {@link ReplayPayloadKindError} on a `"bytes"` ring.
|
|
169
|
+
*/
|
|
170
|
+
pushStored(meta, stored) {
|
|
171
|
+
checkReplay(replay.ringPushStored(this.live(), {
|
|
172
|
+
stream_id: meta.streamId,
|
|
173
|
+
pts: BigInt(meta.pts),
|
|
174
|
+
dts: BigInt(meta.dts ?? meta.pts),
|
|
175
|
+
duration: BigInt(meta.duration),
|
|
176
|
+
is_keyframe: meta.key ?? false,
|
|
177
|
+
is_discard: false,
|
|
178
|
+
}, { file: stored.file, offset: BigInt(stored.offset), len: stored.length }));
|
|
179
|
+
}
|
|
180
|
+
/** The longest clip {@link clipLast} can return now, in ms; 0 before the first keyframe. */
|
|
181
|
+
spanMs() {
|
|
182
|
+
const out = [0n];
|
|
183
|
+
checkReplay(replay.ringSpanMs(this.live(), out));
|
|
184
|
+
return Number(out[0]);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Cut the last `spanMs` of every stream at a keyframe. The clip starts at the latest anchor
|
|
188
|
+
* keyframe at or before `newest - spanMs`: up to one keyframe interval EARLIER than asked,
|
|
189
|
+
* never later; with less than `spanMs` held it starts at the oldest keyframe.
|
|
190
|
+
*
|
|
191
|
+
* @returns the clip, or `null` until the anchor's first keyframe has been pushed.
|
|
192
|
+
*/
|
|
193
|
+
clipLast(spanMs) {
|
|
194
|
+
const clip = [null];
|
|
195
|
+
const has = [false];
|
|
196
|
+
checkReplay(replay.ringClipLast(this.live(), BigInt(spanMs), clip, has));
|
|
197
|
+
return has[0] ? new ReplayClip(clip[0]) : null;
|
|
198
|
+
}
|
|
199
|
+
/** Whether this ring holds payload bytes or locations. */
|
|
200
|
+
get payload() {
|
|
201
|
+
return this.kind;
|
|
202
|
+
}
|
|
203
|
+
/** Close the ring. Clips already taken stay valid. Idempotent. */
|
|
204
|
+
close() {
|
|
205
|
+
if (this.handle) {
|
|
206
|
+
replay.ringClose(this.handle);
|
|
207
|
+
this.handle = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** @internal Take the muxer's placements; used by `Muxer.pollPlacements`. */
|
|
212
|
+
export function takePlacements(muxer) {
|
|
213
|
+
const rows = [null];
|
|
214
|
+
const count = [0];
|
|
215
|
+
checkReplay(replay.muxerPollPlacements(muxer, rows, count));
|
|
216
|
+
const n = Number(count[0]);
|
|
217
|
+
const decoded = decodeArray(rows[0], replay.structs.MwPlacement, n);
|
|
218
|
+
const out = decoded.map((p) => ({
|
|
219
|
+
trackIndex: p.track_id,
|
|
220
|
+
dts: Number(p.dts),
|
|
221
|
+
offset: Number(p.offset),
|
|
222
|
+
length: p.len,
|
|
223
|
+
}));
|
|
224
|
+
if (n > 0)
|
|
225
|
+
replay.placementsFree(rows[0], n);
|
|
226
|
+
return out;
|
|
227
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediaway/container",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Rust-based, lightweight, fast — sans-IO fragmented MP4 (fMP4) mux and demux for Node.js — push packets in, poll bytes out",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|