fix(stream): keep an idle on-demand stream's stream_info

An adaptive master lists a variant only by the resolution and bitrate in its
stream_info, and stopping an on-demand stream for having no viewers cleared
it: an idle HD/FHD variant was left out of the master, and nothing could
then start it, so players only ever got the variants already running.

The two idle stops (OndemandCommand, the on-demand stop in cron:streams via
StreamProcess::stopStream(..., idle: true)) now keep stream_info; a manual
stop, a restart and every other stop still clear it.

Refs: #61
This commit is contained in:
Divarion_D
2026-10-11 14:23:58 +03:00
parent eb8fe68206
commit 2d00a7c74a
4 changed files with 65 additions and 5 deletions
+2 -1
View File
@@ -193,7 +193,8 @@ class OndemandCommand implements CommandInterface {
@unlink($queueFile);
@unlink(SIGNALS_TMP_PATH . 'admin_' . $rStreamID);
StreamStateWriter::update(intval($rStreamID), intval(SERVER_ID), ['bitrate' => null, 'current_source' => null, 'to_analyze' => 0, 'pid' => null, 'stream_started' => null, 'stream_info' => null, 'audio_codec' => null, 'video_codec' => null, 'resolution' => null, 'compatible' => 0, 'stream_status' => 0, 'monitor_pid' => null], $db);
// stream_info is kept: an adaptive master lists the idle variant by its last probe.
StreamStateWriter::update(intval($rStreamID), intval(SERVER_ID), ['bitrate' => null, 'current_source' => null, 'to_analyze' => 0, 'pid' => null, 'stream_started' => null, 'audio_codec' => null, 'video_codec' => null, 'resolution' => null, 'compatible' => 0, 'stream_status' => 0, 'monitor_pid' => null], $db);
// MAIN refreshes its cache from the node's events once its store keeps the streams.
if (!$rLocal) {
+1 -1
View File
@@ -241,7 +241,7 @@ class StreamsCronJob implements CommandInterface {
}
if ($rQueue == 0 && $rAdminQueue == 0 && $rStream['online_clients'] == 0 && (file_exists(STREAMS_PATH . $rStream['stream_id'] . '_.m3u8') || SettingsManager::getInt('on_demand_wait_time') < time() - intval($rStream['stream_started']) || $rStream['stream_status'] == 1)) {
echo 'Stop on-demand stream...' . "\n\n";
StreamProcess::stopStream($rStream['stream_id'], true);
StreamProcess::stopStream($rStream['stream_id'], true, true);
// Stopped: nothing below applies (it would start a thumbnail
// and a TV archive worker for the stream just stopped).
continue;
+10 -3
View File
@@ -850,13 +850,18 @@ class StreamProcess {
*
* @param int $rStreamID Stream id.
* @param bool $rWithMonitor Also clear monitor_pid (full stop vs. movie stop).
* @param bool $rKeepInfo Keep stream_info: an idle on-demand stream's last
* probe is what an adaptive master lists it by (issue #61).
* @return void
*/
private static function resetStreamServerRow(int $rStreamID, bool $rWithMonitor = false) {
private static function resetStreamServerRow(int $rStreamID, bool $rWithMonitor = false, bool $rKeepInfo = false) {
$rFields = ['bitrate' => null, 'current_source' => null, 'to_analyze' => 0, 'pid' => null, 'stream_started' => null, 'stream_info' => null, 'audio_codec' => null, 'video_codec' => null, 'resolution' => null, 'compatible' => 0, 'stream_status' => 0];
if ($rWithMonitor) {
$rFields['monitor_pid'] = null;
}
if ($rKeepInfo) {
unset($rFields['stream_info']);
}
StreamStateWriter::update($rStreamID, intval(SERVER_ID), $rFields, self::db());
}
@@ -2152,9 +2157,11 @@ class StreamProcess {
*
* @param int $rStreamID Stream id.
* @param bool $rStop Mark the stream as fully stopped (not just restarting).
* @param bool $rIdle An on-demand stream stopped for having no viewers:
* its stream_info is kept (see resetStreamServerRow).
* @return mixed Stop result.
*/
public static function stopStream(int $rStreamID, bool $rStop = false) {
public static function stopStream(int $rStreamID, bool $rStop = false, bool $rIdle = false) {
// A supervised stream is released FIRST: its producer dying is exactly
// what the fanout supervisor restarts, so killing it before the release
// would have the daemon start a replacement and the stream refuse to stop.
@@ -2187,7 +2194,7 @@ class StreamProcess {
if ($rStop) {
shell_exec('rm -f ' . DELAY_PATH . intval($rStreamID) . '_*');
self::resetStreamServerRow($rStreamID, true);
self::resetStreamServerRow($rStreamID, true, $rIdle);
self::updateStream($rStreamID);
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
use PHPUnit\Framework\TestCase;
use XcVm\Domain\Stream\StreamProcess;
use XcVm\Domain\Stream\StreamStateWriter;
use XcVm\Infrastructure\Database\DatabaseFactory;
/**
* An on-demand stream stopped for having no viewers keeps its stream_info: an
* adaptive master lists a variant by its resolution and bitrate, so an idle
* variant that lost them could never be offered, nor started (issue #61). Any
* other stop still clears it.
*/
final class IdleOnDemandStreamInfoTest extends TestCase {
/** @var list<array<string, mixed>> */
private array $rWrites = [];
protected function setUp(): void {
defined('SERVER_ID') || define('SERVER_ID', 1);
DatabaseFactory::set(new TestDb());
StreamStateWriter::useSink(function (string $rWhere, array $rFields): bool {
$this->rWrites[] = $rFields;
return true;
});
}
protected function tearDown(): void {
StreamStateWriter::useSink(null);
DatabaseFactory::reset();
}
/** @return array<string, mixed> the fields the stop writes */
private function reset(bool $rKeepInfo): array {
(new ReflectionMethod(StreamProcess::class, 'resetStreamServerRow'))->invoke(null, 42, true, $rKeepInfo);
$this->assertCount(1, $this->rWrites);
return $this->rWrites[0];
}
public function testAnIdleStopKeepsStreamInfo(): void {
$rFields = $this->reset(true);
$this->assertArrayNotHasKey('stream_info', $rFields);
$this->assertArrayHasKey('pid', $rFields);
$this->assertNull($rFields['pid']);
$this->assertSame(0, $rFields['stream_status']);
}
public function testAnyOtherStopClearsStreamInfo(): void {
$rFields = $this->reset(false);
$this->assertArrayHasKey('stream_info', $rFields);
$this->assertNull($rFields['stream_info']);
}
}