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.
93 lines
3.0 KiB
PHP
93 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Thin Twitch Helix client. Replaces twitch-bot/search-game.py + set-channel.py
|
|
* for use from `php artisan rig:loading`.
|
|
*
|
|
* Requires TWITCH_CLIENT_ID + TWITCH_TOKEN (user OAuth token with the
|
|
* channel:manage:broadcast scope) and TWITCH_BROADCASTER_ID in .env.
|
|
*/
|
|
class TwitchHelix
|
|
{
|
|
private Client $http;
|
|
private string $clientId;
|
|
private string $token;
|
|
private string $broadcasterId;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->clientId = (string) config('rig.twitch.client_id');
|
|
$this->token = (string) config('rig.twitch.token');
|
|
$this->broadcasterId = (string) config('rig.twitch.broadcaster_id');
|
|
$this->http = new Client([
|
|
'base_uri' => 'https://api.twitch.tv/helix/',
|
|
'timeout' => 8.0,
|
|
]);
|
|
}
|
|
|
|
public function enabled(): bool
|
|
{
|
|
return $this->clientId !== '' && $this->token !== '';
|
|
}
|
|
|
|
/**
|
|
* Returns [['id' => '12345', 'name' => 'Half-Life 2'], …] for an exact-name match,
|
|
* or null if Twitch returned no matches.
|
|
*/
|
|
public function searchCategory(string $query): ?array
|
|
{
|
|
try {
|
|
$res = $this->http->get('search/categories', [
|
|
'headers' => $this->headers(),
|
|
'query' => ['query' => $query, 'first' => 20],
|
|
]);
|
|
} catch (GuzzleException $e) {
|
|
throw new RuntimeException('Twitch search failed: ' . $e->getMessage(), 0, $e);
|
|
}
|
|
$body = json_decode((string) $res->getBody(), true);
|
|
$hits = $body['data'] ?? [];
|
|
if (empty($hits)) return null;
|
|
|
|
// Prefer exact (case-insensitive) match; otherwise the first hit Twitch returned.
|
|
foreach ($hits as $row) {
|
|
if (strcasecmp((string) ($row['name'] ?? ''), $query) === 0) {
|
|
return ['id' => (string) $row['id'], 'name' => (string) $row['name']];
|
|
}
|
|
}
|
|
return ['id' => (string) $hits[0]['id'], 'name' => (string) $hits[0]['name']];
|
|
}
|
|
|
|
public function setChannel(string $title, ?string $gameId = null): void
|
|
{
|
|
if ($this->broadcasterId === '') {
|
|
throw new RuntimeException('TWITCH_BROADCASTER_ID not set in .env');
|
|
}
|
|
$body = ['title' => $title];
|
|
if ($gameId !== null && $gameId !== '') $body['game_id'] = $gameId;
|
|
|
|
try {
|
|
$this->http->patch('channels', [
|
|
'headers' => $this->headers() + ['Content-Type' => 'application/json'],
|
|
'query' => ['broadcaster_id' => $this->broadcasterId],
|
|
'json' => $body,
|
|
]);
|
|
} catch (GuzzleException $e) {
|
|
throw new RuntimeException('Twitch PATCH /helix/channels failed: ' . $e->getMessage(), 0, $e);
|
|
}
|
|
}
|
|
|
|
private function headers(): array
|
|
{
|
|
return [
|
|
'Authorization' => 'Bearer ' . $this->token,
|
|
'Client-Id' => $this->clientId,
|
|
];
|
|
}
|
|
}
|