@torrent-tv/proxy 2.6.6 → 2.7.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/package.json +1 -1
- package/services/hls-session-manager.js +119 -3
package/package.json
CHANGED
|
@@ -18,13 +18,23 @@ import { logger } from "../utils/logger.js";
|
|
|
18
18
|
|
|
19
19
|
const PLAYLIST_FILE_NAME = "index.m3u8";
|
|
20
20
|
const SEGMENT_FILE_NAME_PATTERN = /^segment-\d{5}\.ts$/;
|
|
21
|
-
const CLEANUP_INTERVAL_MS =
|
|
21
|
+
const CLEANUP_INTERVAL_MS = 30_000;
|
|
22
22
|
const DEFAULT_SEGMENT_DURATION_SEC = 4;
|
|
23
23
|
// How many segments ahead of the current encode head a missing-segment request
|
|
24
24
|
// is allowed to be before we restart ffmpeg at that position (server-side seek).
|
|
25
25
|
// Requests within the window are served by waiting for the running encode.
|
|
26
26
|
const MAX_LOOKAHEAD_SEGMENTS = 8;
|
|
27
|
-
|
|
27
|
+
// Just-in-time pacing: pause the encoder once it is this many segments ahead of
|
|
28
|
+
// the player's current position, and resume once the lead shrinks to the resume
|
|
29
|
+
// threshold. This keeps a comfortable buffer for smooth playback while capping
|
|
30
|
+
// CPU near real-time instead of racing through the whole file at once.
|
|
31
|
+
const PACING_PAUSE_AHEAD_SEGMENTS = 8;
|
|
32
|
+
const PACING_RESUME_AHEAD_SEGMENTS = 4;
|
|
33
|
+
// Idle TTL: a session is disposed this long after the last segment/playlist
|
|
34
|
+
// access. Kept short so an ffmpeg process does not keep burning CPU after the
|
|
35
|
+
// viewer stops or navigates away. Active playback refreshes the timer on every
|
|
36
|
+
// segment fetch, so it never expires mid-watch.
|
|
37
|
+
const DEFAULT_SESSION_TTL_MS = 120 * 1000;
|
|
28
38
|
const DEFAULT_STARTUP_WAIT_MS = 5_000;
|
|
29
39
|
const MICROSECONDS_PER_SECOND = 1_000_000;
|
|
30
40
|
const PROGRESS_LOG_INTERVAL_MS = 5_000;
|
|
@@ -488,6 +498,11 @@ export class HlsSessionManager {
|
|
|
488
498
|
encodeStartIndex: 0,
|
|
489
499
|
// Guards against repeatedly restarting to the same seek position.
|
|
490
500
|
pendingRestartIndex: -1,
|
|
501
|
+
// Just-in-time pacing: highest segment index the player has requested
|
|
502
|
+
// (≈ playhead) and whether the encoder is currently paused for running
|
|
503
|
+
// far enough ahead.
|
|
504
|
+
playheadIndex: 0,
|
|
505
|
+
paused: false,
|
|
491
506
|
progress: {
|
|
492
507
|
state: "starting",
|
|
493
508
|
processedSeconds: 0,
|
|
@@ -575,14 +590,19 @@ export class HlsSessionManager {
|
|
|
575
590
|
|
|
576
591
|
// Terminate any existing encode process before starting a new one. The
|
|
577
592
|
// old process's exit handler no-ops because session.ffmpeg is reassigned
|
|
578
|
-
// below (it checks identity).
|
|
593
|
+
// below (it checks identity). Resume first in case it was paused, so the
|
|
594
|
+
// termination signal is actually delivered.
|
|
579
595
|
if (session.ffmpeg && !session.ffmpeg.killed) {
|
|
580
596
|
try {
|
|
597
|
+
if (session.paused) {
|
|
598
|
+
session.ffmpeg.kill("SIGCONT");
|
|
599
|
+
}
|
|
581
600
|
session.ffmpeg.kill("SIGTERM");
|
|
582
601
|
} catch (_error) {
|
|
583
602
|
// Best effort.
|
|
584
603
|
}
|
|
585
604
|
}
|
|
605
|
+
session.paused = false;
|
|
586
606
|
|
|
587
607
|
const videoCodecArgs = session.transcodeVideo
|
|
588
608
|
? [
|
|
@@ -649,6 +669,7 @@ export class HlsSessionManager {
|
|
|
649
669
|
session.ffmpeg = ffmpeg;
|
|
650
670
|
session.encodeStartIndex = safeIndex;
|
|
651
671
|
session.pendingRestartIndex = -1;
|
|
672
|
+
session.playheadIndex = safeIndex;
|
|
652
673
|
session.state = session.state === "disposed" ? "disposed" : "starting";
|
|
653
674
|
session.progress.state = "running";
|
|
654
675
|
session.progress.processedSeconds = startSeconds;
|
|
@@ -721,6 +742,8 @@ export class HlsSessionManager {
|
|
|
721
742
|
` speed=${session.progress.speed || "n/a"}`
|
|
722
743
|
);
|
|
723
744
|
}
|
|
745
|
+
// Just-in-time pacing: pause once we are far enough ahead of the player.
|
|
746
|
+
this.#maybePauseEncoder(session);
|
|
724
747
|
}
|
|
725
748
|
});
|
|
726
749
|
|
|
@@ -796,6 +819,79 @@ export class HlsSessionManager {
|
|
|
796
819
|
this.#startEncodeRun(session, index);
|
|
797
820
|
}
|
|
798
821
|
|
|
822
|
+
/**
|
|
823
|
+
* Segment index ffmpeg has encoded up to so far, derived from the output
|
|
824
|
+
* timestamp reported via -progress.
|
|
825
|
+
*
|
|
826
|
+
* @param {HlsSession} session
|
|
827
|
+
* @returns {number}
|
|
828
|
+
*/
|
|
829
|
+
#producedSegmentIndex(session) {
|
|
830
|
+
const processed = Number(session.progress?.processedSeconds);
|
|
831
|
+
if (!Number.isFinite(processed) || processed <= 0) {
|
|
832
|
+
return session.encodeStartIndex;
|
|
833
|
+
}
|
|
834
|
+
return Math.floor(processed / this.segmentDurationSec);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Pause the encoder once it has produced enough segments ahead of the
|
|
839
|
+
* player's current position (just-in-time pacing). No-op on platforms
|
|
840
|
+
* without POSIX job-control signals (fail-open: the encoder keeps running).
|
|
841
|
+
*
|
|
842
|
+
* @param {HlsSession} session
|
|
843
|
+
* @returns {void}
|
|
844
|
+
*/
|
|
845
|
+
#maybePauseEncoder(session) {
|
|
846
|
+
if (session.paused || !session.ffmpeg || session.ffmpeg.killed) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (process.platform === "win32") {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const ahead = this.#producedSegmentIndex(session) - session.playheadIndex;
|
|
853
|
+
if (ahead < PACING_PAUSE_AHEAD_SEGMENTS) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
try {
|
|
857
|
+
session.ffmpeg.kill("SIGSTOP");
|
|
858
|
+
session.paused = true;
|
|
859
|
+
logger.info(
|
|
860
|
+
`transcode ${session.id} paused (buffered ${ahead} segments ahead) "${session.fileName}"`
|
|
861
|
+
);
|
|
862
|
+
} catch (_error) {
|
|
863
|
+
// Fail-open: if we cannot pause, let the encoder keep running.
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Resume a paused encoder when the player has caught up enough that the
|
|
869
|
+
* buffered lead has shrunk to the resume threshold.
|
|
870
|
+
*
|
|
871
|
+
* @param {HlsSession} session
|
|
872
|
+
* @returns {void}
|
|
873
|
+
*/
|
|
874
|
+
#maybeResumeEncoder(session) {
|
|
875
|
+
if (!session.paused || !session.ffmpeg || session.ffmpeg.killed) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const ahead = this.#producedSegmentIndex(session) - session.playheadIndex;
|
|
879
|
+
if (ahead > PACING_RESUME_AHEAD_SEGMENTS) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
session.ffmpeg.kill("SIGCONT");
|
|
884
|
+
session.paused = false;
|
|
885
|
+
session.progress.updatedAt = Date.now();
|
|
886
|
+
logger.info(`transcode ${session.id} resumed (lead ${ahead} segments) "${session.fileName}"`);
|
|
887
|
+
} catch (_error) {
|
|
888
|
+
// If resume fails, force a restart from the player's position so it does
|
|
889
|
+
// not stall (fail-safe toward smooth playback).
|
|
890
|
+
session.paused = false;
|
|
891
|
+
this.#startEncodeRun(session, session.playheadIndex);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
799
895
|
#buildVideoFilter(targetWidth, targetHeight) {
|
|
800
896
|
const safeWidth = Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : 1280;
|
|
801
897
|
const safeHeight = Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : 720;
|
|
@@ -874,6 +970,16 @@ export class HlsSessionManager {
|
|
|
874
970
|
}
|
|
875
971
|
session.lastAccessedAt = Date.now();
|
|
876
972
|
|
|
973
|
+
// Track the player position (≈ playhead) from segment requests and resume
|
|
974
|
+
// a paused encoder as the player approaches the buffered edge.
|
|
975
|
+
if (fileName !== PLAYLIST_FILE_NAME) {
|
|
976
|
+
const requestedIndex = segmentIndexFromName(fileName);
|
|
977
|
+
if (requestedIndex >= 0) {
|
|
978
|
+
session.playheadIndex = Math.max(session.playheadIndex, requestedIndex);
|
|
979
|
+
this.#maybeResumeEncoder(session);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
877
983
|
// Serve the synthetic VOD playlist (full duration, terminated with
|
|
878
984
|
// #EXT-X-ENDLIST) so the player gets the correct total length and a fully
|
|
879
985
|
// seekable timeline up-front, independent of how far ffmpeg has encoded.
|
|
@@ -1021,6 +1127,16 @@ export class HlsSessionManager {
|
|
|
1021
1127
|
this.sessionIdBySource.delete(session.sourceMapKey);
|
|
1022
1128
|
|
|
1023
1129
|
if (session.ffmpeg && !session.ffmpeg.killed) {
|
|
1130
|
+
// Resume first if paused, otherwise SIGTERM is not delivered to a
|
|
1131
|
+
// stopped process.
|
|
1132
|
+
if (session.paused) {
|
|
1133
|
+
try {
|
|
1134
|
+
session.ffmpeg.kill("SIGCONT");
|
|
1135
|
+
} catch (_error) {
|
|
1136
|
+
// Best effort.
|
|
1137
|
+
}
|
|
1138
|
+
session.paused = false;
|
|
1139
|
+
}
|
|
1024
1140
|
session.ffmpeg.kill("SIGTERM");
|
|
1025
1141
|
await waitForChildExit(session.ffmpeg);
|
|
1026
1142
|
}
|