@torrent-tv/proxy 2.95.3 → 2.95.4
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/package.json +1 -1
- package/routes/encode-input/get.js +29 -0
- package/server.js +55 -10
- package/services/encode/EncodeInputs.js +8 -2
- package/services/encode/EncodeRuns.js +21 -3
- package/services/encode/OriginalInput.js +59 -0
- package/services/encode/admitted-command.js +8 -2
- package/services/encode/output/Timeline.js +4 -0
- package/services/encode/piece-completeness.js +6 -2
- package/services/encode/run-command.js +5 -5
- package/services/encode/segment-formats/fmp4.js +1 -0
- package/services/encode/segment-formats/mp4-boxes.js +26 -2
- package/services/encode/source-command.js +239 -0
- package/services/media/ContainerOrchestrator.js +7 -1
- package/services/media/container/MatroskaContainer.js +79 -0
- package/services/media/container/ffprobe-record.js +2 -0
- package/services/server/SegmentServing.js +3 -1
- package/services/server/wire-outputs.js +2 -1
- package/services/storage/piece-store/shared-piece-store.js +8 -1
- package/services/torrent/download/FutureDownload.js +98 -150
- package/services/torrent/download/SwarmSelection.js +45 -33
- package/services/torrent/download/bands.js +5 -0
- package/services/torrent/download/registry.js +5 -11
- package/services/viewer/DownloadMaps.js +1 -1
- package/services/viewer/NativeSourceMap.js +25 -0
- package/services/viewer/PriorityMap.js +1 -0
- package/services/viewer/playback-readiness.js +61 -19
- package/services/viewer/segment-demands.js +6 -2
- package/services/torrent/download/DeadlineScheduler.js +0 -195
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { PLAYLIST_FILE_NAME } from "./output/playlists.js";
|
|
2
|
+
import { AUDIO_TRANSCODE_KBPS } from "./args.js";
|
|
3
|
+
import { cutsAtGivenTimes, SEGMENT_CUT_TIME_DELTA_SECONDS } from "./output/index.js";
|
|
4
|
+
import { ffmpegSeconds, onKeyframeGridFor, publishedGridFor, publishedStartTime, segmentCutTimesFrom, nearestKeyframeAtOrBefore } from "./run-command.js";
|
|
5
|
+
|
|
6
|
+
/** Account for FFmpeg's Matroska B-frame input-seek adjustment (ffmpeg_demux.c). */
|
|
7
|
+
function seekLandingOffsetFor(material, keyframe) {
|
|
8
|
+
if (material.transcodeVideo || material.audioOnly || !(material.reorderDepth > 0)) return 0;
|
|
9
|
+
const wanted = 3 / 23 + Math.max(0, material.keyframes?.tolerance ?? 0);
|
|
10
|
+
const next = material.keyframes?.times?.find(time => time > keyframe + 0.001);
|
|
11
|
+
return next === undefined ? wanted : Math.min(wanted, (next - keyframe) / 2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildRunCommand({
|
|
15
|
+
keyframes,
|
|
16
|
+
inputFile,
|
|
17
|
+
audioFile,
|
|
18
|
+
inputUrl,
|
|
19
|
+
audioInputUrl: audioInputUrlGiven,
|
|
20
|
+
timeline,
|
|
21
|
+
output,
|
|
22
|
+
segmentFormat,
|
|
23
|
+
transcodeVideo,
|
|
24
|
+
transcodeAudio,
|
|
25
|
+
audioOnly,
|
|
26
|
+
audioSeparate,
|
|
27
|
+
audioSourceTrackIndex,
|
|
28
|
+
videoSourceTrackIndex = 0,
|
|
29
|
+
rateControl,
|
|
30
|
+
startIndex,
|
|
31
|
+
endIndex,
|
|
32
|
+
positionSecondsOverride,
|
|
33
|
+
videoEncoder,
|
|
34
|
+
segmentDurationSec,
|
|
35
|
+
reorderDepth = 0
|
|
36
|
+
}) {
|
|
37
|
+
const safeIndex = Number.isInteger(startIndex) && startIndex > 0 ? startIndex : 0;
|
|
38
|
+
const startSeconds = Number.isFinite(positionSecondsOverride)
|
|
39
|
+
? positionSecondsOverride
|
|
40
|
+
: publishedStartTime(timeline, safeIndex, segmentDurationSec);
|
|
41
|
+
const keyframeGrid = onKeyframeGridFor({ audioOnly, timeline, transcodeVideo });
|
|
42
|
+
const servesAudioSeparately = audioOnly !== true && audioSeparate === true;
|
|
43
|
+
const audioFileStartTime = audioFile.startTime;
|
|
44
|
+
const sourceStartTime = inputFile.startTime;
|
|
45
|
+
const explicitTimes = segmentFormat.explicitTimesMuxerArgs?.() ?? null;
|
|
46
|
+
const inputIndex = !transcodeVideo && !audioOnly && explicitTimes && safeIndex > 0
|
|
47
|
+
? safeIndex - 1 : safeIndex;
|
|
48
|
+
const gridCutTimes = cutsAtGivenTimes({ segmentFormat, timeline })
|
|
49
|
+
? segmentCutTimesFrom(publishedGridFor(timeline), inputIndex)
|
|
50
|
+
: null;
|
|
51
|
+
const cutTimes = gridCutTimes;
|
|
52
|
+
const videoCodecArgs = transcodeVideo
|
|
53
|
+
? videoEncoder.buildVideoArgs({
|
|
54
|
+
targetWidth: output.encodeWidth,
|
|
55
|
+
targetHeight: output.encodeHeight,
|
|
56
|
+
segmentDurationSec: segmentDurationSec,
|
|
57
|
+
fps: output.outputFps,
|
|
58
|
+
preset: output.softwarePreset ?? undefined,
|
|
59
|
+
tonemap: output.applyTonemap === true,
|
|
60
|
+
forcedKeyframeTimes: cutTimes,
|
|
61
|
+
rateControl: rateControl ?? null
|
|
62
|
+
})
|
|
63
|
+
: ["-c:v", "copy"];
|
|
64
|
+
const audioCodecArgs = transcodeAudio
|
|
65
|
+
? ["-c:a", "aac", "-ac", "2", "-b:a", `${AUDIO_TRANSCODE_KBPS}k`]
|
|
66
|
+
: ["-c:a", "copy"];
|
|
67
|
+
|
|
68
|
+
const args = ["-hide_banner", "-nostats", "-loglevel", "error", "-progress", "pipe:1"];
|
|
69
|
+
if (transcodeVideo && Array.isArray(videoEncoder.inputArgs)) {
|
|
70
|
+
args.push(...videoEncoder.inputArgs);
|
|
71
|
+
}
|
|
72
|
+
const inputStartSeconds = inputIndex === safeIndex ? startSeconds :
|
|
73
|
+
publishedStartTime(timeline, inputIndex, segmentDurationSec);
|
|
74
|
+
const seekSeconds = timeline.cutGrid === "keyframe"
|
|
75
|
+
? inputStartSeconds + sourceStartTime
|
|
76
|
+
: inputStartSeconds;
|
|
77
|
+
const carriedKeyframe = timeline.cutGrid === "keyframe" && typeof timeline.sourceStartOf === "function"
|
|
78
|
+
? timeline.sourceStartOf(inputIndex)
|
|
79
|
+
: null;
|
|
80
|
+
const snappedKeyframe = Number.isFinite(carriedKeyframe)
|
|
81
|
+
? carriedKeyframe
|
|
82
|
+
: (Array.isArray(keyframes?.times) && keyframes.times.length > 0
|
|
83
|
+
? nearestKeyframeAtOrBefore(keyframes.times, seekSeconds)
|
|
84
|
+
: null);
|
|
85
|
+
const audioInputUrl =
|
|
86
|
+
typeof audioInputUrlGiven === "string" && audioInputUrlGiven.length > 0
|
|
87
|
+
? audioInputUrlGiven
|
|
88
|
+
: "";
|
|
89
|
+
const audioTimelineShift = audioInputUrl
|
|
90
|
+
? audioFileStartTime - sourceStartTime
|
|
91
|
+
: 0;
|
|
92
|
+
/**
|
|
93
|
+
* Add the second input, if there is one, with its own seek.
|
|
94
|
+
*
|
|
95
|
+
* Called between the first `-i` and any OUTPUT option, because ffmpeg reads
|
|
96
|
+
* these positionally: an option written after the last `-i` applies to the
|
|
97
|
+
* output, and the residual seek below is exactly such an option. Getting the
|
|
98
|
+
* order wrong would silently turn the audio file's seek into a trim of the
|
|
99
|
+
* finished stream.
|
|
100
|
+
*
|
|
101
|
+
* @param {number} inputSeekSeconds - Where to start, on the PICTURE's
|
|
102
|
+
* timeline. Translated to the soundtrack file's own here.
|
|
103
|
+
*/
|
|
104
|
+
const pushAudioInput = (inputSeekSeconds) => {
|
|
105
|
+
if (!audioInputUrl) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (audioTimelineShift !== 0 && keyframeGrid) {
|
|
109
|
+
args.push("-itsoffset", ffmpegSeconds(-audioTimelineShift));
|
|
110
|
+
}
|
|
111
|
+
const audioSeek = Math.max(0, inputSeekSeconds + audioTimelineShift);
|
|
112
|
+
if (audioSeek > 0) {
|
|
113
|
+
args.push("-accurate_seek", "-ss", ffmpegSeconds(audioSeek));
|
|
114
|
+
}
|
|
115
|
+
args.push("-i", audioInputUrl);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (snappedKeyframe !== null) {
|
|
119
|
+
const residualSeconds = Math.max(0, seekSeconds - snappedKeyframe);
|
|
120
|
+
if (snappedKeyframe > 0) {
|
|
121
|
+
args.push("-ss", ffmpegSeconds(snappedKeyframe + seekLandingOffsetFor({ audioOnly, transcodeVideo, keyframes, reorderDepth }, snappedKeyframe)));
|
|
122
|
+
}
|
|
123
|
+
args.push("-i", inputUrl);
|
|
124
|
+
pushAudioInput(snappedKeyframe);
|
|
125
|
+
if (residualSeconds > 0 && !keyframeGrid) {
|
|
126
|
+
args.push("-ss", ffmpegSeconds(residualSeconds));
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
if (seekSeconds > 0) {
|
|
130
|
+
args.push("-accurate_seek", "-ss", ffmpegSeconds(seekSeconds));
|
|
131
|
+
}
|
|
132
|
+
args.push("-i", inputUrl);
|
|
133
|
+
pushAudioInput(seekSeconds);
|
|
134
|
+
}
|
|
135
|
+
if (!keyframeGrid) {
|
|
136
|
+
if (startSeconds > 0) {
|
|
137
|
+
args.push("-output_ts_offset", ffmpegSeconds(startSeconds));
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
args.push("-copyts");
|
|
141
|
+
if (sourceStartTime !== 0) {
|
|
142
|
+
args.push("-output_ts_offset", ffmpegSeconds(-sourceStartTime));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const runEnd = Number.isInteger(endIndex) ? endIndex : -1;
|
|
146
|
+
const publishedGrid = publishedGridFor(timeline);
|
|
147
|
+
if (runEnd >= safeIndex && Array.isArray(publishedGrid) && publishedGrid[runEnd + 1] > 0) {
|
|
148
|
+
const endsAt = publishedGrid[runEnd + 1];
|
|
149
|
+
if (transcodeVideo) {
|
|
150
|
+
args.push("-t", ffmpegSeconds(Math.max(0.1, endsAt - publishedGrid[safeIndex])));
|
|
151
|
+
} else {
|
|
152
|
+
args.push("-to", ffmpegSeconds(endsAt));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (audioOnly === true) {
|
|
156
|
+
args.push("-vn", "-map", `0:a:${audioSourceTrackIndex}?`, ...audioCodecArgs);
|
|
157
|
+
} else if (servesAudioSeparately) {
|
|
158
|
+
args.push("-an", "-map", `0:v:${videoSourceTrackIndex}?`, ...videoCodecArgs);
|
|
159
|
+
} else {
|
|
160
|
+
args.push(
|
|
161
|
+
"-map",
|
|
162
|
+
`0:v:${videoSourceTrackIndex}?`,
|
|
163
|
+
"-map",
|
|
164
|
+
`${audioInputUrl ? 1 : 0}:a:${audioSourceTrackIndex}?`,
|
|
165
|
+
...videoCodecArgs,
|
|
166
|
+
...audioCodecArgs
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (cutTimes && cutTimes.length > 0) {
|
|
170
|
+
args.push(
|
|
171
|
+
"-f",
|
|
172
|
+
"segment",
|
|
173
|
+
"-segment_times",
|
|
174
|
+
cutTimes.join(","),
|
|
175
|
+
"-segment_time_delta",
|
|
176
|
+
String(SEGMENT_CUT_TIME_DELTA_SECONDS),
|
|
177
|
+
"-segment_start_number",
|
|
178
|
+
String(inputIndex),
|
|
179
|
+
"-segment_list",
|
|
180
|
+
"pipe:3",
|
|
181
|
+
"-segment_list_type",
|
|
182
|
+
"csv",
|
|
183
|
+
"-segment_list_flags",
|
|
184
|
+
"+live",
|
|
185
|
+
...explicitTimes,
|
|
186
|
+
segmentFormat.makingFileNameTemplate(String(safeIndex))
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
args.push(
|
|
190
|
+
"-f",
|
|
191
|
+
"hls",
|
|
192
|
+
"-hls_time",
|
|
193
|
+
String(segmentDurationSec),
|
|
194
|
+
"-hls_list_size",
|
|
195
|
+
"0",
|
|
196
|
+
"-hls_flags",
|
|
197
|
+
"independent_segments+temp_file",
|
|
198
|
+
...segmentFormat.muxerArgs(),
|
|
199
|
+
"-start_number",
|
|
200
|
+
String(safeIndex),
|
|
201
|
+
PLAYLIST_FILE_NAME
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return { args, safeIndex, inputIndex, startSeconds, cutTimes };
|
|
205
|
+
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** One finite original-source run, preserving the published interval and track choice. */
|
|
209
|
+
export function buildOriginalCommand(params) {
|
|
210
|
+
const { admittedInput, timeline, startIndex, inputToken, baseUrl, audioOnly } = params;
|
|
211
|
+
const grid = publishedGridFor(timeline);
|
|
212
|
+
const from = grid[startIndex], to = grid[startIndex + 1];
|
|
213
|
+
if (!(to > from)) throw new Error("Original-source encoding requires a finite published interval.");
|
|
214
|
+
const sourceFor = kind => admittedInput.sources.find(source => source.input.selections.some(selection => selection.track.type === kind));
|
|
215
|
+
const primary = sourceFor(audioOnly ? "audio" : "video");
|
|
216
|
+
const audio = sourceFor("audio") ?? primary;
|
|
217
|
+
if (!primary) throw new Error("The selected source track is absent from original input.");
|
|
218
|
+
const url = source => new URL(`/encode-input/${inputToken}/${source.fileIndex}`, baseUrl).href;
|
|
219
|
+
const localTimeline = { ...timeline, published: [from, to, to + (to - from)],
|
|
220
|
+
boundaries: [from, to, to + (to - from)], sourceStartOf: () => timeline.sourceStartOf?.(startIndex) ?? from + primary.timeShiftSeconds };
|
|
221
|
+
const command = buildRunCommand({ ...params, startIndex: 0, endIndex: 0, timeline: localTimeline,
|
|
222
|
+
videoSourceTrackIndex: primary.input.selections.find(selection => selection.track.type === "video")?.index ?? 0,
|
|
223
|
+
reorderDepth: primary.input.selections.find(selection => selection.track.type === "video")?.track.reorderDepth ?? 0,
|
|
224
|
+
inputFile: { startTime: primary.timeShiftSeconds }, audioFile: { startTime: audio.timeShiftSeconds },
|
|
225
|
+
inputUrl: url(primary), audioInputUrl: audio !== primary && !audioOnly ? url(audio) : "",
|
|
226
|
+
audioSourceTrackIndex: audio.input.selections.find(selection => selection.track.type === "audio")?.index ?? 0 });
|
|
227
|
+
const args = command.args;
|
|
228
|
+
// The segment muxer otherwise shifts negative initial DTS to zero before
|
|
229
|
+
// the inner muxer writes its edit list, moving B-picture PTS off the source.
|
|
230
|
+
args.splice(args.indexOf("-f"), 0, "-avoid_negative_ts", "disabled");
|
|
231
|
+
const formatOptions = args.indexOf("-segment_format_options");
|
|
232
|
+
if (formatOptions >= 0) args[formatOptions + 1] += ":avoid_negative_ts=disabled";
|
|
233
|
+
const numbering = args.indexOf("-segment_start_number");
|
|
234
|
+
if (numbering < 0) throw new Error("The format cannot produce finite original-source segments.");
|
|
235
|
+
args[numbering + 1] = String(startIndex);
|
|
236
|
+
args[args.length - 1] = params.segmentFormat.makingFileNameTemplate(admittedInput.runTag);
|
|
237
|
+
if (audioOnly) args[args.indexOf("-segment_time_delta") + 1] = "0";
|
|
238
|
+
return { ...command, safeIndex: startIndex, inputIndex: startIndex };
|
|
239
|
+
}
|
|
@@ -129,7 +129,7 @@ export class ContainerOrchestrator {
|
|
|
129
129
|
|
|
130
130
|
/** Read one statement without converting missing bytes into an empty answer. */
|
|
131
131
|
async inspect(params, statement = "tracks") {
|
|
132
|
-
if (!["container", "tracks", "media-info", "keyframes", "packets", "fingerprint", "work-tags", "cover"].includes(statement)) {
|
|
132
|
+
if (!["container", "tracks", "media-info", "keyframes", "packets", "source-navigation", "source-ranges", "fingerprint", "work-tags", "cover"].includes(statement)) {
|
|
133
133
|
throw new TypeError(`Unknown media statement: ${statement}`);
|
|
134
134
|
}
|
|
135
135
|
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
@@ -191,6 +191,12 @@ export class ContainerOrchestrator {
|
|
|
191
191
|
} else if (statement === "work-tags" || statement === "cover") {
|
|
192
192
|
value = await (statement === "cover" ? container.readCover(edgesOf(params)) : container.readWorkTags(edgesOf(params)));
|
|
193
193
|
if (value === null) return { kind: "terminal", reason: statement === "cover" ? "no-cover" : "format-states-nothing", requestId };
|
|
194
|
+
} else if (statement === "source-navigation") {
|
|
195
|
+
value = await container.supportsOriginalSourceRanges?.() === true;
|
|
196
|
+
} else if (statement === "source-ranges") {
|
|
197
|
+
if (typeof container.readSourceRanges !== "function") return { kind: "terminal", reason: "source-range-index-not-supported", requestId };
|
|
198
|
+
value = await container.readSourceRanges(params.packetInterval);
|
|
199
|
+
if (value.kind !== "result") return { ...value, requestId };
|
|
194
200
|
} else if (statement === "packets") {
|
|
195
201
|
value = await container.readPacketIndex(params.packetInterval);
|
|
196
202
|
await value?.prepareAudioDependencies?.(params.packetInterval, container.readRange);
|
|
@@ -145,6 +145,8 @@ export class MatroskaContainer extends Container {
|
|
|
145
145
|
|
|
146
146
|
/** @type {SegmentLayout | null} */
|
|
147
147
|
#layout = null;
|
|
148
|
+
#sourceElementEnds = new Map();
|
|
149
|
+
#sourceSeekPoints = null;
|
|
148
150
|
|
|
149
151
|
/** @type {Array<import("../tracks/ContainerTrack.js").ContainerTrack> | null} */
|
|
150
152
|
#tracks = null;
|
|
@@ -569,6 +571,83 @@ export class MatroskaContainer extends Container {
|
|
|
569
571
|
return result;
|
|
570
572
|
}
|
|
571
573
|
|
|
574
|
+
/** Original-file ranges from Cues alone, without reading media block headers.
|
|
575
|
+
* FFmpeg needs the declarations and seek table as well as complete clusters.
|
|
576
|
+
* One preceding cluster retains preroll; one following cluster covers demux
|
|
577
|
+
* lookahead beyond the presentation cut. No packet payload is parsed here.
|
|
578
|
+
*/
|
|
579
|
+
async supportsOriginalSourceRanges() {
|
|
580
|
+
const cues = await this.readCues();
|
|
581
|
+
if (!cues?.points.length) return false;
|
|
582
|
+
const video = ContainerTrack.firstUsable(await this.readTracks(), "video");
|
|
583
|
+
return cues.points.some(point => point.positions.some(position => !video || position.track === video.trackNumber));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async readMappedSourceRanges(interval) { return this.readSourceRanges(interval); }
|
|
587
|
+
|
|
588
|
+
async readSourceRanges(interval) {
|
|
589
|
+
if (!Number.isFinite(interval?.from) || !Number.isFinite(interval?.to) || interval.to <= interval.from) {
|
|
590
|
+
throw new TypeError("Source ranges require a finite increasing interval.");
|
|
591
|
+
}
|
|
592
|
+
const layout = await this.#segmentLayout();
|
|
593
|
+
if (!layout || layout.firstClusterAt === null) return { kind: "terminal", reason: "source-has-no-clusters" };
|
|
594
|
+
const cues = await this.readCues();
|
|
595
|
+
if (!await this.supportsOriginalSourceRanges()) return { kind: "needs-index", reason: "source-has-no-cue-map" };
|
|
596
|
+
const tracks = await this.readTracks();
|
|
597
|
+
const requested = tracks.filter(track => !interval.trackIds?.length || interval.trackIds.includes(track.trackNumber));
|
|
598
|
+
const video = ContainerTrack.firstUsable(tracks, "video");
|
|
599
|
+
const points = this.#sourceSeekPoints ??= cues.points.flatMap(point => point.positions
|
|
600
|
+
.filter(position => !video || position.track === video.trackNumber)
|
|
601
|
+
.map(position => ({ at: position.clusterAt, seconds: point.ticks * layout.secondsPerTick })))
|
|
602
|
+
.sort((left, right) => left.seconds - right.seconds || left.at - right.at);
|
|
603
|
+
if (!points.length) return super.readSourceRanges(interval);
|
|
604
|
+
const preroll = Math.max(0, ...requested.map(track => track.seekPrerollSeconds ?? 0));
|
|
605
|
+
const firstAt = (seconds, strict) => {
|
|
606
|
+
let low = 0, high = points.length;
|
|
607
|
+
while (low < high) {
|
|
608
|
+
const middle = Math.floor((low + high) / 2);
|
|
609
|
+
if (points[middle].seconds < seconds || strict && points[middle].seconds === seconds) low = middle + 1;
|
|
610
|
+
else high = middle;
|
|
611
|
+
}
|
|
612
|
+
return low;
|
|
613
|
+
};
|
|
614
|
+
const first = Math.max(0, firstAt(interval.from - preroll, true) - 2);
|
|
615
|
+
const beyond = firstAt(interval.to, false);
|
|
616
|
+
const following = points[beyond + 1];
|
|
617
|
+
const mediaEnd = following ? following.at - 1 : layout.segmentEnd - 1;
|
|
618
|
+
const elementEnd = async at => {
|
|
619
|
+
if (this.#sourceElementEnds.has(at)) return this.#sourceElementEnds.get(at);
|
|
620
|
+
const reader = new ElementReader({ read: this.readRange, fileSize: this.fileSize, portionBytes: this.portionBytes });
|
|
621
|
+
const header = await reader.header(at, layout.segmentEnd);
|
|
622
|
+
if (!header || header.end === null) return null;
|
|
623
|
+
this.#sourceElementEnds.set(at, header.end);
|
|
624
|
+
return header.end;
|
|
625
|
+
};
|
|
626
|
+
const cuesEnd = await elementEnd(layout.cuesAt);
|
|
627
|
+
if (cuesEnd === null) return { kind: "terminal", reason: "source-seek-index-is-unbounded" };
|
|
628
|
+
const requestBytes = 1024 * 1024;
|
|
629
|
+
const paddedEnd = end => Math.min(this.fileSize - 1, end + requestBytes);
|
|
630
|
+
const ranges = [[0, paddedEnd(layout.firstClusterAt - 1)],
|
|
631
|
+
[points[first].at, paddedEnd(mediaEnd)], [layout.cuesAt, paddedEnd(cuesEnd - 1)],
|
|
632
|
+
[Math.max(0, this.fileSize - requestBytes), this.fileSize - 1]];
|
|
633
|
+
// SeekHead may name top-level declarations after media clusters. Preserve
|
|
634
|
+
// their original bytes instead of reconstructing declarations for FFmpeg.
|
|
635
|
+
for (const at of [layout.infoAt, layout.tracksAt]) {
|
|
636
|
+
if (at === null || at < layout.firstClusterAt) continue;
|
|
637
|
+
const end = await elementEnd(at);
|
|
638
|
+
if (end === null) return { kind: "terminal", reason: "source-declarations-are-unbounded" };
|
|
639
|
+
ranges.push([at, paddedEnd(end - 1)]);
|
|
640
|
+
}
|
|
641
|
+
ranges.sort((left, right) => left[0] - right[0]);
|
|
642
|
+
const union = [];
|
|
643
|
+
for (const range of ranges) {
|
|
644
|
+
const previous = union.at(-1);
|
|
645
|
+
if (previous && range[0] <= previous[1] + 1) previous[1] = Math.max(previous[1], range[1]);
|
|
646
|
+
else union.push([...range]);
|
|
647
|
+
}
|
|
648
|
+
return { kind: "result", from: interval.from, to: interval.to, ranges: union, fileLength: this.fileSize };
|
|
649
|
+
}
|
|
650
|
+
|
|
572
651
|
async readPacketIndex(interval) {
|
|
573
652
|
if (this.#packets) return this.#packets;
|
|
574
653
|
const layout = await this.#segmentLayout();
|
|
@@ -15,6 +15,8 @@ export function ffprobeRecord(line) {
|
|
|
15
15
|
const kind = fields.shift();
|
|
16
16
|
const record = { kind };
|
|
17
17
|
for (const [position, value] of fields.entries()) {
|
|
18
|
+
// Disabled nested sections may leave a trailing separator in FFprobe 8.
|
|
19
|
+
if (value === "" && fields.slice(position + 1).every(field => field === "")) break;
|
|
18
20
|
// ffprobe 5 emits an empty nested section even when its entries are disabled.
|
|
19
21
|
if (value === "side_data" && fields.slice(position + 1).every(field => field === "")) break;
|
|
20
22
|
const separator = value.indexOf("=");
|
|
@@ -794,7 +794,9 @@ export class SegmentServing {
|
|
|
794
794
|
}
|
|
795
795
|
const ownInit = session.segmentFormat.extractInit(cached ?? await readFile(found));
|
|
796
796
|
const init = ownInit && session.segmentFormat.prepareSharedInit ?
|
|
797
|
-
session.segmentFormat.prepareSharedInit(ownInit
|
|
797
|
+
session.segmentFormat.prepareSharedInit(ownInit, {
|
|
798
|
+
audioEncoderDelay: session.spec.transcodesAudio ? 1024 : 0
|
|
799
|
+
}) : ownInit;
|
|
798
800
|
if (!init || init.length === 0) {
|
|
799
801
|
continue;
|
|
800
802
|
}
|
|
@@ -341,6 +341,7 @@ export function wireOutputs({
|
|
|
341
341
|
get encodeCost() { return parts.encodeCost; },
|
|
342
342
|
get encodeOrchestrator() { return parts.encodeOrchestrator; },
|
|
343
343
|
get ffmpegBin() { return parts.ffmpegBin; },
|
|
344
|
+
get localBaseUrl() { return parts.localBaseUrl; },
|
|
344
345
|
get outputTimes() { return parts.outputTimes; },
|
|
345
346
|
get outputs() { return parts.outputs; },
|
|
346
347
|
get priority() { return parts.priority; },
|
|
@@ -972,7 +973,7 @@ export function wireOutputs({
|
|
|
972
973
|
reviseBudget: () => parts.machineBudget.revise(),
|
|
973
974
|
capacity: () => parts.machineBudget.capacityOf("memory"),
|
|
974
975
|
urgent: (output, index) => parts.encodeOrchestrator.wantedSegmentsOn(output.outputKey).some(window =>
|
|
975
|
-
index >= window.from && index <= window.to && window.
|
|
976
|
+
index >= window.from && index <= window.to && window.urgent === true),
|
|
976
977
|
changed: (output, result) => {
|
|
977
978
|
if (result.kind === "terminal") {
|
|
978
979
|
logger.warn(`encode input output=${output.outputKey} terminal=${result.reason}: ${result.message ?? ""}` +
|
|
@@ -52,6 +52,7 @@ import { PieceDiskStore } from "./piece-disk-store.js";
|
|
|
52
52
|
* @type {Set<SharedPieceStore>}
|
|
53
53
|
*/
|
|
54
54
|
const liveStores = new Set();
|
|
55
|
+
let assignedMemoryAllowance = null;
|
|
55
56
|
|
|
56
57
|
export function collectStoreStats() {
|
|
57
58
|
return [...liveStores].map((store) => store.stats());
|
|
@@ -107,11 +108,17 @@ export function machineReserveBytes() {
|
|
|
107
108
|
/** Forget what other processes have needed. For tests, which share a module. */
|
|
108
109
|
export function forgetMachineMemory() {
|
|
109
110
|
otherDemand.forget();
|
|
111
|
+
assignedMemoryAllowance = null;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
export { divideAllowance, machineAllowanceBytes };
|
|
113
115
|
|
|
114
|
-
export function reviseStoreBudgets(allowanceBytes =
|
|
116
|
+
export function reviseStoreBudgets(allowanceBytes = assignedMemoryAllowance) {
|
|
117
|
+
// Periodic maintenance must retain the main thread's latest allocation.
|
|
118
|
+
// Recomputing it from host free memory would overwrite a fixed limit and
|
|
119
|
+
// the memory reserved for admitted encoder input.
|
|
120
|
+
assignedMemoryAllowance = Number.isFinite(allowanceBytes) && allowanceBytes !== null
|
|
121
|
+
? Math.max(0, allowanceBytes) : null;
|
|
115
122
|
const stores = [...liveStores];
|
|
116
123
|
const held = stores.reduce((sum, store) => sum + store.residentBytes, 0);
|
|
117
124
|
// TOLD, NOT WORKED OUT. Memory has one owner and it is on the main thread,
|