@torrent-tv/proxy 2.9.45 → 2.9.46

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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.9.46
2
+
3
+ - **New**: `getFileStats` now reports `resumeNeededBytes` / `resumeDownloadedBytes` — the bytes still to download in the 16 MB window ahead of the file's current read position (tracked per file by `prioritizeByteRange`, cleared on torrent removal), counted byte-accurately including partial pieces. Lets the browser show how much is left to download and the time to resume while buffering.
4
+
1
5
  ## 2.9.45
2
6
 
3
7
  - **New**: HLS transcode output switched from MPEG-TS (`.ts`) to **fMP4/CMAF** (`.m4s` segments + a shared `init.mp4`). Codec parameter sets (SPS/PPS) now live once in the init segment (referenced by `#EXT-X-MAP`) instead of being repeated in every segment. Benefits: (1) hardware encoders that do not repeat parameter sets — notably the CM4 / HA-Yellow `h264_v4l2m2m` — produce independently-usable segments (on `.ts` the segments after the first lacked SPS/PPS → "non-existing PPS", which is why v4l2m2m was rejected); (2) lower container overhead. The synthetic VOD playlist now emits `#EXT-X-VERSION:7` + `#EXT-X-MAP`; each seek-restart run rewrites `init.mp4`, so `getFileStream` caches and serves the FIRST init for the whole session — it is codec-config-only and position-independent (verified: a single init cleanly decodes segments produced by a later seek-restart run). Raised v4l2m2m `-num_capture_buffers` to 32 (the default 4 deadlocks / drops frames on the CM4). **Verified**: server-side clean decode of the synthetic playlist across seek-restart runs; end-to-end playback **and seek** in hls.js 1.6.16. **Still needs**: verification on native iOS HLS (Safari fMP4) before relying on it. NOTE: v4l2m2m still emits a residual no-picture access unit that the strict startup test rejects, so it continues to fall back to software for now (no regression); fMP4 removes the SPS/PPS blocker — the remaining quirk is separate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.45",
3
+ "version": "2.9.46",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -295,6 +295,16 @@ export class TorrentPool {
295
295
  */
296
296
  #lastAccess = new Map();
297
297
 
298
+ /**
299
+ * Last byte offset each active reader is streaming from, keyed by torrent then
300
+ * fileIndex. Set by prioritizeByteRange on every /stream range request; read by
301
+ * getFileStats to report how much of the window ahead of the read head is still
302
+ * to download — the "amount left to resume" shown while buffering.
303
+ *
304
+ * @type {Map<import("webtorrent").Torrent, Map<number, number>>}
305
+ */
306
+ #readPositionByTorrent = new Map();
307
+
298
308
  /** Global disk cap in bytes (0 = disabled). */
299
309
  #maxDiskBytes = 0;
300
310
 
@@ -700,6 +710,7 @@ export class TorrentPool {
700
710
  }
701
711
  this.fileUsageByTorrent.delete(torrent);
702
712
  this.#lastAccess.delete(torrent);
713
+ this.#readPositionByTorrent.delete(torrent);
703
714
  const name = typeof torrent.name === "string" ? torrent.name : "(unknown)";
704
715
  try {
705
716
  torrent.destroy({ destroyStore: true }, () => {
@@ -743,6 +754,14 @@ export class TorrentPool {
743
754
 
744
755
  const header = this.#getHeaderRangeProgress(torrent, file);
745
756
 
757
+ // Bytes still to download in the window ahead of where this file is being
758
+ // read — "how much left to resume". Null until a read position is known.
759
+ const readPositions = this.#readPositionByTorrent.get(torrent);
760
+ const readByteStart = readPositions ? readPositions.get(fileIndex) : undefined;
761
+ const resume = typeof readByteStart === "number"
762
+ ? this.#getResumeWindowProgress(torrent, file, readByteStart)
763
+ : null;
764
+
746
765
  // Null-safe downloaded/progress (webtorrent's own getters throw on a
747
766
  // deselected null piece — see fileDownloadedBytes).
748
767
  const fileLength = typeof file.length === "number" ? file.length : 0;
@@ -753,6 +772,9 @@ export class TorrentPool {
753
772
  fileProgress: fileLength > 0 ? Math.max(0, Math.min(1, fileDownloaded / fileLength)) : 0,
754
773
  fileDownloaded,
755
774
  fileLength,
775
+ // Resume window (ahead of the read head): bytes needed vs downloaded.
776
+ resumeNeededBytes: resume ? resume.totalBytes : null,
777
+ resumeDownloadedBytes: resume ? resume.downloadedBytes : null,
756
778
  // Phase-1 progress: how much of the header/index region (the bytes the
757
779
  // codec probe needs before transcoding can start) is downloaded. Counted
758
780
  // by whole pieces from the torrent bitfield, so it advances coarsely
@@ -808,6 +830,45 @@ export class TorrentPool {
808
830
  return { totalBytes, downloadedBytes };
809
831
  }
810
832
 
833
+ /**
834
+ * Count, by whole torrent pieces, how many bytes of the window AHEAD of the
835
+ * current read position are downloaded — i.e. how much is still to download
836
+ * before playback can continue from that point. Mirrors
837
+ * {@link #getHeaderRangeProgress}, for the moving read head.
838
+ *
839
+ * @param {import("webtorrent").Torrent} torrent
840
+ * @param {import("webtorrent").TorrentFile} file
841
+ * @param {number} readByteStart - Byte offset within the file the reader is at.
842
+ * @returns {{ totalBytes: number, downloadedBytes: number } | null}
843
+ */
844
+ #getResumeWindowProgress(torrent, file, readByteStart) {
845
+ const pieceLength = Number(torrent?.pieceLength);
846
+ const bitfield = torrent?.bitfield;
847
+ const fileLength = Number(file?.length);
848
+ if (
849
+ !Number.isFinite(pieceLength) || pieceLength <= 0 ||
850
+ !bitfield || typeof bitfield.get !== "function" ||
851
+ !Number.isFinite(fileLength) || fileLength <= 0
852
+ ) {
853
+ return null;
854
+ }
855
+ const fileOffset = Number.isFinite(file.offset) ? file.offset : 0;
856
+ const windowStart = Math.max(0, Math.min(Number(readByteStart) || 0, fileLength - 1));
857
+ const windowEnd = Math.min(fileLength - 1, windowStart + PRIORITY_WINDOW_BYTES - 1);
858
+ const firstPiece = Math.floor((fileOffset + windowStart) / pieceLength);
859
+ const lastPiece = Math.floor((fileOffset + windowEnd) / pieceLength);
860
+ // Byte-accurate: count the PARTIAL progress of in-progress pieces (not whole
861
+ // pieces), so "amount left" moves smoothly instead of jumping by a whole
862
+ // piece (8 MB here) at a time.
863
+ let totalBytes = 0;
864
+ let downloadedBytes = 0;
865
+ for (let piece = firstPiece; piece <= lastPiece; piece += 1) {
866
+ totalBytes += pieceLength;
867
+ downloadedBytes += pieceDownloadedBytes(torrent, piece);
868
+ }
869
+ return { totalBytes, downloadedBytes };
870
+ }
871
+
811
872
  /**
812
873
  * Pre-fetch the leading and trailing bytes of a torrent file so that
813
874
  * WebTorrent prioritises the pieces that contain file headers and footers.
@@ -967,6 +1028,16 @@ export class TorrentPool {
967
1028
  const fileEndPiece = Math.floor((fileOffset + fileLength - 1) / pieceLength);
968
1029
 
969
1030
  const safeStart = Math.max(0, Number(byteStart) || 0);
1031
+
1032
+ // Remember where this file is being read from, so getFileStats can report the
1033
+ // download progress of the window ahead of the read head (resume amount).
1034
+ let readPositions = this.#readPositionByTorrent.get(torrent);
1035
+ if (!readPositions) {
1036
+ readPositions = new Map();
1037
+ this.#readPositionByTorrent.set(torrent, readPositions);
1038
+ }
1039
+ readPositions.set(fileIndex, safeStart);
1040
+
970
1041
  const absStart = fileOffset + safeStart;
971
1042
  const playheadPiece = Math.floor(absStart / pieceLength);
972
1043
  const absWindowEnd = Math.min(