@torrent-tv/proxy 2.90.0 → 2.92.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/bin/cli.js +5 -2
- package/package.json +18 -2
- package/routes/api/playback-plan/post.js +21 -4
- package/routes/api/sources/files/get.js +20 -28
- package/routes/api/sources/post.js +4 -2
- package/routes/api/sources/stats/get.js +4 -11
- package/routes/api/sources/viewer/post.js +16 -0
- package/routes/api/sources/warm/post.js +6 -215
- package/routes/api/subtitles/get.js +22 -125
- package/routes/api/subtitles/post.js +26 -0
- package/routes/api/transcode-sessions/post.js +24 -2
- package/routes/media/get.js +80 -0
- package/routes/stream/get.js +26 -94
- package/routes/transcode/audio-warm/get.js +9 -21
- package/routes/transcode/variant-warm/get.js +8 -14
- package/server.js +410 -46
- package/services/encode/AdmittedInput.js +136 -0
- package/services/encode/CushionReport.js +1 -76
- package/services/encode/EncodeInputs.js +142 -0
- package/services/encode/EncodeOrchestrator.js +4 -1
- package/services/encode/EncodeRun.js +13 -4
- package/services/encode/EncodeRuns.js +132 -52
- package/services/encode/InputFailures.js +37 -0
- package/services/encode/MatroskaInput.js +158 -0
- package/services/encode/OutputOpening.js +16 -64
- package/services/encode/Renditions.js +1 -17
- package/services/encode/admitted-command.js +60 -0
- package/services/encode/annex-b-input.js +37 -0
- package/services/encode/bit-payload.js +29 -0
- package/services/encode/output/OutputCatalog.js +1 -1
- package/services/encode/output/Timeline.js +11 -0
- package/services/encode/pcm-normalization.js +30 -0
- package/services/encode/piece-completeness.js +54 -1
- package/services/encode/quality/HostLoad.js +19 -73
- package/services/encode/quality/QualityOffer.js +1 -1
- package/services/encode/run-command.js +1 -589
- package/services/encode/segment-formats/mp4-boxes.js +3 -1
- package/services/encode/segment-formats/presentation-segment.js +114 -0
- package/services/media/ContainerOrchestrator.js +118 -48
- package/services/media/KeyframeTables.js +16 -115
- package/services/media/MediaReadRequests.js +120 -0
- package/services/media/ProbeRequests.js +135 -0
- package/services/media/SegmentInputs.js +48 -0
- package/services/media/SourceFile.js +6 -0
- package/services/media/SubtitleOrchestrator.js +190 -4
- package/services/media/await-plan.js +47 -0
- package/services/media/container/AsfContainer.js +217 -0
- package/services/media/container/AviContainer.js +343 -34
- package/services/media/container/CadencedPictureOrder.js +45 -0
- package/services/media/container/Container.js +7 -3
- package/services/media/container/ContainerFactory.js +10 -3
- package/services/media/container/FfprobeContainer.js +222 -0
- package/services/media/container/MatroskaContainer.js +72 -46
- package/services/media/container/Mp4Container.js +472 -139
- package/services/media/container/MpegPsContainer.js +281 -0
- package/services/media/container/MpegTsContainer.js +277 -0
- package/services/media/container/PacketIndex.js +270 -0
- package/services/media/container/PacketRecords.js +237 -0
- package/services/media/container/RetainedBytes.js +39 -0
- package/services/media/container/RetainedReads.js +51 -0
- package/services/media/container/ac3-frame.js +46 -0
- package/services/media/container/asf-packets.js +153 -0
- package/services/media/container/avc-picture-order.js +50 -0
- package/services/media/container/avi-open-dml.js +47 -0
- package/services/media/container/dts-frame.js +51 -0
- package/services/media/container/ebml-stream.js +12 -1
- package/services/media/container/ffprobe-record.js +45 -0
- package/services/media/container/flv-packet-ranges.js +27 -0
- package/services/media/container/following-packet-time.js +37 -0
- package/services/media/container/h264-configuration.js +183 -0
- package/services/media/container/h264-elementary-index.js +245 -0
- package/services/media/container/hevc-configuration.js +165 -0
- package/services/media/container/hevc-elementary-index.js +132 -0
- package/services/media/container/hevc-picture-order.js +66 -0
- package/services/media/container/latm-frame.js +99 -0
- package/services/media/container/matroska-packets.js +207 -0
- package/services/media/container/memory-unavailable.js +8 -0
- package/services/media/container/mp3-packet-dependencies.js +41 -0
- package/services/media/container/mp4-fragments.js +148 -0
- package/services/media/container/mpeg-audio-frame.js +19 -0
- package/services/media/container/mpeg-audio-reservoir.js +27 -0
- package/services/media/container/mpeg-elementary-index.js +196 -0
- package/services/media/container/mpeg-sequence-reader.js +69 -0
- package/services/media/container/mpeg-ts-packets.js +93 -0
- package/services/media/container/mpeg-video-configuration.js +50 -0
- package/services/media/container/mpeg4-picture-timing.js +108 -0
- package/services/media/container/pes-header.js +34 -0
- package/services/media/playback-declarations.js +57 -0
- package/services/media/playback-planner.js +72 -230
- package/services/media/probe-packets.js +61 -0
- package/services/media/read-playback-declarations.js +18 -0
- package/services/media/tracks/AudioTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +4 -1
- package/services/server/OutputLifecycle.js +2 -0
- package/services/server/SegmentServing.js +40 -33
- package/services/server/ViewerRequests.js +22 -41
- package/services/server/controllers/PlaybackController.js +4 -0
- package/services/server/controllers/SubtitleController.js +8 -29
- package/services/server/transcode-session-files.js +58 -99
- package/services/server/wire-outputs.js +58 -15
- package/services/storage/IndexMemory.js +85 -0
- package/services/storage/MachineBudget.js +27 -3
- package/services/storage/allowance.js +8 -1
- package/services/storage/piece-store/piece-disk-store.js +18 -3
- package/services/storage/piece-store/piece-lru.js +8 -4
- package/services/storage/piece-store/shared-piece-store.js +26 -11
- package/services/storage/segment-store/SegmentStore.js +49 -18
- package/services/storage/wire.js +7 -0
- package/services/torrent/Contents.js +52 -20
- package/services/torrent/demand/DemandRegister.js +13 -4
- package/services/torrent/demand/Window.js +13 -2
- package/services/torrent/demand-progress.js +25 -0
- package/services/torrent/download/DeadlineScheduler.js +167 -0
- package/services/torrent/download/FutureDownload.js +166 -0
- package/services/torrent/download/SwarmSelection.js +30 -59
- package/services/torrent/download/registry.js +45 -5
- package/services/torrent/download/withdraw-claim.js +2 -0
- package/services/torrent/torrent-pool.js +39 -600
- package/services/torrent/worker/channel.js +8 -1
- package/services/torrent/worker/client.js +74 -72
- package/services/torrent/worker/files-in-use.js +9 -0
- package/services/torrent/worker/handle-state.js +12 -0
- package/services/torrent/worker/held-bytes.js +82 -99
- package/services/torrent/worker/held-file-bytes.js +17 -0
- package/services/torrent/worker/piece-reader.js +67 -683
- package/services/torrent/worker/pool-adapter.js +37 -123
- package/services/torrent/worker/protocol.js +6 -17
- package/services/torrent/worker/whole-file-stats.js +2 -0
- package/services/torrent/worker/worker.js +86 -156
- package/services/transport/data-channel-handler.js +56 -13
- package/services/transport/tunnel-client.js +111 -20
- package/services/viewer/DownloadMaps.js +168 -0
- package/services/viewer/NativeSourceMap.js +33 -0
- package/services/viewer/PriorityMap.js +49 -40
- package/services/viewer/PriorityOrchestrator.js +42 -13
- package/services/viewer/SourcePreparation.js +278 -0
- package/services/viewer/Viewer.js +11 -0
- package/services/viewer/Viewers.js +109 -2
- package/services/viewer/playback-readiness.js +50 -63
- package/services/viewer/segment-demands.js +37 -0
- package/services/viewer/urgent-output-ready.js +26 -0
- package/services/encode/run-inputs.js +0 -53
- package/services/media/keyframe-probe.js +0 -157
- package/services/media/media-info-probe.js +0 -93
- package/services/torrent/worker/background-fill.js +0 -213
- package/services/torrent/worker/resume-warm.js +0 -107
|
@@ -14,8 +14,12 @@ import { ENCODE_RUN_STATE, liveRunsOf, runStateOf, wireState } from "./encode-ru
|
|
|
14
14
|
import { ENCODE_EXIT } from "./encode-exit.js";
|
|
15
15
|
import { EncodeRun } from "./EncodeRun.js";
|
|
16
16
|
import { computeOutputDimensions } from "./args.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { buildAdmittedCommand } from "./admitted-command.js";
|
|
18
|
+
import { writeAdmittedInput } from "./AdmittedInput.js";
|
|
19
|
+
import { cutOf, judgeNeighbors, judgePiece } from "./piece-completeness.js";
|
|
20
|
+
import { presentationSegment } from "./segment-formats/presentation-segment.js";
|
|
21
|
+
import { InputFailures, failedAdmittedInput } from "./InputFailures.js";
|
|
22
|
+
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* @typedef {Object} SegmentFiles
|
|
@@ -158,6 +162,7 @@ export class EncodeRuns {
|
|
|
158
162
|
#runsByInputToken = new Map();
|
|
159
163
|
|
|
160
164
|
#lastInputToken = 0;
|
|
165
|
+
#inputFailures = new InputFailures();
|
|
161
166
|
/** What this reads and asks of the rest of the proxy, and nothing else. @type {object} */
|
|
162
167
|
#host;
|
|
163
168
|
|
|
@@ -185,7 +190,6 @@ export class EncodeRuns {
|
|
|
185
190
|
let state = this.#inputState.get(output);
|
|
186
191
|
if (!state) {
|
|
187
192
|
state = {
|
|
188
|
-
readWindowBytes: 0,
|
|
189
193
|
inputRetryCount: 0,
|
|
190
194
|
backwardRestarts: { count: 0, segmentsBack: 0, worstBack: 0, remade: 0 },
|
|
191
195
|
firstWantedAt: new Map(),
|
|
@@ -199,14 +203,6 @@ export class EncodeRuns {
|
|
|
199
203
|
return state;
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
setReadWindow(output, bytes) {
|
|
203
|
-
this.#inputStateFor(output).readWindowBytes = Number.isFinite(bytes) ? bytes : 0;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
readWindowFor(output) {
|
|
207
|
-
return this.#inputStateFor(output).readWindowBytes;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
206
|
noteWanted(output, index, at = Date.now()) {
|
|
211
207
|
const wanted = this.#inputStateFor(output).firstWantedAt;
|
|
212
208
|
if (!wanted.has(index)) wanted.set(index, at);
|
|
@@ -289,10 +285,16 @@ export class EncodeRuns {
|
|
|
289
285
|
}
|
|
290
286
|
|
|
291
287
|
runStateOf(output) {
|
|
288
|
+
if (this.#host.encodeInputs?.failureOf(output)) return ENCODE_RUN_STATE.ENDED_FAILED;
|
|
289
|
+
if (output && this.liveRunsOf(output).length === 0 && this.#inputFailures.reason(output.outputKey)) return ENCODE_RUN_STATE.ENDED_FAILED;
|
|
292
290
|
return output ? this.#host.encodeOrchestrator.stateOf(output.outputKey) : runStateOf([]);
|
|
293
291
|
}
|
|
294
292
|
|
|
295
293
|
lastErrorOf(output) {
|
|
294
|
+
const failedInput = this.#host.encodeInputs?.failureOf(output);
|
|
295
|
+
if (failedInput) return `${failedInput.reason}: ${failedInput.message ?? ""}`;
|
|
296
|
+
const failed = output && this.#inputFailures.reason(output.outputKey);
|
|
297
|
+
if (failed) return failed;
|
|
296
298
|
return output ? this.#host.encodeOrchestrator.errorOf(output.outputKey) : "";
|
|
297
299
|
}
|
|
298
300
|
|
|
@@ -346,24 +348,46 @@ export class EncodeRuns {
|
|
|
346
348
|
* @param {number} index
|
|
347
349
|
* @returns {object | null | false}
|
|
348
350
|
*/
|
|
349
|
-
#wholeClosedPiece(session, name, index,
|
|
351
|
+
#wholeClosedPiece(session, name, index, admitted = false, bytes = null) {
|
|
352
|
+
|
|
350
353
|
const format = session.segmentFormat;
|
|
351
354
|
if (!format?.readMediaRanges || !Number.isInteger(index) || index < 0) {
|
|
352
355
|
return null;
|
|
353
356
|
}
|
|
354
357
|
const address = session.outputKey ?? "";
|
|
355
358
|
try {
|
|
356
|
-
const mediaRanges = format.readMediaRanges(this.#host.segmentFiles.closedBytesOf(address, name));
|
|
357
|
-
|
|
359
|
+
const mediaRanges = format.readMediaRanges(bytes ?? this.#host.segmentFiles.closedBytesOf(address, name));
|
|
360
|
+
|
|
358
361
|
const cut = cutOf(session.timeline, index);
|
|
359
|
-
const
|
|
362
|
+
const grid = session.timeline.published ?? session.timeline.boundaries;
|
|
363
|
+
const interval = admitted ? { from: grid?.[index], to: grid?.[index + 1],
|
|
364
|
+
...(index === session.timeline.segmentCount - 1 ? { sourceEnds: Object.fromEntries(
|
|
365
|
+
(admitted.tracks ?? []).filter(input => Number.isFinite(input.sourceEndSeconds))
|
|
366
|
+
.map(input => [input.track.type === "video" ? "vide" : "soun", input.sourceEndSeconds])) } : {}),
|
|
367
|
+
requiredKinds: [session.spec.carries !== "audio-only" ? "vide" : null,
|
|
368
|
+
session.spec.audio && !this.#host.servesAudioSeparately(session) ? "soun" : null].filter(Boolean) } : undefined;
|
|
369
|
+
const { whole, throughSeconds, reason } = judgePiece(format, mediaRanges, cut, interval);
|
|
360
370
|
if (!whole) {
|
|
361
371
|
this.#host.logger.warn(
|
|
362
372
|
`encode: not publishing piece ${index} of ${address.slice(0, 60)}: ` +
|
|
363
|
-
(throughSeconds === null ? "it holds no playable media" : `produced through ${throughSeconds}s, cut ${cut}s`)
|
|
373
|
+
(reason ?? (throughSeconds === null ? "it holds no playable media" : `produced through ${throughSeconds}s, cut ${cut}s`))
|
|
364
374
|
);
|
|
365
375
|
return false;
|
|
366
376
|
}
|
|
377
|
+
if (admitted) {
|
|
378
|
+
const produced = this.#host.producedNumbers(session);
|
|
379
|
+
for (const neighbor of [index - 1, index + 1]) {
|
|
380
|
+
if (!produced.includes(neighbor)) continue;
|
|
381
|
+
const ranges = this.#host.segmentFiles.mediaRangesOf(address, neighbor,
|
|
382
|
+
{ startSeconds: grid[neighbor] });
|
|
383
|
+
if (!ranges) continue;
|
|
384
|
+
const judged = neighbor < index ? judgeNeighbors(ranges, mediaRanges) : judgeNeighbors(mediaRanges, ranges);
|
|
385
|
+
if (!judged.whole) {
|
|
386
|
+
this.#host.logger.warn(`encode: not publishing piece ${index} of ${address}: ${judged.reason}`);
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
367
391
|
return mediaRanges;
|
|
368
392
|
} catch (error) {
|
|
369
393
|
this.#host.logger.warn(`encode: could not read the media of ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -519,7 +543,9 @@ export class EncodeRuns {
|
|
|
519
543
|
}
|
|
520
544
|
|
|
521
545
|
forgetEncodingOfGone(output) {
|
|
546
|
+
this.#host.encodeInputs?.forget(output);
|
|
522
547
|
if (this.#host.outputs.outputsOn(output.outputKey).length === 0) {
|
|
548
|
+
this.#inputFailures.forget(output.outputKey);
|
|
523
549
|
this.#host.encodeOrchestrator.forgetOutput(output.outputKey);
|
|
524
550
|
}
|
|
525
551
|
}
|
|
@@ -567,7 +593,7 @@ export class EncodeRuns {
|
|
|
567
593
|
//
|
|
568
594
|
// A DIFFERENT position is unaffected and gets its own budget, and the count
|
|
569
595
|
// resets the moment a run at this one does real work.
|
|
570
|
-
if (!this.#host.encodeOrchestrator.mayStartAt(address, from)) {
|
|
596
|
+
if (!this.#host.encodeInputs && !this.#host.encodeOrchestrator.mayStartAt(address, from)) {
|
|
571
597
|
return null;
|
|
572
598
|
}
|
|
573
599
|
// The encoder is built and handed back in this same call. Nothing here
|
|
@@ -772,6 +798,19 @@ export class EncodeRuns {
|
|
|
772
798
|
if (!this.isLive(session)) {
|
|
773
799
|
return null;
|
|
774
800
|
}
|
|
801
|
+
this.#host.encodeInputs?.retain(session, this.#host.encodeOrchestrator.wantedSegmentsOn(session.outputKey));
|
|
802
|
+
const admittedInput = this.#host.encodeInputs?.take(session, startIndex, startIndex) ?? null;
|
|
803
|
+
if (!admittedInput) return null;
|
|
804
|
+
const inputParameters = admittedInput ? { outputKey: session.outputKey, encoder: this.#host.videoEncoder.name,
|
|
805
|
+
width: session.output.encodeWidth, height: session.output.encodeHeight, fps: session.output.outputFps,
|
|
806
|
+
preset: session.output.softwarePreset, tonemap: session.output.applyTonemap,
|
|
807
|
+
rateControl: session.spec.video?.encode?.rateControl ?? null } : null;
|
|
808
|
+
const inputKey = admittedInput ? this.#inputFailures.key(admittedInput, inputParameters) : null;
|
|
809
|
+
if (inputKey && this.#inputFailures.failure(session.outputKey, startIndex, inputKey)) {
|
|
810
|
+
admittedInput.release();
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
775
814
|
// Where a restart's seconds go. A seek costs 5-8 s in the field and the
|
|
776
815
|
// recorded reason — waiting for the previous ffmpeg to exit, measured at
|
|
777
816
|
// 0.54-1.47 s — does not account for it. Before rebuilding the hottest path
|
|
@@ -793,7 +832,7 @@ export class EncodeRuns {
|
|
|
793
832
|
// an argument. Reading it off the coverage map a second time was the last
|
|
794
833
|
// remaining second answer to that question: the plan computed `to`, passed
|
|
795
834
|
// it, and the parameter list did not name it.
|
|
796
|
-
const runEnd = Number.isInteger(ordered?.to)
|
|
835
|
+
const runEnd = admittedInput ? startIndex : Number.isInteger(ordered?.to)
|
|
797
836
|
? ordered.to
|
|
798
837
|
: this.#runEndFrom(session, startIndex, null);
|
|
799
838
|
// The restart backs off a segment or two from what was asked for, so the
|
|
@@ -839,10 +878,6 @@ export class EncodeRuns {
|
|
|
839
878
|
// time and landed apart again. It is a fact of the FILE's cutting, held in
|
|
840
879
|
// the live table every session of the file shares, so it is read from
|
|
841
880
|
// there.
|
|
842
|
-
const positionSecondsOverride = session.spec.carries === "audio-only"
|
|
843
|
-
? trueStartOf(session.timeline, startIndex)
|
|
844
|
-
: undefined;
|
|
845
|
-
|
|
846
881
|
// What is still read off the session for a run. The list is the measure of
|
|
847
882
|
// how far a session still is from being the three things a run is built
|
|
848
883
|
// from — the material, what is produced of it, and the stretch — and it
|
|
@@ -851,13 +886,10 @@ export class EncodeRuns {
|
|
|
851
886
|
// delete, and this is a thing that disappears by being emptied.
|
|
852
887
|
this.#lastInputToken += 1;
|
|
853
888
|
const inputToken = this.#lastInputToken;
|
|
854
|
-
|
|
855
|
-
const { args, safeIndex,
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
audioFile: inputs.audioFile,
|
|
859
|
-
inputUrl: inputs.inputUrl,
|
|
860
|
-
audioInputUrl: inputs.audioInputUrl,
|
|
889
|
+
if (admittedInput) admittedInput.runTag = `${startIndex}r${inputToken}`;
|
|
890
|
+
const { args, safeIndex, startSeconds, cutTimes } = buildAdmittedCommand({
|
|
891
|
+
admittedInput,
|
|
892
|
+
|
|
861
893
|
timeline: session.timeline,
|
|
862
894
|
output: session.output,
|
|
863
895
|
segmentFormat: session.segmentFormat,
|
|
@@ -868,8 +900,7 @@ export class EncodeRuns {
|
|
|
868
900
|
audioSourceTrackIndex: session.spec.audioSourceTrackIndex,
|
|
869
901
|
rateControl: session.spec.video?.encode?.rateControl ?? null,
|
|
870
902
|
startIndex,
|
|
871
|
-
endIndex: runEnd,
|
|
872
|
-
positionSecondsOverride,
|
|
903
|
+
endIndex: Number.isInteger(ordered?.to) ? ordered.to : runEnd >= startIndex ? runEnd : session.timeline.segmentCount - 1,
|
|
873
904
|
videoEncoder: this.#host.videoEncoder,
|
|
874
905
|
segmentDurationSec: this.#host.segmentDurationSec
|
|
875
906
|
});
|
|
@@ -898,18 +929,17 @@ export class EncodeRuns {
|
|
|
898
929
|
// killed mid-piece leaving a partial file — is not a correctness problem
|
|
899
930
|
// either, because the store serves a segment only once its closure is
|
|
900
931
|
// proven, and it is cleared up when the run ends.
|
|
901
|
-
let originMicros = null;
|
|
902
|
-
const referenceKind = session.spec.video ? "vide" : "soun";
|
|
903
932
|
const run = new EncodeRun({
|
|
904
933
|
address: session.outputKey ?? session.id,
|
|
905
934
|
encoder: this.#host.videoEncoder,
|
|
906
935
|
from: safeIndex,
|
|
907
936
|
to: runEnd,
|
|
937
|
+
makingTag: admittedInput?.runTag ?? String(safeIndex),
|
|
908
938
|
buildArgs: () => args,
|
|
909
939
|
argsDescribed: describeFfmpegArgs(args),
|
|
910
940
|
// Whether this run cuts at times we gave it. Decides how a segment is
|
|
911
941
|
// judged finished — see getFileStream.
|
|
912
|
-
usesExplicitCuts: Boolean(cutTimes && cutTimes.length > 0),
|
|
942
|
+
usesExplicitCuts: Boolean(admittedInput || (cutTimes && cutTimes.length > 0)),
|
|
913
943
|
startSeconds,
|
|
914
944
|
totalSeconds: Number(session.file.durationSeconds) || null,
|
|
915
945
|
spawn: (spawnArgs) =>
|
|
@@ -917,14 +947,14 @@ export class EncodeRuns {
|
|
|
917
947
|
cwd: this.#host.segmentFiles.directoryFor(session.outputKey ?? ""),
|
|
918
948
|
// A fourth channel: the encoder names every piece it has CLOSED on it,
|
|
919
949
|
// which is the only proof a piece is whole.
|
|
920
|
-
stdio: ["
|
|
950
|
+
stdio: ["pipe", "pipe", "pipe", "pipe"]
|
|
921
951
|
}),
|
|
922
952
|
logger: this.#host.logger,
|
|
923
953
|
// The film's last segment number, which is what tells "it reached the
|
|
924
954
|
// end" from "its input dried up": ffmpeg exits zero for both and over a
|
|
925
955
|
// torrent cannot tell them apart.
|
|
926
956
|
lastSegmentIndex: () =>
|
|
927
|
-
|
|
957
|
+
run.to,
|
|
928
958
|
inputUnavailable: (message) => isInputUnavailable(message),
|
|
929
959
|
onProgress: (report) => this.#noteRunProgress(session, run, report),
|
|
930
960
|
indexOfName: (name) => session.segmentFormat.segmentIndexFromName(
|
|
@@ -932,22 +962,31 @@ export class EncodeRuns {
|
|
|
932
962
|
// Why this encoder exists, recorded with its argument list. It used to be
|
|
933
963
|
// handed to a separate `start` call; there is no separate call now.
|
|
934
964
|
because,
|
|
935
|
-
onClosed: (name,
|
|
965
|
+
onClosed: (name, following) => {
|
|
966
|
+
|
|
936
967
|
const index = session.segmentFormat.segmentIndexFromName(
|
|
937
968
|
session.segmentFormat.servedNameOf?.(name) ?? name);
|
|
938
|
-
if (timing && originMicros === null && session.segmentFormat.readMediaRanges) {
|
|
939
|
-
const ranges = session.segmentFormat.readMediaRanges(
|
|
940
|
-
this.#host.segmentFiles.closedBytesOf(session.outputKey ?? "", name));
|
|
941
|
-
originMicros = originOf(ranges, timing.endMicros, referenceKind);
|
|
942
|
-
}
|
|
943
969
|
if (Number.isInteger(index) && index < safeIndex) return null;
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
970
|
+
let bytes = null;
|
|
971
|
+
if (admittedInput && session.segmentFormat.id === "fmp4" && session.spec.carries !== "audio-only" &&
|
|
972
|
+
session.spec.audio && !this.#host.servesAudioSeparately(session)) {
|
|
973
|
+
try {
|
|
974
|
+
const address = session.outputKey ?? "";
|
|
975
|
+
const grid = session.timeline.published ?? session.timeline.boundaries;
|
|
976
|
+
bytes = presentationSegment(this.#host.segmentFiles.closedBytesOf(address, name),
|
|
977
|
+
following ? this.#host.segmentFiles.closedBytesOf(address, following) : null,
|
|
978
|
+
{ from: grid[index], to: grid[index + 1] });
|
|
979
|
+
} catch (error) {
|
|
980
|
+
this.#host.logger.warn(`encode: could not partition closed segment ${index}: ${error.message}`);
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const mediaRanges = this.#wholeClosedPiece(session, name, index, admittedInput, bytes);
|
|
985
|
+
if (mediaRanges === false) throw new Error(`Closed piece #${index} has incomplete media.`);
|
|
986
|
+
const published = this.#host.segmentFiles.publish(session.outputKey ?? "", name, session.segmentFormat, { mediaRanges, bytes });
|
|
987
|
+
if (!published) throw new Error(`Closed piece #${index} could not be stored.`);
|
|
950
988
|
return published;
|
|
989
|
+
|
|
951
990
|
},
|
|
952
991
|
onSpeedMeasured: () => this.#host.noteRunSpeedMeasured?.(session, run),
|
|
953
992
|
onEnded: (ended) => {
|
|
@@ -955,6 +994,27 @@ export class EncodeRuns {
|
|
|
955
994
|
this.noteRunEnded(session, run, ended);
|
|
956
995
|
}
|
|
957
996
|
});
|
|
997
|
+
if (admittedInput) {
|
|
998
|
+
run.inputFingerprint = admittedInput.fingerprint;
|
|
999
|
+
run.admittedInputKeys = new Map([[safeIndex, inputKey]]);
|
|
1000
|
+
let nextIndex = startIndex + 1;
|
|
1001
|
+
const canAppend = index => this.isLive(session) && run.isAlive &&
|
|
1002
|
+
this.#host.encodeOrchestrator.wantedSegmentsOn(session.outputKey).some(window => index >= window.from && index <= window.to) &&
|
|
1003
|
+
!this.liveRunsOf(session).some(other => other !== run && index >= other.from && index <= other.to) &&
|
|
1004
|
+
!this.#host.producedNumbers(session).includes(index);
|
|
1005
|
+
void writeAdmittedInput(admittedInput, run.process.stdin, { next: async () => {
|
|
1006
|
+
if (nextIndex > (ordered?.to ?? startIndex) || !canAppend(nextIndex)) return null;
|
|
1007
|
+
const next = await this.#host.encodeInputs.acquire(session, nextIndex, nextIndex);
|
|
1008
|
+
if (!next) return null;
|
|
1009
|
+
if (!canAppend(nextIndex)) { next.release(); return null; }
|
|
1010
|
+
run.admittedInputKeys.set(nextIndex, this.#inputFailures.key(next, inputParameters));
|
|
1011
|
+
run.to = nextIndex++;
|
|
1012
|
+
return next;
|
|
1013
|
+
} }).catch(error => {
|
|
1014
|
+
this.#host.logger.warn(`encode input output=${session.outputKey} segment=${safeIndex} write failed: ${error.message}`);
|
|
1015
|
+
run.process?.stdin?.destroy();
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
958
1018
|
this.#runsByInputToken.set(inputToken, run);
|
|
959
1019
|
// THE ONE FAULT THAT IS OTHERWISE SILENT, asked before this run produces a
|
|
960
1020
|
// frame. It lost its caller in a refactor on 2026-09-04 and had none until
|
|
@@ -978,6 +1038,17 @@ export class EncodeRuns {
|
|
|
978
1038
|
`numbering from #${safeIndex}`
|
|
979
1039
|
);
|
|
980
1040
|
return run;
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
admittedInput?.release();
|
|
1043
|
+
if (inputKey && failedAdmittedInput({ ending: ENCODE_EXIT.FAILED,
|
|
1044
|
+
code: error.code ? null : 1, because: error.message },
|
|
1045
|
+
session.spec.transcodesVideo && this.#host.videoEncoder.kind !== "software")) {
|
|
1046
|
+
this.#inputFailures.note(session.outputKey, startIndex, inputKey, error.message);
|
|
1047
|
+
this.#host.invalidateWaits(session);
|
|
1048
|
+
this.#host.productionFailed(session);
|
|
1049
|
+
}
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
981
1052
|
}
|
|
982
1053
|
|
|
983
1054
|
/**
|
|
@@ -1040,6 +1111,16 @@ export class EncodeRuns {
|
|
|
1040
1111
|
// been dead code — which is also why nothing ever stopped the restart loop
|
|
1041
1112
|
// recorded in the field the same day.
|
|
1042
1113
|
const wasCurrent = this.runsOf(session).includes(run);
|
|
1114
|
+
const inputFailed = run.admittedInputKeys && failedAdmittedInput(ended,
|
|
1115
|
+
session.spec.transcodesVideo && this.#host.videoEncoder.kind !== "software");
|
|
1116
|
+
if (inputFailed) {
|
|
1117
|
+
const index = run.head;
|
|
1118
|
+
const key = run.admittedInputKeys.get(index);
|
|
1119
|
+
if (key) {
|
|
1120
|
+
this.#inputFailures.note(session.outputKey, index, key, ended.because);
|
|
1121
|
+
this.#host.logger.warn(`encode: unchanged input will not be retried output=${session.outputKey} segment=${index} input=${key} reason=${ended.because}`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1043
1124
|
this.#host.encodeOrchestrator.noteEnded(ended);
|
|
1044
1125
|
// What this admitted encode was seen to do, kept for the next time this
|
|
1045
1126
|
// host prices the same mode (roadmap item 97, step 14). Whatever the
|
|
@@ -1178,7 +1259,7 @@ export class EncodeRuns {
|
|
|
1178
1259
|
// an unbounded loop: measured 2026-09-05, ffmpeg failing to spawn produced
|
|
1179
1260
|
// fifty passes of the plan before a probe stopped it, as fast as the
|
|
1180
1261
|
// failures arrived.
|
|
1181
|
-
const failure = this.#host.encodeOrchestrator.noteStartFailure(
|
|
1262
|
+
const failure = inputFailed ? { count: 0 } : this.#host.encodeOrchestrator.noteStartFailure(
|
|
1182
1263
|
session.outputKey,
|
|
1183
1264
|
ended.from,
|
|
1184
1265
|
ended.livedMs
|
|
@@ -1223,9 +1304,8 @@ export class EncodeRuns {
|
|
|
1223
1304
|
// Named exactly as the command line names them, second input included: a
|
|
1224
1305
|
// refusal whose message describes a different mapping than the one that was
|
|
1225
1306
|
// refused is the reading that cost a wrong diagnosis before.
|
|
1226
|
-
const
|
|
1227
|
-
const
|
|
1228
|
-
const audioTrack = session.spec.audioSourceTrackIndex;
|
|
1307
|
+
const audioInput = 0;
|
|
1308
|
+
const audioTrack = 0;
|
|
1229
1309
|
if (session.spec.carries === "audio-only") {
|
|
1230
1310
|
wanted.push(`audio 0:a:${audioTrack}`);
|
|
1231
1311
|
} else if (this.#host.servesAudioSeparately(session)) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ENCODE_EXIT } from "./encode-exit.js";
|
|
3
|
+
|
|
4
|
+
/** Process and resource failures are not evidence against the admitted bytes. */
|
|
5
|
+
export function failedAdmittedInput(ended, hardwareEncode = false) {
|
|
6
|
+
if (hardwareEncode || ended.signal || ended.code === null || ended.code === undefined) return false;
|
|
7
|
+
if (!Number.isInteger(ended.code) || ended.code < 0) return false;
|
|
8
|
+
if (/cannot allocate memory|out of memory|no space left|resource temporarily unavailable|too many open files|ENOMEM|ENOSPC|EMFILE|EAGAIN/i.test(ended.because ?? "")) return false;
|
|
9
|
+
return ended.ending === ENCODE_EXIT.SHORT || ended.ending === ENCODE_EXIT.FAILED;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A failed input is retried only after its bytes or output parameters change. */
|
|
13
|
+
export class InputFailures {
|
|
14
|
+
#failures = new Map();
|
|
15
|
+
|
|
16
|
+
key(input, parameters) {
|
|
17
|
+
return createHash("sha256").update(input.fingerprint).update(JSON.stringify(parameters)).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
failure(address, index, key) {
|
|
21
|
+
const entries = this.#failures.get(address);
|
|
22
|
+
const entry = entries?.get(index);
|
|
23
|
+
if (entry?.key === key) return entry.reason;
|
|
24
|
+
if (entry) entries.delete(index);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
reason(address) { return this.#failures.get(address)?.values().next().value?.reason ?? null; }
|
|
29
|
+
|
|
30
|
+
note(address, index, key, reason) {
|
|
31
|
+
const entries = this.#failures.get(address) ?? new Map();
|
|
32
|
+
entries.set(index, { key, reason });
|
|
33
|
+
this.#failures.set(address, entries);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
forget(address) { this.#failures.delete(address); }
|
|
37
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { avcPacketSlices } from "./annex-b-input.js";
|
|
2
|
+
import { bitPayloadSlices } from "./bit-payload.js";
|
|
3
|
+
|
|
4
|
+
/** Emit already admitted packets as one timestamped Matroska input. */
|
|
5
|
+
export async function* matroskaInput({ tracks, readPacket, originSeconds = 0, includeHeader = true, seen = new Map() }) {
|
|
6
|
+
if (!Array.isArray(tracks) || tracks.length === 0 || typeof readPacket !== "function" || !Number.isFinite(originSeconds)) {
|
|
7
|
+
throw new TypeError("A Matroska input requires tracks, admitted packets and a timeline origin.");
|
|
8
|
+
}
|
|
9
|
+
if (includeHeader) {
|
|
10
|
+
const declarations = tracks.map((input, position) => trackElement(input.track, position + 1));
|
|
11
|
+
yield element("1a45dfa3", Buffer.concat([
|
|
12
|
+
uintElement("4286", 1), uintElement("42f7", 1), uintElement("42f2", 4), uintElement("42f3", 8),
|
|
13
|
+
textElement("4282", "matroska"), uintElement("4287", 4), uintElement("4285", 2)
|
|
14
|
+
]));
|
|
15
|
+
yield Buffer.from("1853806701ffffffffffffff", "hex");
|
|
16
|
+
yield element("1549a966", uintElement("2ad7b1", 1000));
|
|
17
|
+
yield element("1654ae6b", Buffer.concat(declarations));
|
|
18
|
+
}
|
|
19
|
+
const positions = tracks.map(() => 0);
|
|
20
|
+
const currentSeen = new Map();
|
|
21
|
+
while (true) {
|
|
22
|
+
let selected = -1, time = Infinity;
|
|
23
|
+
for (let position = 0; position < tracks.length; position++) {
|
|
24
|
+
const packet = tracks[position].packets[positions[position]];
|
|
25
|
+
if (!packet) continue;
|
|
26
|
+
const decodeTime = packet.dts ?? packet.pts;
|
|
27
|
+
if (selected < 0 || decodeTime < time) { selected = position; time = decodeTime; }
|
|
28
|
+
}
|
|
29
|
+
if (selected < 0) {
|
|
30
|
+
seen.clear();
|
|
31
|
+
for (const [position, identities] of currentSeen) seen.set(position, identities);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const input = tracks[selected];
|
|
35
|
+
const packet = input.packets[positions[selected]++];
|
|
36
|
+
const identity = `${packet.pts}:${packet.ranges.map(range => range.join("-")).join(",")}`;
|
|
37
|
+
const identities = currentSeen.get(selected) ?? new Set();
|
|
38
|
+
if (identities.has(identity)) continue;
|
|
39
|
+
identities.add(identity);
|
|
40
|
+
currentSeen.set(selected, identities);
|
|
41
|
+
if (seen.get(selected)?.has(identity)) continue;
|
|
42
|
+
const stamp = Math.round((packet.pts + (input.track.codecDelaySeconds ?? 0) - originSeconds) * 1e6);
|
|
43
|
+
if (!Number.isSafeInteger(stamp) || stamp < 0) throw new Error("Admitted packet precedes its Matroska timeline origin.");
|
|
44
|
+
const payload = await readPacket(input, packet);
|
|
45
|
+
let slices = Array.isArray(payload) ? payload : [payload];
|
|
46
|
+
let payloadBytes = slices.reduce((sum, slice) => sum + (Buffer.isBuffer(slice) ? slice.length : 0), 0);
|
|
47
|
+
const expected = packet.ranges.reduce((sum, [start, end]) => sum + end - start + 1, 0);
|
|
48
|
+
if (!slices.length || slices.some(slice => !Buffer.isBuffer(slice)) || payloadBytes !== expected) throw new Error("Admitted packet bytes are incomplete.");
|
|
49
|
+
if (packet.bitOffset !== undefined) {
|
|
50
|
+
slices = bitPayloadSlices(slices, packet.bitOffset, packet.bitLength);
|
|
51
|
+
payloadBytes = packet.bitLength / 8;
|
|
52
|
+
}
|
|
53
|
+
if (input.track.packetFraming === "annex-b") {
|
|
54
|
+
slices = avcPacketSlices(slices);
|
|
55
|
+
payloadBytes = slices.reduce((sum, slice) => sum + slice.length, 0);
|
|
56
|
+
}
|
|
57
|
+
const blockHeader = Buffer.concat([vint(selected + 1), Buffer.from([0, 0, 0])]);
|
|
58
|
+
const duration = Math.round(packet.duration * 1e6);
|
|
59
|
+
const afterBlock = Buffer.concat([
|
|
60
|
+
...(duration > 0 ? [uintElement("9b", duration)] : []),
|
|
61
|
+
...(packet.discardPaddingSeconds ? [signedElement("75a2", Math.round(packet.discardPaddingSeconds * 1e9))] : []),
|
|
62
|
+
...(packet.keyframe ? [] : [element("fb", Buffer.from([0]))])]);
|
|
63
|
+
const blockPrefix = prefix("a1", blockHeader.length + payloadBytes);
|
|
64
|
+
const groupPrefix = prefix("a0", blockPrefix.length + blockHeader.length + payloadBytes + afterBlock.length);
|
|
65
|
+
const timestamp = uintElement("e7", stamp);
|
|
66
|
+
const clusterPrefix = prefix("1f43b675", timestamp.length + groupPrefix.length + blockPrefix.length +
|
|
67
|
+
blockHeader.length + payloadBytes + afterBlock.length);
|
|
68
|
+
yield Buffer.concat([clusterPrefix, timestamp, groupPrefix, blockPrefix, blockHeader]);
|
|
69
|
+
for (const slice of slices) yield slice;
|
|
70
|
+
if (afterBlock.length) yield afterBlock;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function trackElement(track, number) {
|
|
75
|
+
const type = track.type === "video" ? 1 : track.type === "audio" ? 2 : null;
|
|
76
|
+
if (type === null) throw new Error("Admitted input carries an unsupported track type.");
|
|
77
|
+
const codec = track.matroskaCodecId ?? matroskaCodec(track.codecId);
|
|
78
|
+
const fields = [uintElement("d7", number), uintElement("73c5", number), uintElement("83", type),
|
|
79
|
+
uintElement("9c", 0), textElement("86", codec)];
|
|
80
|
+
const privateBytes = track.matroskaCodecPrivateB64 ?? track.codecPrivateB64;
|
|
81
|
+
if (privateBytes) fields.push(element("63a2", Buffer.from(privateBytes, "base64")));
|
|
82
|
+
if (track.language) fields.push(textElement("22b59c", track.language));
|
|
83
|
+
if (track.codecDelaySeconds > 0) fields.push(uintElement("56aa", Math.round(track.codecDelaySeconds * 1e9)));
|
|
84
|
+
if (track.seekPrerollSeconds > 0) fields.push(uintElement("56bb", Math.round(track.seekPrerollSeconds * 1e9)));
|
|
85
|
+
if (type === 1) {
|
|
86
|
+
if (!(track.width > 0 && track.height > 0)) throw new Error("Admitted video has no declared dimensions.");
|
|
87
|
+
fields.push(element("e0", Buffer.concat([uintElement("b0", track.width), uintElement("ba", track.height)])));
|
|
88
|
+
} else {
|
|
89
|
+
if (!(track.samplingFrequency > 0 && track.channels > 0)) throw new Error("Admitted audio has no declared format.");
|
|
90
|
+
const frequency = Buffer.alloc(8);
|
|
91
|
+
frequency.writeDoubleBE(track.samplingFrequency);
|
|
92
|
+
if (codec.startsWith("A_PCM/") && !(track.bitDepth > 0)) throw new Error("Admitted PCM has no declared sample bit depth.");
|
|
93
|
+
fields.push(element("e1", Buffer.concat([element("b5", frequency), uintElement("9f", track.channels),
|
|
94
|
+
...(track.bitDepth > 0 ? [uintElement("6264", track.bitDepth)] : [])])));
|
|
95
|
+
}
|
|
96
|
+
return element("ae", Buffer.concat(fields));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Stable identity of every declaration emitted into the input stream. */
|
|
100
|
+
export function matroskaTrackIdentity(track) {
|
|
101
|
+
return JSON.stringify([
|
|
102
|
+
track.type, track.matroskaCodecId ?? matroskaCodec(track.codecId),
|
|
103
|
+
track.matroskaCodecPrivateB64 ?? track.codecPrivateB64 ?? "",
|
|
104
|
+
track.language ?? "", track.packetFraming ?? "", track.width ?? null, track.height ?? null,
|
|
105
|
+
track.samplingFrequency ?? null, track.channels ?? null, track.bitDepth ?? null,
|
|
106
|
+
track.codecDelaySeconds ?? 0, track.seekPrerollSeconds ?? 0
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function matroskaCodec(codec) {
|
|
111
|
+
if (/^[VA]_/.test(codec)) return codec;
|
|
112
|
+
if (/^pcm_[su](?:16|24|32|64)le$/.test(codec) || /^pcm_[su]8(?:le|be)?$/.test(codec)) return "A_PCM/INT/LIT";
|
|
113
|
+
if (/^pcm_[su](?:16|24|32|64)be$/.test(codec)) return "A_PCM/INT/BIG";
|
|
114
|
+
if (/^pcm_f(?:32|64)(?:le|be)$/.test(codec)) return "A_PCM/FLOAT/IEEE";
|
|
115
|
+
const declared = new Map([
|
|
116
|
+
["avc1", "V_MPEG4/ISO/AVC"], ["avc3", "V_MPEG4/ISO/AVC"], ["h264", "V_MPEG4/ISO/AVC"],
|
|
117
|
+
["hvc1", "V_MPEGH/ISO/HEVC"], ["hev1", "V_MPEGH/ISO/HEVC"], ["hevc", "V_MPEGH/ISO/HEVC"],
|
|
118
|
+
["av01", "V_AV1"], ["av1", "V_AV1"], ["vp9", "V_VP9"], ["vp8", "V_VP8"],
|
|
119
|
+
["mjpeg", "V_MJPEG"], ["ffv1", "V_FFV1"], ["theora", "V_THEORA"], ["jpeg2000", "V_JPEG2000"],
|
|
120
|
+
["mpeg1video", "V_MPEG1"], ["mpeg2video", "V_MPEG2"], ["mpeg4", "V_MPEG4/ISO/ASP"],
|
|
121
|
+
["FMP4", "V_MPEG4/ISO/ASP"], ["XVID", "V_MPEG4/ISO/ASP"], ["DIVX", "V_MPEG4/ISO/ASP"],
|
|
122
|
+
["mp4a", "A_AAC"], ["aac", "A_AAC"], ["mp3", "A_MPEG/L3"], ["mp2", "A_MPEG/L2"], ["mp1", "A_MPEG/L1"],
|
|
123
|
+
["ac-3", "A_AC3"], ["ac3", "A_AC3"], ["ec-3", "A_EAC3"], ["eac3", "A_EAC3"],
|
|
124
|
+
["Opus", "A_OPUS"], ["opus", "A_OPUS"], ["fLaC", "A_FLAC"], ["flac", "A_FLAC"], ["dts", "A_DTS"],
|
|
125
|
+
["vorbis", "A_VORBIS"], ["alac", "A_ALAC"], ["truehd", "A_TRUEHD"]
|
|
126
|
+
]).get(codec);
|
|
127
|
+
if (!declared) throw new Error(`Admitted codec ${codec} has no Matroska declaration.`);
|
|
128
|
+
return declared;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function vint(value) {
|
|
132
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("EBML value is outside its integer range.");
|
|
133
|
+
for (let width = 1; width <= 8; width++) {
|
|
134
|
+
if (BigInt(value) >= (1n << BigInt(7 * width)) - 1n) continue;
|
|
135
|
+
const encoded = Buffer.alloc(width);
|
|
136
|
+
let bits = BigInt(value) | (1n << BigInt(7 * width));
|
|
137
|
+
for (let at = width - 1; at >= 0; at--) { encoded[at] = Number(bits & 255n); bits >>= 8n; }
|
|
138
|
+
return encoded;
|
|
139
|
+
}
|
|
140
|
+
throw new TypeError("EBML value has no valid width.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function prefix(id, size) { return Buffer.concat([Buffer.from(id, "hex"), vint(size)]); }
|
|
144
|
+
function element(id, payload) { return Buffer.concat([prefix(id, payload.length), payload]); }
|
|
145
|
+
function textElement(id, text) { return element(id, Buffer.from(text, "utf8")); }
|
|
146
|
+
function signedElement(id, value) {
|
|
147
|
+
if (!Number.isSafeInteger(value)) throw new TypeError("EBML signed integer is outside its exact range.");
|
|
148
|
+
const bytes = Buffer.alloc(8);
|
|
149
|
+
bytes.writeBigInt64BE(BigInt(value));
|
|
150
|
+
return element(id, bytes);
|
|
151
|
+
}
|
|
152
|
+
function uintElement(id, value) {
|
|
153
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("EBML unsigned integer is invalid.");
|
|
154
|
+
let bits = BigInt(value);
|
|
155
|
+
const bytes = [];
|
|
156
|
+
do { bytes.unshift(Number(bits & 255n)); bits >>= 8n; } while (bits > 0n);
|
|
157
|
+
return element(id, Buffer.from(bytes));
|
|
158
|
+
}
|