@volter/world-core 3.0.151 → 3.0.152
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/dist/src/derived-core.d.ts +8 -0
- package/dist/src/derived-core.js +2 -0
- package/dist/src/derived.d.ts +4 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/live/access.d.ts +22 -0
- package/dist/src/live/access.js +83 -0
- package/dist/src/live/amf0.d.ts +7 -0
- package/dist/src/live/amf0.js +122 -0
- package/dist/src/live/flv.d.ts +42 -0
- package/dist/src/live/flv.js +148 -0
- package/dist/src/live/fmp4.d.ts +25 -0
- package/dist/src/live/fmp4.js +96 -0
- package/dist/src/live/hls.d.ts +22 -0
- package/dist/src/live/hls.js +46 -0
- package/dist/src/live/index.d.ts +9 -0
- package/dist/src/live/index.js +8 -0
- package/dist/src/live/ingest.d.ts +25 -0
- package/dist/src/live/ingest.js +229 -0
- package/dist/src/live/rtmp.d.ts +35 -0
- package/dist/src/live/rtmp.js +456 -0
- package/dist/src/live/segmenter.d.ts +57 -0
- package/dist/src/live/segmenter.js +204 -0
- package/dist/src/live/types.d.ts +102 -0
- package/dist/src/live/types.js +5 -0
- package/dist/src/pack-fetch.d.ts +4 -0
- package/dist/src/pack-fetch.js +24 -2
- package/dist/src/packRegistry.d.ts +1 -0
- package/dist/src/serve-http.d.ts +7 -1
- package/dist/src/serve-http.js +36 -3
- package/dist/src/twin-fetch.d.ts +4 -1
- package/package.json +5 -1
- package/src/derived-core.ts +10 -0
- package/src/derived.ts +4 -0
- package/src/index.ts +4 -1
- package/src/live/access.ts +89 -0
- package/src/live/amf0.ts +80 -0
- package/src/live/flv.ts +145 -0
- package/src/live/fmp4.ts +102 -0
- package/src/live/hls.ts +59 -0
- package/src/live/index.ts +13 -0
- package/src/live/ingest.ts +210 -0
- package/src/live/rtmp.ts +375 -0
- package/src/live/segmenter.ts +218 -0
- package/src/live/types.ts +105 -0
- package/src/pack-fetch.ts +28 -2
- package/src/packRegistry.ts +1 -1
- package/src/serve-http.ts +29 -4
- package/src/twin-fetch.ts +4 -2
|
@@ -2,6 +2,7 @@ import { type TwinResource } from './serve.js';
|
|
|
2
2
|
import { GitObjectStore } from './git/objects.js';
|
|
3
3
|
import { GitRefs } from './git/refs.js';
|
|
4
4
|
import { type SocketDecl, type SocketSession } from './sockets.js';
|
|
5
|
+
import type { LiveAccess, LiveDecl } from './live/types.js';
|
|
5
6
|
import { type MachinePool } from './machines.js';
|
|
6
7
|
import { type ApplicationAnswer, type ApplicationStandIn, type EventRender, type EventScheme, type EventsDecl, type EventValues } from './events.js';
|
|
7
8
|
import type { RateBudgetDeclaration } from './rateBudget.js';
|
|
@@ -766,6 +767,10 @@ export type DerivedManifest = {
|
|
|
766
767
|
/** The persistent wires the vendor's clients hold open (sockets.ts: Discord's Gateway): each served at its path by
|
|
767
768
|
* the pack's `semantics/sockets.ts`, which the kernel offers every write after its webhooks. */
|
|
768
769
|
sockets?: ReadonlyArray<SocketDecl>;
|
|
770
|
+
/** The broadcasts the vendor takes over RTMP (architecture, "Other wires": live media): each ingest's id, which names
|
|
771
|
+
* its engine in the pack's `semantics/live.ts`, and the RTMP app it answers. The pack's fetch carries the ingest
|
|
772
|
+
* (`fetch.ingest`), which its server serves on a side port. */
|
|
773
|
+
live?: ReadonlyArray<LiveDecl>;
|
|
769
774
|
/** The vendor speaks gRPC (architecture, "Other wires": gRPC): its server answers HTTP/2 and HTTP/1 on one port, each
|
|
770
775
|
* rpc (by its proto unit's schema, `src/generated/proto.gen.json`) through the pack's own fetch. */
|
|
771
776
|
grpc?: true;
|
|
@@ -949,6 +954,9 @@ export type SemanticsContext = {
|
|
|
949
954
|
issue(resource: string): Promise<string>;
|
|
950
955
|
/** The vendor's open sessions of one of its sockets in this World (a request answered by a session: sockets.ts). */
|
|
951
956
|
sockets(id: string): SocketSession[];
|
|
957
|
+
/** The World's recordings of what was broadcast to this twin (live media, live/access.ts): a recording's facts, and its
|
|
958
|
+
* playlists, init and segments answered as the vendor's media host answers them. */
|
|
959
|
+
live: LiveAccess;
|
|
952
960
|
/** Hold this read until the World writes (held-reads.ts: HTTP long polling): true when a write of one of the named
|
|
953
961
|
* resources lands in this World, false when `ms` pass or the client hangs up; at once false in a read-only World. */
|
|
954
962
|
awaitWrite(resources: ReadonlyArray<string>, ms: number): Promise<boolean>;
|
package/dist/src/derived-core.js
CHANGED
|
@@ -15,6 +15,7 @@ import { GitRefs } from "./git/refs.js";
|
|
|
15
15
|
import { worldNow } from "./world-clock.js";
|
|
16
16
|
import { worldEnvValue } from "./world-env.js";
|
|
17
17
|
import { openSessions, socketWrite } from "./sockets.js";
|
|
18
|
+
import { liveAccess } from "./live/access.js";
|
|
18
19
|
import { awaitWrite, awaitCondition, heldReadsWrite } from "./held-reads.js";
|
|
19
20
|
import { MACHINE_POOL, poolFor, guardedMachinePool } from "./machines.js";
|
|
20
21
|
import { askApplication, deliverEvents } from "./events.js";
|
|
@@ -1538,6 +1539,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1538
1539
|
...(options.database === undefined ? {} : { database: options.database }),
|
|
1539
1540
|
...(options.transaction === undefined ? {} : { transaction: options.transaction }),
|
|
1540
1541
|
}),
|
|
1542
|
+
live: liveAccess({ rows: (type) => resourcesOfType(m.service, type, root), blob: (key) => readResourceBlob(m.service, key, root) }, call.request),
|
|
1541
1543
|
blobs: {
|
|
1542
1544
|
put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
|
|
1543
1545
|
get: (key) => readResourceBlob(m.service, key, root),
|
package/dist/src/derived.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WebSocketUpgrade } from './serve-http.js';
|
|
2
|
+
import type { TwinStream } from './twin-fetch.js';
|
|
2
3
|
/** The slice of a generated surface the dispatch reads. */
|
|
3
4
|
export type DerivedOperation = {
|
|
4
5
|
id: string;
|
|
@@ -126,6 +127,9 @@ export type DerivedFetch = ((request: Request) => Promise<Response>) & {
|
|
|
126
127
|
ownersByUnit?(): Record<string, Record<string, DerivedOwner>>;
|
|
127
128
|
/** The vendor's sockets (sockets.ts), for the serve seam to upgrade to, when its manifest declares any. */
|
|
128
129
|
upgrade?: WebSocketUpgrade;
|
|
130
|
+
/** The vendor's RTMP ingest (live/ingest.ts), for its server to serve on its side port (`serveStream`), when its
|
|
131
|
+
* manifest declares `live`. */
|
|
132
|
+
ingest?: TwinStream;
|
|
129
133
|
maxRequestBodySize?: number;
|
|
130
134
|
/** The workspace screens it draws, its lanes' too (`GET /twin`'s `screens`). */
|
|
131
135
|
workspaces?(): Workspace[];
|
package/dist/src/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export { verifyEvent } from './events.js';
|
|
|
25
25
|
export { deriveStateSystem, deriveVendorStateSystem } from './derived-real.js';
|
|
26
26
|
export type { IngestDecl } from './derived-core.js';
|
|
27
27
|
export type { SocketDecl, SocketEngine, SocketSession } from './sockets.js';
|
|
28
|
+
export type { LiveAccess, LiveAdmission, LiveAudio, LiveDecl, LiveEngine, LivePublish, LiveRecording, LiveRefusal, LiveServeOptions, LiveVideo } from './live/types.js';
|
|
28
29
|
export { machinePool, isMachineCapabilityGap, type ExecSpec, type MachinePool, type MachineProcess, type MachineRun, type MachineSpec, type MachineStatus, type MachineFiles, type MachineFileStat } from './machines.js';
|
|
29
30
|
export type { CorsDecl } from './cors.js';
|
|
30
31
|
export type { Handler, HandlerContext, WriteHookContext } from './derived-core.js';
|
|
@@ -39,7 +40,7 @@ export { answerVendorErrors, RefusedWriteError } from './head.js';
|
|
|
39
40
|
export { GraphqlError, type GraphqlPart, type GraphqlResolver } from './graphql-wire.js';
|
|
40
41
|
export { VendorUnreachableError } from './vendor-call.js';
|
|
41
42
|
export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER } from './request-scope.js';
|
|
42
|
-
export { serveHttp, serveStream, type HttpHandler, type HttpServer, type ServeHttpOptions } from './serve-http.js';
|
|
43
|
+
export { serveHttp, serveStream, StreamListenerUnavailable, type HttpHandler, type HttpServer, type ServeHttpOptions } from './serve-http.js';
|
|
43
44
|
export type { TwinStream, TwinStreamConnection, TwinStreamSink } from './twin-fetch.js';
|
|
44
45
|
export { privateTransportKey } from './private-transport.js';
|
|
45
46
|
export { siteLabel, siteUrlOf, twinSiteUrl, twinVendorUrl } from './twin-fetch.js';
|
|
@@ -68,6 +69,7 @@ export { placeholderPng } from './placeholder-image.js';
|
|
|
68
69
|
export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4.js';
|
|
69
70
|
export * as redis from './redis/index.js';
|
|
70
71
|
export * as mysql from './mysql/index.js';
|
|
72
|
+
export * as live from './live/index.js';
|
|
71
73
|
export * as git from './git/index.js';
|
|
72
74
|
export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from './scenario.js';
|
|
73
75
|
export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.js';
|
package/dist/src/index.js
CHANGED
|
@@ -29,7 +29,7 @@ export { GraphqlError } from "./graphql-wire.js";
|
|
|
29
29
|
export { VendorUnreachableError } from "./vendor-call.js";
|
|
30
30
|
export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER } from "./request-scope.js";
|
|
31
31
|
// ── the serve factory ──────────────────────────────────────────────────────────────────────────
|
|
32
|
-
export { serveHttp, serveStream } from "./serve-http.js";
|
|
32
|
+
export { serveHttp, serveStream, StreamListenerUnavailable } from "./serve-http.js";
|
|
33
33
|
export { privateTransportKey } from "./private-transport.js";
|
|
34
34
|
// where a page links a hostname its twin answers, inside the World that shows it
|
|
35
35
|
export { siteLabel, siteUrlOf, twinSiteUrl, twinVendorUrl } from "./twin-fetch.js";
|
|
@@ -60,6 +60,7 @@ export { placeholderPng } from "./placeholder-image.js";
|
|
|
60
60
|
export { presignSigV4, signSigV4, verifySigV4 } from "./sigv4.js";
|
|
61
61
|
export * as redis from "./redis/index.js";
|
|
62
62
|
export * as mysql from "./mysql/index.js";
|
|
63
|
+
export * as live from "./live/index.js";
|
|
63
64
|
export * as git from "./git/index.js";
|
|
64
65
|
// ── the scenario grammar (architecture, "Behaviour") ───────────────────────────────────────────
|
|
65
66
|
export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from "./scenario.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type HlsSegment } from './hls.js';
|
|
2
|
+
import type { LiveAccess, LiveRecording } from './types.js';
|
|
3
|
+
/** The bookkeeping types a recording is kept in. */
|
|
4
|
+
export declare const LIVE = "_live";
|
|
5
|
+
export declare const LIVE_SEGMENT = "_live_segment";
|
|
6
|
+
/** Where a recording's bytes are kept among the pack's resource blobs. */
|
|
7
|
+
export declare const initBlob: (stream: string, init: number) => string;
|
|
8
|
+
export declare const segmentBlob: (stream: string, seq: number) => string;
|
|
9
|
+
type Row = Record<string, unknown>;
|
|
10
|
+
export type LiveReads = {
|
|
11
|
+
rows(type: string): Row[];
|
|
12
|
+
blob(key: string): Promise<Uint8Array | null>;
|
|
13
|
+
};
|
|
14
|
+
/** A recording's kept segments, in order. */
|
|
15
|
+
export declare function segmentsOf(reads: LiveReads, stream: string): Array<HlsSegment & {
|
|
16
|
+
start: number;
|
|
17
|
+
blob: string;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function recordingOf(row: Row, segments: ReadonlyArray<HlsSegment>): LiveRecording;
|
|
20
|
+
/** `ctx.live` over a request (the one a media host answers, for its Range and its method). */
|
|
21
|
+
export declare function liveAccess(reads: LiveReads, request: Request): LiveAccess;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// What a handler and a screen read of the World's recordings (`ctx.live`, architecture "Other wires": live media): a
|
|
2
|
+
// recording's facts from its bookkeeping subjects, and its playlists, init and segments answered as a media host
|
|
3
|
+
// answers them. The recording's rows are the tree's (`_live`, `_live_segment`); the bytes are the pack's resource blobs.
|
|
4
|
+
import { bytesResponse } from "../file-response.js";
|
|
5
|
+
import { masterPlaylist, mediaPlaylist } from "./hls.js";
|
|
6
|
+
/** The bookkeeping types a recording is kept in. */
|
|
7
|
+
export const LIVE = '_live';
|
|
8
|
+
export const LIVE_SEGMENT = '_live_segment';
|
|
9
|
+
/** Where a recording's bytes are kept among the pack's resource blobs. */
|
|
10
|
+
export const initBlob = (stream, init) => `live/${stream}/init-${init}.mp4`;
|
|
11
|
+
export const segmentBlob = (stream, seq) => `live/${stream}/seg-${seq}.m4s`;
|
|
12
|
+
/** A recording's kept segments, in order. */
|
|
13
|
+
export function segmentsOf(reads, stream) {
|
|
14
|
+
return reads.rows(LIVE_SEGMENT)
|
|
15
|
+
.filter((r) => r.stream === stream && r.deleted !== true)
|
|
16
|
+
.map((r) => ({ seq: Number(r.seq), init: Number(r.init), duration: Number(r.duration), size: Number(r.size), start: Number(r.start), blob: String(r.blob) }))
|
|
17
|
+
.sort((a, b) => a.seq - b.seq);
|
|
18
|
+
}
|
|
19
|
+
export function recordingOf(row, segments) {
|
|
20
|
+
return {
|
|
21
|
+
stream: String(row.id),
|
|
22
|
+
app: String(row.app ?? ''),
|
|
23
|
+
key: String(row.key ?? ''),
|
|
24
|
+
state: row.state === 'ended' ? 'ended' : 'publishing',
|
|
25
|
+
startedAt: String(row.startedAt ?? ''),
|
|
26
|
+
...(typeof row.endedAt === 'string' ? { endedAt: row.endedAt } : {}),
|
|
27
|
+
publishes: Number(row.publishes ?? 1),
|
|
28
|
+
segments: segments.length,
|
|
29
|
+
duration: Math.round(segments.reduce((n, s) => n + s.duration, 0) * 1000) / 1000,
|
|
30
|
+
...(row.video && typeof row.video === 'object' ? { video: row.video } : {}),
|
|
31
|
+
...(row.audio && typeof row.audio === 'object' ? { audio: row.audio } : {}),
|
|
32
|
+
...(row.metadata && typeof row.metadata === 'object' ? { metadata: row.metadata } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const CORS = { 'access-control-allow-origin': '*' };
|
|
36
|
+
const missing = () => new Response('Not Found', { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8', ...CORS } });
|
|
37
|
+
/** `ctx.live` over a request (the one a media host answers, for its Range and its method). */
|
|
38
|
+
export function liveAccess(reads, request) {
|
|
39
|
+
const rowOf = (stream) => reads.rows(LIVE).find((r) => r.id === stream && r.deleted !== true);
|
|
40
|
+
return {
|
|
41
|
+
recording(stream) {
|
|
42
|
+
const row = rowOf(stream);
|
|
43
|
+
return row ? recordingOf(row, segmentsOf(reads, stream)) : undefined;
|
|
44
|
+
},
|
|
45
|
+
recordings() {
|
|
46
|
+
return reads.rows(LIVE).filter((r) => r.deleted !== true).map((r) => recordingOf(r, segmentsOf(reads, String(r.id))));
|
|
47
|
+
},
|
|
48
|
+
async serve(stream, file, options = {}) {
|
|
49
|
+
const row = rowOf(stream);
|
|
50
|
+
if (!row)
|
|
51
|
+
return missing();
|
|
52
|
+
const segments = segmentsOf(reads, stream);
|
|
53
|
+
const ended = row.state === 'ended';
|
|
54
|
+
if (file === 'index.m3u8' || file === 'master.m3u8') {
|
|
55
|
+
// a recording with nothing playable yet has no playlist: a player waits, as for a stream not yet live
|
|
56
|
+
if (!segments.length)
|
|
57
|
+
return missing();
|
|
58
|
+
const recording = recordingOf(row, segments);
|
|
59
|
+
const uri = options.uri ?? ((f) => f);
|
|
60
|
+
const text = file === 'index.m3u8'
|
|
61
|
+
? mediaPlaylist({ state: recording.state, ...(typeof row.window === 'number' ? { window: row.window } : {}) }, segments, options)
|
|
62
|
+
: masterPlaylist(recording, segments, uri('index.m3u8'));
|
|
63
|
+
return new Response(request.method === 'HEAD' ? null : text, { headers: { 'content-type': 'application/vnd.apple.mpegurl', 'cache-control': ended ? 'public, max-age=3600' : 'no-cache, no-store', ...CORS } });
|
|
64
|
+
}
|
|
65
|
+
const init = /^init-(\d{1,9})\.mp4$/.exec(file);
|
|
66
|
+
const seg = /^seg-(\d{1,12})\.m4s$/.exec(file);
|
|
67
|
+
let key;
|
|
68
|
+
if (init && segments.some((s) => s.init === Number(init[1])))
|
|
69
|
+
key = initBlob(stream, Number(init[1]));
|
|
70
|
+
else if (seg)
|
|
71
|
+
key = segments.find((s) => s.seq === Number(seg[1]))?.blob;
|
|
72
|
+
if (!key)
|
|
73
|
+
return missing();
|
|
74
|
+
const bytes = await reads.blob(key);
|
|
75
|
+
if (!bytes)
|
|
76
|
+
return missing();
|
|
77
|
+
const answer = bytesResponse(request, bytes, 'video/mp4');
|
|
78
|
+
for (const [k, v] of Object.entries(CORS))
|
|
79
|
+
answer.headers.set(k, v);
|
|
80
|
+
return answer;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Amf0Value = number | boolean | string | null | undefined | Date | Amf0Value[] | {
|
|
2
|
+
[key: string]: Amf0Value;
|
|
3
|
+
};
|
|
4
|
+
/** Every value in `bytes`, in order; a value of a type AMF0 commands do not carry ends the read. */
|
|
5
|
+
export declare function decodeAmf0(bytes: Uint8Array): Amf0Value[];
|
|
6
|
+
/** `values` as AMF0, in order. */
|
|
7
|
+
export declare function encodeAmf0(...values: Amf0Value[]): Uint8Array;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// AMF0, the encoding of RTMP's commands and data messages (Adobe's AMF0 specification, as RTMP 1.0 §7 uses it): a
|
|
2
|
+
// command is a sequence of values (its name, a transaction id, an object, its arguments). Read: number, boolean,
|
|
3
|
+
// string, object, null, undefined, ECMA array, strict array, date, long string, typed object; a reference (0x07) is read
|
|
4
|
+
// as undefined, never followed, so no value read is cyclic (a publisher's metadata pointing back at itself); anything
|
|
5
|
+
// else stops the read where it is. Written: number, boolean, string, object (and a plain record), null, undefined.
|
|
6
|
+
const utf8 = new TextDecoder();
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
/** Every value in `bytes`, in order; a value of a type AMF0 commands do not carry ends the read. */
|
|
9
|
+
export function decodeAmf0(bytes) {
|
|
10
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
11
|
+
let at = 0;
|
|
12
|
+
const str = (length) => { const s = utf8.decode(bytes.subarray(at, at + length)); at += length; return s; };
|
|
13
|
+
const key = () => { const n = view.getUint16(at); at += 2; return str(n); };
|
|
14
|
+
const properties = (into) => {
|
|
15
|
+
for (;;) {
|
|
16
|
+
if (at + 3 > bytes.length)
|
|
17
|
+
return into;
|
|
18
|
+
if (view.getUint16(at) === 0 && bytes[at + 2] === 0x09) {
|
|
19
|
+
at += 3;
|
|
20
|
+
return into;
|
|
21
|
+
}
|
|
22
|
+
const name = key();
|
|
23
|
+
into[name] = value();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const value = () => {
|
|
27
|
+
const marker = bytes[at++];
|
|
28
|
+
switch (marker) {
|
|
29
|
+
case 0x00: {
|
|
30
|
+
const n = view.getFloat64(at);
|
|
31
|
+
at += 8;
|
|
32
|
+
return n;
|
|
33
|
+
}
|
|
34
|
+
case 0x01: return bytes[at++] !== 0;
|
|
35
|
+
case 0x02: return key();
|
|
36
|
+
case 0x03: return properties({});
|
|
37
|
+
case 0x05: return null;
|
|
38
|
+
case 0x06: return undefined;
|
|
39
|
+
case 0x07:
|
|
40
|
+
at += 2;
|
|
41
|
+
return undefined;
|
|
42
|
+
case 0x08:
|
|
43
|
+
at += 4;
|
|
44
|
+
return properties({});
|
|
45
|
+
case 0x0a: {
|
|
46
|
+
const n = view.getUint32(at);
|
|
47
|
+
at += 4;
|
|
48
|
+
const list = [];
|
|
49
|
+
for (let i = 0; i < n && at < bytes.length; i++)
|
|
50
|
+
list.push(value());
|
|
51
|
+
return list;
|
|
52
|
+
}
|
|
53
|
+
case 0x0b: {
|
|
54
|
+
const ms = view.getFloat64(at);
|
|
55
|
+
at += 10;
|
|
56
|
+
return new Date(ms);
|
|
57
|
+
}
|
|
58
|
+
case 0x0c: {
|
|
59
|
+
const n = view.getUint32(at);
|
|
60
|
+
at += 4;
|
|
61
|
+
return str(n);
|
|
62
|
+
}
|
|
63
|
+
case 0x10:
|
|
64
|
+
key();
|
|
65
|
+
return properties({});
|
|
66
|
+
default: throw new Amf0End();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const out = [];
|
|
70
|
+
while (at < bytes.length) {
|
|
71
|
+
try {
|
|
72
|
+
out.push(value());
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof Amf0End || error instanceof RangeError)
|
|
76
|
+
break;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
class Amf0End extends Error {
|
|
83
|
+
}
|
|
84
|
+
/** `values` as AMF0, in order. */
|
|
85
|
+
export function encodeAmf0(...values) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
const u16 = (n) => { parts.push((n >> 8) & 0xff, n & 0xff); };
|
|
88
|
+
const text = (s) => { const b = encoder.encode(s); u16(b.length); for (const x of b)
|
|
89
|
+
parts.push(x); };
|
|
90
|
+
const write = (v) => {
|
|
91
|
+
if (typeof v === 'number') {
|
|
92
|
+
parts.push(0x00);
|
|
93
|
+
const b = new Uint8Array(8);
|
|
94
|
+
new DataView(b.buffer).setFloat64(0, v);
|
|
95
|
+
for (const x of b)
|
|
96
|
+
parts.push(x);
|
|
97
|
+
}
|
|
98
|
+
else if (typeof v === 'boolean')
|
|
99
|
+
parts.push(0x01, v ? 1 : 0);
|
|
100
|
+
else if (typeof v === 'string') {
|
|
101
|
+
parts.push(0x02);
|
|
102
|
+
text(v);
|
|
103
|
+
}
|
|
104
|
+
else if (v === null)
|
|
105
|
+
parts.push(0x05);
|
|
106
|
+
else if (v === undefined)
|
|
107
|
+
parts.push(0x06);
|
|
108
|
+
else if (v instanceof Date || Array.isArray(v))
|
|
109
|
+
throw new Error('amf0: this writer writes no dates or arrays');
|
|
110
|
+
else {
|
|
111
|
+
parts.push(0x03);
|
|
112
|
+
for (const [k, x] of Object.entries(v)) {
|
|
113
|
+
text(k);
|
|
114
|
+
write(x);
|
|
115
|
+
}
|
|
116
|
+
parts.push(0x00, 0x00, 0x09);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
for (const v of values)
|
|
120
|
+
write(v);
|
|
121
|
+
return Uint8Array.from(parts);
|
|
122
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type AvcConfig = {
|
|
2
|
+
/** the AVCDecoderConfigurationRecord as sent, which an `avcC` box holds as is (ISO/IEC 14496-15 §5.3.3.1) */
|
|
3
|
+
record: Uint8Array;
|
|
4
|
+
/** the RFC 6381 codec string: `avc1.` and the profile, constraint flags and level, in hex */
|
|
5
|
+
codec: string;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
export type AacConfig = {
|
|
10
|
+
/** the AudioSpecificConfig as sent, which an `esds` box's DecoderSpecificInfo holds as is */
|
|
11
|
+
asc: Uint8Array;
|
|
12
|
+
codec: string;
|
|
13
|
+
objectType: number;
|
|
14
|
+
sampleRate: number;
|
|
15
|
+
channels: number;
|
|
16
|
+
};
|
|
17
|
+
export type VideoTag = {
|
|
18
|
+
kind: 'config';
|
|
19
|
+
config: AvcConfig;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'frame';
|
|
22
|
+
key: boolean;
|
|
23
|
+
cts: number;
|
|
24
|
+
data: Uint8Array;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'other';
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'unsupported';
|
|
29
|
+
codec: string;
|
|
30
|
+
};
|
|
31
|
+
export type AudioTag = {
|
|
32
|
+
kind: 'config';
|
|
33
|
+
config: AacConfig;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'frame';
|
|
36
|
+
data: Uint8Array;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'unsupported';
|
|
39
|
+
codec: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function readVideoTag(p: Uint8Array): VideoTag;
|
|
42
|
+
export declare function readAudioTag(p: Uint8Array): AudioTag;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// An FLV tag's body as RTMP carries it (FLV 10.1, Annex E.4.2 and E.4.3): video as AVC (codec 7, its packet type 0 the
|
|
2
|
+
// AVCDecoderConfigurationRecord, 1 length-prefixed NAL units with a composition time offset), audio as AAC (format 10, its
|
|
3
|
+
// packet type 0 the AudioSpecificConfig, 1 a raw frame). Anything else is named as the codec it is and not read: other
|
|
4
|
+
// codecs, and Enhanced RTMP's ex-header (its first byte's high bit), which carries HEVC, AV1 and multitrack video.
|
|
5
|
+
const VIDEO_CODECS = { 2: 'Sorenson H.263', 3: 'Screen video', 4: 'On2 VP6', 5: 'On2 VP6 with alpha', 6: 'Screen video 2', 12: 'HEVC', 13: 'AV1' };
|
|
6
|
+
const AUDIO_FORMATS = { 0: 'linear PCM', 1: 'ADPCM', 2: 'MP3', 3: 'linear PCM (little-endian)', 4: 'Nellymoser 16 kHz', 5: 'Nellymoser 8 kHz', 6: 'Nellymoser', 7: 'G.711 A-law', 8: 'G.711 mu-law', 11: 'Speex', 14: 'MP3 8 kHz', 15: 'device-specific' };
|
|
7
|
+
const hex2 = (n) => n.toString(16).padStart(2, '0');
|
|
8
|
+
export function readVideoTag(p) {
|
|
9
|
+
if (p.length < 1)
|
|
10
|
+
return { kind: 'other' };
|
|
11
|
+
if (p[0] & 0x80) {
|
|
12
|
+
const fourcc = p.length >= 5 ? String.fromCharCode(p[1], p[2], p[3], p[4]) : '?';
|
|
13
|
+
return { kind: 'unsupported', codec: `Enhanced RTMP ${fourcc}` };
|
|
14
|
+
}
|
|
15
|
+
const frameType = p[0] >> 4;
|
|
16
|
+
const codec = p[0] & 0x0f;
|
|
17
|
+
if (codec !== 7)
|
|
18
|
+
return { kind: 'unsupported', codec: VIDEO_CODECS[codec] ?? `video codec ${codec}` };
|
|
19
|
+
// a video info/command frame (frame type 5) carries no picture
|
|
20
|
+
if (frameType === 5 || p.length < 5)
|
|
21
|
+
return { kind: 'other' };
|
|
22
|
+
const packetType = p[1];
|
|
23
|
+
const cts = ((p[2] << 16) | (p[3] << 8) | p[4]) << 8 >> 8;
|
|
24
|
+
const data = p.subarray(5);
|
|
25
|
+
if (packetType === 0)
|
|
26
|
+
return { kind: 'config', config: avcConfig(data) };
|
|
27
|
+
if (packetType === 1)
|
|
28
|
+
return { kind: 'frame', key: frameType === 1, cts, data };
|
|
29
|
+
return { kind: 'other' };
|
|
30
|
+
}
|
|
31
|
+
export function readAudioTag(p) {
|
|
32
|
+
if (p.length < 2)
|
|
33
|
+
return { kind: 'frame', data: new Uint8Array(0) };
|
|
34
|
+
const format = p[0] >> 4;
|
|
35
|
+
if (format !== 10)
|
|
36
|
+
return { kind: 'unsupported', codec: AUDIO_FORMATS[format] ?? `audio format ${format}` };
|
|
37
|
+
if (p[1] === 0)
|
|
38
|
+
return { kind: 'config', config: aacConfig(p.subarray(2)) };
|
|
39
|
+
return { kind: 'frame', data: p.subarray(2) };
|
|
40
|
+
}
|
|
41
|
+
function avcConfig(record) {
|
|
42
|
+
const copy = record.slice();
|
|
43
|
+
const codec = copy.length >= 4 ? `avc1.${hex2(copy[1])}${hex2(copy[2])}${hex2(copy[3])}` : 'avc1.42e01e';
|
|
44
|
+
let width = 0;
|
|
45
|
+
let height = 0;
|
|
46
|
+
// the first SPS: after the version, profile, compatibility, level, length size and SPS count
|
|
47
|
+
if (copy.length > 8 && (copy[5] & 0x1f) > 0) {
|
|
48
|
+
const length = (copy[6] << 8) | copy[7];
|
|
49
|
+
try {
|
|
50
|
+
({ width, height } = spsSize(copy.subarray(8, 8 + length)));
|
|
51
|
+
}
|
|
52
|
+
catch { /* the metadata's size stands in */ }
|
|
53
|
+
}
|
|
54
|
+
return { record: copy, codec, width, height };
|
|
55
|
+
}
|
|
56
|
+
function aacConfig(asc) {
|
|
57
|
+
const copy = asc.slice();
|
|
58
|
+
const RATES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
|
|
59
|
+
const bits = new Bits(copy);
|
|
60
|
+
let objectType = bits.u(5);
|
|
61
|
+
if (objectType === 31)
|
|
62
|
+
objectType = 32 + bits.u(6);
|
|
63
|
+
const index = bits.u(4);
|
|
64
|
+
const sampleRate = index === 15 ? bits.u(24) : RATES[index] ?? 44100;
|
|
65
|
+
const channels = bits.u(4);
|
|
66
|
+
return { asc: copy, codec: `mp4a.40.${objectType}`, objectType, sampleRate, channels };
|
|
67
|
+
}
|
|
68
|
+
/** A bit reader over bytes, MSB first; a read past the end reads zeros. */
|
|
69
|
+
class Bits {
|
|
70
|
+
bytes;
|
|
71
|
+
at = 0;
|
|
72
|
+
constructor(bytes) {
|
|
73
|
+
this.bytes = bytes;
|
|
74
|
+
}
|
|
75
|
+
bit() { const byte = this.bytes[this.at >> 3] ?? 0; const v = (byte >> (7 - (this.at & 7))) & 1; this.at++; return v; }
|
|
76
|
+
u(n) { let v = 0; for (let i = 0; i < n; i++)
|
|
77
|
+
v = v * 2 + this.bit(); return v; }
|
|
78
|
+
ue() { let zeros = 0; while (this.bit() === 0) {
|
|
79
|
+
if (++zeros > 31)
|
|
80
|
+
throw new Error('exp-Golomb code too long');
|
|
81
|
+
} return 2 ** zeros - 1 + this.u(zeros); }
|
|
82
|
+
se() { const k = this.ue(); return k % 2 ? (k + 1) / 2 : -(k / 2); }
|
|
83
|
+
}
|
|
84
|
+
/** A sequence parameter set's picture size (H.264 §7.3.2.1.1), its cropping applied. */
|
|
85
|
+
function spsSize(nal) {
|
|
86
|
+
// the NAL unit's RBSP: its header byte off, each emulation-prevention byte (00 00 03) removed
|
|
87
|
+
const rbsp = [];
|
|
88
|
+
for (let i = 1; i < nal.length; i++) {
|
|
89
|
+
if (i >= 3 && nal[i] === 3 && nal[i - 1] === 0 && nal[i - 2] === 0)
|
|
90
|
+
continue;
|
|
91
|
+
rbsp.push(nal[i]);
|
|
92
|
+
}
|
|
93
|
+
const b = new Bits(Uint8Array.from(rbsp));
|
|
94
|
+
const profile = b.u(8);
|
|
95
|
+
b.u(16);
|
|
96
|
+
b.ue();
|
|
97
|
+
let chroma = 1;
|
|
98
|
+
if ([100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135].includes(profile)) {
|
|
99
|
+
chroma = b.ue();
|
|
100
|
+
if (chroma === 3)
|
|
101
|
+
b.bit();
|
|
102
|
+
b.ue();
|
|
103
|
+
b.ue();
|
|
104
|
+
b.bit();
|
|
105
|
+
if (b.bit()) {
|
|
106
|
+
for (let i = 0; i < (chroma === 3 ? 12 : 8); i++) {
|
|
107
|
+
if (!b.bit())
|
|
108
|
+
continue;
|
|
109
|
+
let last = 8;
|
|
110
|
+
let next = 8;
|
|
111
|
+
for (let j = 0; j < (i < 6 ? 16 : 64); j++) {
|
|
112
|
+
if (next !== 0)
|
|
113
|
+
next = (last + b.se() + 256) % 256;
|
|
114
|
+
last = next === 0 ? last : next;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
b.ue();
|
|
120
|
+
const poc = b.ue();
|
|
121
|
+
if (poc === 0)
|
|
122
|
+
b.ue();
|
|
123
|
+
else if (poc === 1) {
|
|
124
|
+
b.bit();
|
|
125
|
+
b.se();
|
|
126
|
+
b.se();
|
|
127
|
+
const n = b.ue();
|
|
128
|
+
for (let i = 0; i < n; i++)
|
|
129
|
+
b.se();
|
|
130
|
+
}
|
|
131
|
+
b.ue();
|
|
132
|
+
b.bit();
|
|
133
|
+
const widthMbs = b.ue() + 1;
|
|
134
|
+
const heightUnits = b.ue() + 1;
|
|
135
|
+
const frameMbsOnly = b.bit();
|
|
136
|
+
if (!frameMbsOnly)
|
|
137
|
+
b.bit();
|
|
138
|
+
b.bit();
|
|
139
|
+
let crop = [0, 0, 0, 0];
|
|
140
|
+
if (b.bit())
|
|
141
|
+
crop = [b.ue(), b.ue(), b.ue(), b.ue()];
|
|
142
|
+
const unitX = chroma === 1 || chroma === 2 ? 2 : 1;
|
|
143
|
+
const unitY = (chroma === 1 ? 2 : 1) * (2 - frameMbsOnly);
|
|
144
|
+
return {
|
|
145
|
+
width: widthMbs * 16 - (crop[0] + crop[1]) * unitX,
|
|
146
|
+
height: (2 - frameMbsOnly) * heightUnits * 16 - (crop[2] + crop[3]) * unitY,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AacConfig, AvcConfig } from './flv.js';
|
|
2
|
+
export declare const VIDEO_TRACK = 1;
|
|
3
|
+
export declare const AUDIO_TRACK = 2;
|
|
4
|
+
export declare const VIDEO_TIMESCALE = 90000;
|
|
5
|
+
/** The init segment for the tracks a publish carries. */
|
|
6
|
+
export declare function initSegment(tracks: {
|
|
7
|
+
video?: AvcConfig & {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
audio?: AacConfig;
|
|
12
|
+
}): Uint8Array;
|
|
13
|
+
export type Sample = {
|
|
14
|
+
duration: number;
|
|
15
|
+
data: Uint8Array;
|
|
16
|
+
key: boolean;
|
|
17
|
+
cts?: number;
|
|
18
|
+
};
|
|
19
|
+
export type Fragment = {
|
|
20
|
+
track: number;
|
|
21
|
+
baseTime: number;
|
|
22
|
+
samples: Sample[];
|
|
23
|
+
};
|
|
24
|
+
/** A media segment: one `moof` over the fragments, then their samples in one `mdat`, video's first. */
|
|
25
|
+
export declare function mediaSegment(sequence: number, fragments: Fragment[]): Uint8Array;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const VIDEO_TRACK = 1;
|
|
2
|
+
export const AUDIO_TRACK = 2;
|
|
3
|
+
export const VIDEO_TIMESCALE = 90_000;
|
|
4
|
+
const ascii = (s) => Array.from(s, (c) => c.charCodeAt(0));
|
|
5
|
+
const u16 = (n) => [(n >> 8) & 0xff, n & 0xff];
|
|
6
|
+
const u32 = (n) => [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff];
|
|
7
|
+
const u64 = (n) => [...u32(Math.floor(n / 2 ** 32)), ...u32(n % 2 ** 32)];
|
|
8
|
+
const zeros = (n) => new Array(n).fill(0);
|
|
9
|
+
function box(type, ...parts) {
|
|
10
|
+
const size = 8 + parts.reduce((n, p) => n + p.length, 0);
|
|
11
|
+
const out = new Uint8Array(size);
|
|
12
|
+
out.set(u32(size), 0);
|
|
13
|
+
out.set(ascii(type), 4);
|
|
14
|
+
let at = 8;
|
|
15
|
+
for (const p of parts) {
|
|
16
|
+
out.set(p, at);
|
|
17
|
+
at += p.length;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
const full = (type, version, flags, ...parts) => box(type, [version, (flags >> 16) & 0xff, (flags >> 8) & 0xff, flags & 0xff], ...parts);
|
|
22
|
+
const MATRIX = [...u32(0x00010000), ...u32(0), ...u32(0), ...u32(0), ...u32(0x00010000), ...u32(0), ...u32(0), ...u32(0), ...u32(0x40000000)];
|
|
23
|
+
function descriptor(tag, ...parts) {
|
|
24
|
+
const body = parts.flatMap((p) => Array.from(p));
|
|
25
|
+
// the size as four bytes of seven bits, as MP4 writers write it
|
|
26
|
+
const n = body.length;
|
|
27
|
+
return [tag, 0x80 | ((n >> 21) & 0x7f), 0x80 | ((n >> 14) & 0x7f), 0x80 | ((n >> 7) & 0x7f), n & 0x7f, ...body];
|
|
28
|
+
}
|
|
29
|
+
function trak(id, kind, timescale, width, height, entry) {
|
|
30
|
+
const tkhd = full('tkhd', 0, 3, u32(0), u32(0), u32(id), u32(0), u32(0), zeros(8), u16(0), u16(0), u16(kind === 'audio' ? 0x0100 : 0), u16(0), MATRIX, u32(width * 65536), u32(height * 65536));
|
|
31
|
+
const mdhd = full('mdhd', 0, 0, u32(0), u32(0), u32(timescale), u32(0), u16(0x55c4), u16(0));
|
|
32
|
+
const hdlr = full('hdlr', 0, 0, u32(0), ascii(kind === 'video' ? 'vide' : 'soun'), zeros(12), [...ascii(kind === 'video' ? 'VideoHandler' : 'SoundHandler'), 0]);
|
|
33
|
+
const header = kind === 'video' ? full('vmhd', 0, 1, u16(0), zeros(6)) : full('smhd', 0, 0, u16(0), u16(0));
|
|
34
|
+
const dinf = box('dinf', full('dref', 0, 0, u32(1), full('url ', 0, 1)));
|
|
35
|
+
const stbl = box('stbl', full('stsd', 0, 0, u32(1), entry), full('stts', 0, 0, u32(0)), full('stsc', 0, 0, u32(0)), full('stsz', 0, 0, u32(0), u32(0)), full('stco', 0, 0, u32(0)));
|
|
36
|
+
return box('trak', tkhd, box('mdia', mdhd, hdlr, box('minf', header, dinf, stbl)));
|
|
37
|
+
}
|
|
38
|
+
/** The init segment for the tracks a publish carries. */
|
|
39
|
+
export function initSegment(tracks) {
|
|
40
|
+
const traks = [];
|
|
41
|
+
const trex = [];
|
|
42
|
+
const brands = ['isom', 'iso6', ...(tracks.video ? ['avc1'] : []), 'mp41'];
|
|
43
|
+
if (tracks.video) {
|
|
44
|
+
const v = tracks.video;
|
|
45
|
+
const avc1 = box('avc1', zeros(6), u16(1), u16(0), u16(0), zeros(12), u16(v.width), u16(v.height), u32(0x00480000), u32(0x00480000), u32(0), u16(1), zeros(32), u16(0x0018), u16(0xffff), box('avcC', v.record));
|
|
46
|
+
traks.push(trak(VIDEO_TRACK, 'video', VIDEO_TIMESCALE, v.width, v.height, avc1));
|
|
47
|
+
trex.push(full('trex', 0, 0, u32(VIDEO_TRACK), u32(1), u32(0), u32(0), u32(0)));
|
|
48
|
+
}
|
|
49
|
+
if (tracks.audio) {
|
|
50
|
+
const a = tracks.audio;
|
|
51
|
+
const esds = full('esds', 0, 0, descriptor(3, u16(0), [0], descriptor(4, [0x40, 0x15], zeros(3), u32(0), u32(0), descriptor(5, a.asc)), descriptor(6, [0x02])));
|
|
52
|
+
const mp4a = box('mp4a', zeros(6), u16(1), zeros(8), u16(Math.max(1, a.channels)), u16(16), u16(0), u16(0), u32((a.sampleRate > 0xffff ? 0 : a.sampleRate) * 65536), esds);
|
|
53
|
+
traks.push(trak(AUDIO_TRACK, 'audio', a.sampleRate, 0, 0, mp4a));
|
|
54
|
+
trex.push(full('trex', 0, 0, u32(AUDIO_TRACK), u32(1), u32(0), u32(0), u32(0)));
|
|
55
|
+
}
|
|
56
|
+
const mvhd = full('mvhd', 0, 0, u32(0), u32(0), u32(1000), u32(0), u32(0x00010000), u16(0x0100), u16(0), zeros(8), MATRIX, zeros(24), u32(3));
|
|
57
|
+
const ftyp = box('ftyp', ascii('isom'), u32(0x200), ...brands.map(ascii));
|
|
58
|
+
const moov = box('moov', mvhd, ...traks, box('mvex', ...trex));
|
|
59
|
+
const out = new Uint8Array(ftyp.length + moov.length);
|
|
60
|
+
out.set(ftyp, 0);
|
|
61
|
+
out.set(moov, ftyp.length);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** A media segment: one `moof` over the fragments, then their samples in one `mdat`, video's first. */
|
|
65
|
+
export function mediaSegment(sequence, fragments) {
|
|
66
|
+
const present = fragments.filter((f) => f.samples.length > 0);
|
|
67
|
+
const moofOf = (offsets) => box('moof', full('mfhd', 0, 0, u32(sequence)), ...present.map((f, i) => {
|
|
68
|
+
const video = f.track === VIDEO_TRACK;
|
|
69
|
+
// data offset, duration, size, flags and (video) composition offset per sample; version 1 for signed offsets
|
|
70
|
+
const flags = video ? 0x000f01 : 0x000301;
|
|
71
|
+
const rows = f.samples.flatMap((s) => [
|
|
72
|
+
...u32(s.duration), ...u32(s.data.length),
|
|
73
|
+
...(video ? [...u32(s.key ? 0x02000000 : 0x01010000), ...u32((s.cts ?? 0) | 0)] : []),
|
|
74
|
+
]);
|
|
75
|
+
return box('traf', full('tfhd', 0, 0x020000, u32(f.track)), full('tfdt', 1, 0, u64(f.baseTime)), full('trun', video ? 1 : 0, flags, u32(f.samples.length), u32(offsets[i] | 0), rows));
|
|
76
|
+
}));
|
|
77
|
+
const sized = moofOf(present.map(() => 0)).length;
|
|
78
|
+
const offsets = [];
|
|
79
|
+
let at = sized + 8;
|
|
80
|
+
for (const f of present) {
|
|
81
|
+
offsets.push(at);
|
|
82
|
+
at += f.samples.reduce((n, s) => n + s.data.length, 0);
|
|
83
|
+
}
|
|
84
|
+
const moof = moofOf(offsets);
|
|
85
|
+
// one buffer for the whole segment, the samples written straight into its mdat: a segment's bytes are copied once
|
|
86
|
+
const out = new Uint8Array(at);
|
|
87
|
+
out.set(moof, 0);
|
|
88
|
+
out.set([...u32(at - moof.length), ...ascii('mdat')], moof.length);
|
|
89
|
+
let to = moof.length + 8;
|
|
90
|
+
for (const f of present)
|
|
91
|
+
for (const s of f.samples) {
|
|
92
|
+
out.set(s.data, to);
|
|
93
|
+
to += s.data.length;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|