mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-04 04:02:30 +02:00
feat(fanout): make encoder supervision a real, settable option
Production-readiness pass on the panel side of daemon encoder supervision. Until now the feature was gated on a setting that did not exist as a column, so it could only be enabled by hand-editing the database and the daemon never learned about it at all. * Migration 018 adds `fanout_supervise`, off for every existing install. * Admin -> Settings gains the toggle, alongside the other fanout tuning. * SettingsService saves it (it is a checkbox, so it has to be in the boolean list or it can never be turned back off). * FanoutConfig writes it into the daemon's config file as `supervise`, which is how the node learns it may supervise at all. Both halves must be on for anything to change, and either one off is a full rollback. * StreamProcess reads `fanout_supervise` rather than the placeholder name, matching the panel's `fanout_*` convention. Also: the recorded command file now says WHO ran the stream. `_.fanout` when the daemon owns the process (the bare command it was handed) and `_.ffmpeg` when this node ran it itself (with the redirect-and-background tail). Two names rather than one because the first question in any incident is which path the stream took, and a single filename cannot answer it; the stale one is removed so a stream that switched paths does not leave a lie behind. FanoutConfigTest covers the new key both ways, including that a settings array predating it reads as off -- that absence is the upgrade path for every existing install. Verified: php -l clean on every changed file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8244ece100
commit
8e24a2427a
@@ -1778,6 +1778,7 @@ fanout_source_grace = "Fanout Source Grace"
|
||||
fanout_write_timeout = "Fanout Write Timeout"
|
||||
fanout_idle_buffer_grace = "Fanout Idle Buffer Grace"
|
||||
fanout_idle_buffer_ratio = "Fanout Idle Buffer Ratio"
|
||||
fanout_supervise = "Fanout Encoder Supervision"
|
||||
fanout_source_chunk_bytes = "Fanout Source Chunk Bytes"
|
||||
fanout_max_gop_bytes = "Fanout Max GOP Bytes"
|
||||
fanout_source_insecure_tls = "Fanout Source Insecure TLS"
|
||||
|
||||
@@ -34,7 +34,7 @@ class SettingsService {
|
||||
|
||||
$rArray = QueryHelper::verifyPostTable('settings', $rData, true);
|
||||
|
||||
foreach (array('php_loopback', 'restreamer_bypass_proxy', 'request_prebuffer', 'modal_edit', 'group_buttons', 'enable_search', 'on_demand_checker', 'ondemand_balance_equal', 'disable_mag_token', 'allow_cdn_access', 'dts_legacy_ffmpeg', 'mag_load_all_channels', 'disable_xmltv_restreamer', 'disable_playlist_restreamer', 'ffmpeg_warnings', 'reseller_ssl_domain', 'extract_subtitles', 'show_category_duplicates', 'vod_sort_newest', 'header_stats', 'mag_keep_extension', 'keep_protocol', 'read_native_hls', 'player_allow_playlist', 'player_allow_bouquet', 'player_hide_incompatible', 'player_allow_hevc', 'force_epg_timezone', 'check_vod', 'ignore_keyframes', 'save_login_logs', 'save_restart_logs', 'mag_legacy_redirect', 'restrict_playlists', 'monitor_connection_status', 'kill_rogue_ffmpeg', 'show_images', 'on_demand_instant_off', 'on_demand_failure_exit', 'playlist_from_mysql', 'ignore_invalid_users', 'legacy_mag_auth', 'ministra_allow_blank', 'block_proxies', 'block_streaming_servers', 'ip_subnet_match', 'auto_unban_ip', 'debug_show_errors', 'enable_debug_stalker', 'restart_php_fpm', 'restream_deny_unauthorised', 'api_probe', 'legacy_panel_api', 'hide_failures', 'verify_host', 'encrypt_playlist', 'encrypt_playlist_restreamer', 'mag_disable_ssl', 'legacy_get', 'legacy_xmltv', 'save_closed_connection', 'show_tickets', 'stream_logs_save', 'client_logs_save', 'streams_grouped', 'cloudflare', 'cleanup', 'dashboard_stats', 'dashboard_status', 'dashboard_map', 'dashboard_display_alt', 'recaptcha_enable', 'ip_logout', 'disable_player_api', 'disable_playlist', 'disable_xmltv', 'disable_enigma2', 'disable_ministra', 'enable_isp_lock', 'block_svp', 'disable_ts', 'disable_ts_allow_restream', 'disable_hls', 'disable_hls_allow_restream', 'disable_rtmp', 'disable_rtmp_allow_restream', 'case_sensitive_line', 'county_override_1st', 'disallow_2nd_ip_con', 'use_mdomain_in_lists', 'encrypt_hls', 'disallow_empty_user_agents', 'detect_restream_block_user', 'download_images', 'api_redirect', 'use_buffer', 'audio_restart_loss', 'show_isps', 'priority_backup', 'rtmp_random', 'show_connected_video', 'show_not_on_air_video', 'show_banned_video', 'show_expired_video', 'show_expiring_video', 'show_all_category_mag', 'always_enabled_subtitles', 'enable_connection_problem_indication', 'show_tv_channel_logo', 'show_channel_logo_in_preview', 'disable_trial', 'restrict_same_ip', 'fanout_source_insecure', 'js_navigate') as $rSetting) {
|
||||
foreach (array('php_loopback', 'restreamer_bypass_proxy', 'request_prebuffer', 'modal_edit', 'group_buttons', 'enable_search', 'on_demand_checker', 'ondemand_balance_equal', 'disable_mag_token', 'allow_cdn_access', 'dts_legacy_ffmpeg', 'mag_load_all_channels', 'disable_xmltv_restreamer', 'disable_playlist_restreamer', 'ffmpeg_warnings', 'reseller_ssl_domain', 'extract_subtitles', 'show_category_duplicates', 'vod_sort_newest', 'header_stats', 'mag_keep_extension', 'keep_protocol', 'read_native_hls', 'player_allow_playlist', 'player_allow_bouquet', 'player_hide_incompatible', 'player_allow_hevc', 'force_epg_timezone', 'check_vod', 'ignore_keyframes', 'save_login_logs', 'save_restart_logs', 'mag_legacy_redirect', 'restrict_playlists', 'monitor_connection_status', 'kill_rogue_ffmpeg', 'show_images', 'on_demand_instant_off', 'on_demand_failure_exit', 'playlist_from_mysql', 'ignore_invalid_users', 'legacy_mag_auth', 'ministra_allow_blank', 'block_proxies', 'block_streaming_servers', 'ip_subnet_match', 'auto_unban_ip', 'debug_show_errors', 'enable_debug_stalker', 'restart_php_fpm', 'restream_deny_unauthorised', 'api_probe', 'legacy_panel_api', 'hide_failures', 'verify_host', 'encrypt_playlist', 'encrypt_playlist_restreamer', 'mag_disable_ssl', 'legacy_get', 'legacy_xmltv', 'save_closed_connection', 'show_tickets', 'stream_logs_save', 'client_logs_save', 'streams_grouped', 'cloudflare', 'cleanup', 'dashboard_stats', 'dashboard_status', 'dashboard_map', 'dashboard_display_alt', 'recaptcha_enable', 'ip_logout', 'disable_player_api', 'disable_playlist', 'disable_xmltv', 'disable_enigma2', 'disable_ministra', 'enable_isp_lock', 'block_svp', 'disable_ts', 'disable_ts_allow_restream', 'disable_hls', 'disable_hls_allow_restream', 'disable_rtmp', 'disable_rtmp_allow_restream', 'case_sensitive_line', 'county_override_1st', 'disallow_2nd_ip_con', 'use_mdomain_in_lists', 'encrypt_hls', 'disallow_empty_user_agents', 'detect_restream_block_user', 'download_images', 'api_redirect', 'use_buffer', 'audio_restart_loss', 'show_isps', 'priority_backup', 'rtmp_random', 'show_connected_video', 'show_not_on_air_video', 'show_banned_video', 'show_expired_video', 'show_expiring_video', 'show_all_category_mag', 'always_enabled_subtitles', 'enable_connection_problem_indication', 'show_tv_channel_logo', 'show_channel_logo_in_preview', 'disable_trial', 'restrict_same_ip', 'fanout_source_insecure', 'fanout_supervise', 'js_navigate') as $rSetting) {
|
||||
if (isset($rData[$rSetting])) {
|
||||
$rArray[$rSetting] = 1;
|
||||
} else {
|
||||
|
||||
@@ -946,14 +946,15 @@ class StreamProcess {
|
||||
* Whether this node hands live encoders to the fanout daemon to supervise
|
||||
* instead of running them itself under the PHP watchdog.
|
||||
*
|
||||
* Off unless `daemon_supervise` is set, so a panel that has never heard of
|
||||
* the setting keeps the legacy behaviour exactly — no migration needed, and
|
||||
* clearing the setting is the rollback.
|
||||
* Off unless `fanout_supervise` is set. A panel that predates the setting
|
||||
* reads null and keeps the legacy behaviour exactly, and clearing it is the
|
||||
* rollback: MonitorCommand resumes, because its stand-down check asks the
|
||||
* daemon rather than assuming, and an unreachable daemon answers "no".
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function daemonSupervises(): bool {
|
||||
return (bool) SettingsManager::get('daemon_supervise');
|
||||
return (bool) SettingsManager::get('fanout_supervise');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1633,9 +1634,16 @@ class StreamProcess {
|
||||
$rFFMPEG .= self::liveRedirectTail($rStreamID);
|
||||
shell_exec($rFFMPEG);
|
||||
}
|
||||
// Record what actually ran: the bare command when the daemon owns
|
||||
// the process, the backgrounded one when this node does.
|
||||
file_put_contents(STREAMS_PATH . $rStreamID . '_.ffmpeg', $rFFMPEG);
|
||||
// Record what actually ran, under a name that says WHO ran it:
|
||||
// `_.fanout` when the daemon owns the process (the bare command it
|
||||
// was handed), `_.ffmpeg` when this node ran it itself (with the
|
||||
// redirect-and-background tail). Two names rather than one because
|
||||
// the first question in any incident is which path the stream took,
|
||||
// and a single file cannot answer it. The stale one is removed so a
|
||||
// stream that switched paths does not leave a lie behind.
|
||||
$rCmdFile = $rHandedOver ? '_.fanout' : '_.ffmpeg';
|
||||
file_put_contents(STREAMS_PATH . $rStreamID . $rCmdFile, $rFFMPEG);
|
||||
@unlink(STREAMS_PATH . $rStreamID . ($rHandedOver ? '_.ffmpeg' : '_.fanout'));
|
||||
|
||||
// Wait briefly for PID file to be written, with retry
|
||||
$rPID = 0;
|
||||
|
||||
@@ -1555,6 +1555,16 @@ use XcVm\Streaming\Codec\FfmpegBinaries; // Code reconstruction by Squallp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-4">
|
||||
<label class="col-md-4 col-form-label" for="fanout_supervise">
|
||||
<?= $language::get('fanout_supervise') ?>
|
||||
<i class="icon-base ti tabler-info-circle text-body-secondary" data-bs-toggle="tooltip" title="xc_fanout: let the daemon run and watch stream encoders instead of one PHP monitor process per stream. The node's daemon must also have supervision enabled. Off = the PHP watchdog keeps running."></i>
|
||||
</label>
|
||||
<div class="col-md-2">
|
||||
<div class="form-check form-switch"><input name="fanout_supervise" id="fanout_supervise" type="checkbox" <?= ($rSettings["fanout_supervise"] ?? 0) == 1 ? ' checked' : '' ?> class="form-check-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-4">
|
||||
<label class="col-md-4 col-form-label" for="split_by">
|
||||
<?= $language::get('load_balancing') ?>
|
||||
|
||||
@@ -121,6 +121,10 @@ class FanoutConfig {
|
||||
'idle_buffer_grace_sec' => self::clampInt((int) ($rSettings['fanout_idle_buffer_grace_sec'] ?? 30), 0, 3600),
|
||||
'idle_buffer_ratio' => $rRatio,
|
||||
'source_backend' => self::backend((string) ($rSettings['fanout_source_backend'] ?? 'auto')),
|
||||
// Whether this node's daemon may run and watch stream encoders at all.
|
||||
// The panel handing a stream over is the other half; both must be on
|
||||
// for anything to change, and either one off is a full rollback.
|
||||
'supervise' => (bool) ($rSettings['fanout_supervise'] ?? false),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- xc_fanout: let the daemon run and watch this node's stream encoders instead of
|
||||
-- a per-stream PHP watchdog (console.php monitor). See the daemon's
|
||||
-- docs/adr/0002-monitor-in-daemon.md.
|
||||
--
|
||||
-- Off for every existing install. Turning it on is a two-sided opt-in: the panel
|
||||
-- hands streams over, and the node's daemon must itself be configured to accept
|
||||
-- them (the `supervise` key in its config.json, which FanoutConfig writes from
|
||||
-- this setting). Turning it off is the rollback -- MonitorCommand resumes, since
|
||||
-- its stand-down check asks the daemon and an unreachable one answers "no".
|
||||
ALTER TABLE `settings`
|
||||
ADD COLUMN IF NOT EXISTS `fanout_supervise` tinyint(1) DEFAULT 0 AFTER `fanout_source_backend`;
|
||||
@@ -67,6 +67,33 @@ final class FanoutConfigTest extends TestCase {
|
||||
$this->assertSame(0.5, $c['idle_buffer_ratio']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encoder supervision is off unless the panel says otherwise, and a panel
|
||||
* that predates the setting must read as off rather than as anything else --
|
||||
* that absence is the upgrade path for every existing install.
|
||||
*/
|
||||
public function testSuperviseDefaultsOff(): void {
|
||||
$rSettings = $this->baseSettings();
|
||||
unset($rSettings['fanout_supervise']);
|
||||
$this->assertTrue(FanoutConfig::sync($rSettings));
|
||||
$this->assertFalse($this->read()['supervise']);
|
||||
|
||||
$rSettings['fanout_supervise'] = 0;
|
||||
$this->assertTrue(FanoutConfig::sync($rSettings));
|
||||
$this->assertFalse($this->read()['supervise']);
|
||||
}
|
||||
|
||||
/**
|
||||
* And it reaches the daemon's config file when it IS set, since that file is
|
||||
* the only way the node learns it may supervise at all.
|
||||
*/
|
||||
public function testSuperviseReachesTheDaemonConfig(): void {
|
||||
$rSettings = $this->baseSettings();
|
||||
$rSettings['fanout_supervise'] = 1;
|
||||
$this->assertTrue(FanoutConfig::sync($rSettings));
|
||||
$this->assertTrue($this->read()['supervise']);
|
||||
}
|
||||
|
||||
public function testPrebufferMaxSecIsDerivedAndFloored(): void {
|
||||
// client 30, restreamer 0, hls_window*seg = 6*6 = 36 → floor 40.
|
||||
$this->assertTrue(FanoutConfig::sync($this->baseSettings()));
|
||||
|
||||
Reference in New Issue
Block a user