webapp: introduce Laravel app under webapp/ for scene overlays
Replaces the per-scene HTML directories (landing/, loading/, game/,
desktop/, goodbye/, music-box/, music/) with a single Laravel app
serving every overlay over HTTP. Supervisord runs `php artisan serve`
on 127.0.0.1:1118 and the OBS scene JSON now references HTTP routes
instead of file:// URLs.
Highlights:
- public/css/hud.css consolidates the duplicated HUD chrome,
scanlines/vignette/flicker, terminal styling, and pulse keyframes
that were copy-pasted across all seven scenes.
- Blade partials own hud-strip, crt-overlays, obs-ws-scripts,
camera-frame, screen-frame; the seven scenes extend a shared
overlay layout.
- Artisan commands (`rig:setup`, `rig:telemetry`, `rig:loading`,
`rig:playlist`, `rig:cmd`) replace the shell scripts that wrote
per-rig JSON snapshots. TwitchHelix + HardwareSnapshot services
handle the work the bash + Python helpers used to.
- ObsWsClient + MusicCommandController kill the 250 ms cmd.js poll
in the music daemon: POST /cmd/{skip|prev|pause|resume} opens a
short-lived Pawl WS, authenticates, and broadcasts mpd:cmd.
- AudioController streams files from the configured music dirs so
CEF can load tracks under the HTTP origin (Chromium blocks
HTTP-origin pages from loading file:// media).
- DataController serves /data/playlist.js (with ETag mtime cache)
and /cover.jpg (no-store) so the existing overlays' window.__PLAYLIST
and cover.jpg cache-bust pattern keeps working.
scripts/obs-webapp.supervisord.conf is the supervisord unit; install
to /etc/supervisor.d/obs-webapp.conf.
scripts/rewrite-scene-urls.py is a one-shot tool that rewrites
basic/scenes/Default_Stream_HUD.json from file:// to HTTP URLs.
basic/scenes/Default_Stream_HUD.json.pre-webapp is the rollback
artifact (made with OBS closed; full pre-migration state).
The seven old scene directories, vendor/, and the bash scripts are
still on disk pending visual verification; the next commit will
prune them.
This commit is contained in:
101
webapp/app/Console/Commands/RigLoadingCommand.php
Normal file
101
webapp/app/Console/Commands/RigLoadingCommand.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\TwitchHelix;
|
||||
use App\Support\RigData;
|
||||
use Illuminate\Console\Command;
|
||||
use Throwable;
|
||||
|
||||
class RigLoadingCommand extends Command
|
||||
{
|
||||
protected $signature = 'rig:loading {--no-twitch : Skip pushing the channel update to Twitch}';
|
||||
protected $description = 'Interactive Project Loading manifest builder';
|
||||
|
||||
public function handle(RigData $rig, TwitchHelix $twitch): int
|
||||
{
|
||||
$prev = $rig->loading();
|
||||
|
||||
$this->line('── Project Loading manifest ──');
|
||||
$this->line(' (press Enter to keep [defaults])');
|
||||
$this->line('');
|
||||
|
||||
$game = null;
|
||||
$gameId = null;
|
||||
$defaultGame = (string) ($prev['game'] ?? '');
|
||||
while (true) {
|
||||
$query = (string) $this->ask('Game (search)', $defaultGame ?: null);
|
||||
if ($query === '') {
|
||||
$this->error(' ✗ Game is required');
|
||||
continue;
|
||||
}
|
||||
if ($twitch->enabled()) {
|
||||
$cachedId = (string) ($prev['gameId'] ?? '');
|
||||
if ($query === $defaultGame && preg_match('/^\d+$/', $cachedId)) {
|
||||
$game = $defaultGame;
|
||||
$gameId = $cachedId;
|
||||
$this->line(" ↻ reusing cached: {$game} (id={$gameId})");
|
||||
break;
|
||||
}
|
||||
try {
|
||||
$hit = $twitch->searchCategory($query);
|
||||
} catch (Throwable $e) {
|
||||
$this->warn(' ! Twitch search failed: ' . $e->getMessage());
|
||||
if ($this->confirm('Use the raw input without resolving an id?', true)) {
|
||||
$game = $query;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($hit === null) {
|
||||
$this->warn(' ! no Twitch matches for "' . $query . '"');
|
||||
continue;
|
||||
}
|
||||
$game = $hit['name'];
|
||||
$gameId = $hit['id'];
|
||||
$this->line(" ↳ matched: {$game} (id={$gameId})");
|
||||
break;
|
||||
}
|
||||
$game = $query;
|
||||
break;
|
||||
}
|
||||
|
||||
$subtitle = (string) $this->ask('Subtitle (mode / episode / note)', $prev['subtitle'] ?? null);
|
||||
$countdown = (int) $this->ask('Countdown (minutes)', (string) ($prev['countdownMin'] ?? 5));
|
||||
$camera = $this->confirm('Camera', (bool) ($prev['camera'] ?? true));
|
||||
$microphone = $this->confirm('Microphone', (bool) ($prev['microphone'] ?? true));
|
||||
|
||||
$data = [
|
||||
'compiledAt' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||
'game' => $game,
|
||||
'gameId' => $gameId,
|
||||
'subtitle' => $subtitle ?: null,
|
||||
'countdownMin' => $countdown,
|
||||
'camera' => $camera,
|
||||
'microphone' => $microphone,
|
||||
];
|
||||
$path = rtrim(config('rig.storage_data'), '/') . '/loading.json';
|
||||
@mkdir(dirname($path), 0775, true);
|
||||
file_put_contents(
|
||||
$path,
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"
|
||||
);
|
||||
$this->info('');
|
||||
$this->info(" ✓ wrote {$path}");
|
||||
|
||||
// Push to Twitch — soft-fail so a network blip doesn't block the local manifest.
|
||||
if (!$this->option('no-twitch') && $twitch->enabled() && $gameId) {
|
||||
$title = $subtitle ?: $game;
|
||||
try {
|
||||
$twitch->setChannel($title, $gameId);
|
||||
$this->info(" ✓ Twitch channel updated → \"{$title}\" / category {$game}");
|
||||
} catch (Throwable $e) {
|
||||
$this->warn(' ! Twitch sync failed (manifest still saved): ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
$this->line(' → refresh the Project Loading browser source in OBS.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user