@torrent-tv/proxy 2.95.4 → 2.95.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.95.4",
3
+ "version": "2.95.5",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -467,9 +467,7 @@ export async function startProxyServer({
467
467
  const navigation = await containerOrchestrator.inspect(intervalParams, "source-navigation");
468
468
  if (navigation.kind !== "result") continue;
469
469
  const originalRanges = navigation.value;
470
- const packets = originalRanges && !zone.urgent
471
- ? { kind: "result", value: await container.readMappedSourceRanges(interval) }
472
- : await containerOrchestrator.inspect(intervalParams, originalRanges ? "source-ranges" : "packets");
470
+ const packets = await containerOrchestrator.inspect(intervalParams, originalRanges ? "source-ranges" : "packets");
473
471
  if (!map.isCurrent()) return [];
474
472
  if (packets.kind !== "result") continue;
475
473
  const input = originalRanges ? packets.value : new SegmentInputs({ index: packets.value, tracks: wanted }).forInterval({ ...interval, mode: track => modes.get(track) });
@@ -64,6 +64,13 @@ export function buildRunCommand({
64
64
  const audioCodecArgs = transcodeAudio
65
65
  ? ["-c:a", "aac", "-ac", "2", "-b:a", `${AUDIO_TRANSCODE_KBPS}k`]
66
66
  : ["-c:a", "copy"];
67
+ // Under `-copyts` FFmpeg trims a decoded stream at `-ss` PLUS its own idea of
68
+ // the file's start, ignoring `-seek_timestamp` (ffmpeg_demux.c, n8.1.2,
69
+ // `trim_start_us`): an absolute seek would be trimmed a second start later.
70
+ // On the keyframe grid its trim is therefore switched off on every input and
71
+ // the decoded soundtrack is cut here, on the picture file's clock, which is
72
+ // the clock every stream carries there.
73
+ const inputSeekArgs = keyframeGrid ? ["-noaccurate_seek"] : ["-accurate_seek"];
67
74
 
68
75
  const args = ["-hide_banner", "-nostats", "-loglevel", "error", "-progress", "pipe:1"];
69
76
  if (transcodeVideo && Array.isArray(videoEncoder.inputArgs)) {
@@ -71,16 +78,23 @@ export function buildRunCommand({
71
78
  }
72
79
  const inputStartSeconds = inputIndex === safeIndex ? startSeconds :
73
80
  publishedStartTime(timeline, inputIndex, segmentDurationSec);
74
- const seekSeconds = timeline.cutGrid === "keyframe"
75
- ? inputStartSeconds + sourceStartTime
76
- : inputStartSeconds;
81
+ // ONE CLOCK FOR EVERY INPUT SEEK. Each `-ss` below is an absolute timestamp
82
+ // of the file it seeks, given with `-seek_timestamp 1`: without it FFmpeg
83
+ // adds its own idea of the file's start time to the value, which is a second
84
+ // copy of a start this code already knows. Published positions begin at
85
+ // zero, so the picture's instant is the published time plus the picture
86
+ // file's start; keyframe times are on the same file clock; a soundtrack in
87
+ // another file is the same instant on that file's clock. Whether the cuts
88
+ // follow the source's keyframes or an even grid changes how the run ENDS
89
+ // and what its output timestamps are, never where its input is.
90
+ const seekAt = inputStartSeconds + sourceStartTime;
77
91
  const carriedKeyframe = timeline.cutGrid === "keyframe" && typeof timeline.sourceStartOf === "function"
78
92
  ? timeline.sourceStartOf(inputIndex)
79
93
  : null;
80
94
  const snappedKeyframe = Number.isFinite(carriedKeyframe)
81
95
  ? carriedKeyframe
82
96
  : (Array.isArray(keyframes?.times) && keyframes.times.length > 0
83
- ? nearestKeyframeAtOrBefore(keyframes.times, seekSeconds)
97
+ ? nearestKeyframeAtOrBefore(keyframes.times, seekAt)
84
98
  : null);
85
99
  const audioInputUrl =
86
100
  typeof audioInputUrlGiven === "string" && audioInputUrlGiven.length > 0
@@ -98,8 +112,8 @@ export function buildRunCommand({
98
112
  * order wrong would silently turn the audio file's seek into a trim of the
99
113
  * finished stream.
100
114
  *
101
- * @param {number} inputSeekSeconds - Where to start, on the PICTURE's
102
- * timeline. Translated to the soundtrack file's own here.
115
+ * @param {number} inputSeekSeconds - Where to start, on the PICTURE
116
+ * file's clock. Translated to the soundtrack file's own here.
103
117
  */
104
118
  const pushAudioInput = (inputSeekSeconds) => {
105
119
  if (!audioInputUrl) {
@@ -108,17 +122,17 @@ export function buildRunCommand({
108
122
  if (audioTimelineShift !== 0 && keyframeGrid) {
109
123
  args.push("-itsoffset", ffmpegSeconds(-audioTimelineShift));
110
124
  }
111
- const audioSeek = Math.max(0, inputSeekSeconds + audioTimelineShift);
112
- if (audioSeek > 0) {
113
- args.push("-accurate_seek", "-ss", ffmpegSeconds(audioSeek));
125
+ const audioSeek = inputSeekSeconds + audioTimelineShift;
126
+ if (audioSeek > audioFileStartTime) {
127
+ args.push("-seek_timestamp", "1", ...inputSeekArgs, "-ss", ffmpegSeconds(audioSeek));
114
128
  }
115
129
  args.push("-i", audioInputUrl);
116
130
  };
117
131
 
118
132
  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)));
133
+ const residualSeconds = Math.max(0, seekAt - snappedKeyframe);
134
+ if (snappedKeyframe > sourceStartTime) {
135
+ args.push("-seek_timestamp", "1", ...inputSeekArgs, "-ss", ffmpegSeconds(snappedKeyframe + seekLandingOffsetFor({ audioOnly, transcodeVideo, keyframes, reorderDepth }, snappedKeyframe)));
122
136
  }
123
137
  args.push("-i", inputUrl);
124
138
  pushAudioInput(snappedKeyframe);
@@ -126,11 +140,11 @@ export function buildRunCommand({
126
140
  args.push("-ss", ffmpegSeconds(residualSeconds));
127
141
  }
128
142
  } else {
129
- if (seekSeconds > 0) {
130
- args.push("-accurate_seek", "-ss", ffmpegSeconds(seekSeconds));
143
+ if (seekAt > sourceStartTime) {
144
+ args.push("-seek_timestamp", "1", ...inputSeekArgs, "-ss", ffmpegSeconds(seekAt));
131
145
  }
132
146
  args.push("-i", inputUrl);
133
- pushAudioInput(seekSeconds);
147
+ pushAudioInput(seekAt);
134
148
  }
135
149
  if (!keyframeGrid) {
136
150
  if (startSeconds > 0) {
@@ -146,12 +160,20 @@ export function buildRunCommand({
146
160
  const publishedGrid = publishedGridFor(timeline);
147
161
  if (runEnd >= safeIndex && Array.isArray(publishedGrid) && publishedGrid[runEnd + 1] > 0) {
148
162
  const endsAt = publishedGrid[runEnd + 1];
149
- if (transcodeVideo) {
150
- args.push("-t", ffmpegSeconds(Math.max(0.1, endsAt - publishedGrid[safeIndex])));
163
+ if (keyframeGrid) {
164
+ // `-copyts` keeps the input's own clock up to the muxer, and an output
165
+ // `-to` is compared there, before `-output_ts_offset` moves it.
166
+ args.push("-to", ffmpegSeconds(endsAt + sourceStartTime));
151
167
  } else {
152
- args.push("-to", ffmpegSeconds(endsAt));
168
+ // The output clock starts at zero where this run begins, so the end is
169
+ // a length; an output `-to` would be read as a length from zero too and
170
+ // run past the interval by its own start.
171
+ args.push("-t", ffmpegSeconds(Math.max(0.1, endsAt - startSeconds)));
153
172
  }
154
173
  }
174
+ if (keyframeGrid && transcodeAudio && !servesAudioSeparately) {
175
+ audioCodecArgs.push("-af", `atrim=start=${ffmpegSeconds(seekAt)}`);
176
+ }
155
177
  if (audioOnly === true) {
156
178
  args.push("-vn", "-map", `0:a:${audioSourceTrackIndex}?`, ...audioCodecArgs);
157
179
  } else if (servesAudioSeparately) {
@@ -217,7 +239,7 @@ export function buildOriginalCommand(params) {
217
239
  if (!primary) throw new Error("The selected source track is absent from original input.");
218
240
  const url = source => new URL(`/encode-input/${inputToken}/${source.fileIndex}`, baseUrl).href;
219
241
  const localTimeline = { ...timeline, published: [from, to, to + (to - from)],
220
- boundaries: [from, to, to + (to - from)], sourceStartOf: () => timeline.sourceStartOf?.(startIndex) ?? from + primary.timeShiftSeconds };
242
+ boundaries: [from, to, to + (to - from)], sourceStartOf: () => primary.input.from ?? from + primary.timeShiftSeconds };
221
243
  const command = buildRunCommand({ ...params, startIndex: 0, endIndex: 0, timeline: localTimeline,
222
244
  videoSourceTrackIndex: primary.input.selections.find(selection => selection.track.type === "video")?.index ?? 0,
223
245
  reorderDepth: primary.input.selections.find(selection => selection.track.type === "video")?.track.reorderDepth ?? 0,
@@ -229,7 +251,7 @@ export function buildOriginalCommand(params) {
229
251
  // the inner muxer writes its edit list, moving B-picture PTS off the source.
230
252
  args.splice(args.indexOf("-f"), 0, "-avoid_negative_ts", "disabled");
231
253
  const formatOptions = args.indexOf("-segment_format_options");
232
- if (formatOptions >= 0) args[formatOptions + 1] += ":avoid_negative_ts=disabled";
254
+ if (formatOptions >= 0) args[formatOptions + 1] += ":avoid_negative_ts=disabled:movie_timescale=1000000";
233
255
  const numbering = args.indexOf("-segment_start_number");
234
256
  if (numbering < 0) throw new Error("The format cannot produce finite original-source segments.");
235
257
  args[numbering + 1] = String(startIndex);
@@ -600,7 +600,7 @@ export class MatroskaContainer extends Container {
600
600
  .filter(position => !video || position.track === video.trackNumber)
601
601
  .map(position => ({ at: position.clusterAt, seconds: point.ticks * layout.secondsPerTick })))
602
602
  .sort((left, right) => left.seconds - right.seconds || left.at - right.at);
603
- if (!points.length) return super.readSourceRanges(interval);
603
+ if (!points.length) return { kind: "needs-index", reason: "source-has-no-cue-map" };
604
604
  const preroll = Math.max(0, ...requested.map(track => track.seekPrerollSeconds ?? 0));
605
605
  const firstAt = (seconds, strict) => {
606
606
  let low = 0, high = points.length;
@@ -632,7 +632,7 @@ export class MatroskaContainer extends Container {
632
632
  [Math.max(0, this.fileSize - requestBytes), this.fileSize - 1]];
633
633
  // SeekHead may name top-level declarations after media clusters. Preserve
634
634
  // their original bytes instead of reconstructing declarations for FFmpeg.
635
- for (const at of [layout.infoAt, layout.tracksAt]) {
635
+ for (const at of [layout.infoAt, layout.tracksAt, layout.tagsAt, layout.chaptersAt, layout.attachmentsAt]) {
636
636
  if (at === null || at < layout.firstClusterAt) continue;
637
637
  const end = await elementEnd(at);
638
638
  if (end === null) return { kind: "terminal", reason: "source-declarations-are-unbounded" };