92 lines
3.1 KiB
PHP
92 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Plugin\AzuraCastOnDemandHls\Controller;
|
|
|
|
use App\Controller\SingleActionInterface;
|
|
use App\Http\Response;
|
|
use App\Http\ServerRequest;
|
|
use Plugin\AzuraCastOnDemandHls\Config;
|
|
use Plugin\AzuraCastOnDemandHls\Service\AssetAuthorizer;
|
|
use Plugin\AzuraCastOnDemandHls\Service\CleanupService;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use Throwable;
|
|
|
|
final readonly class ServePlaybackAssetAction implements SingleActionInterface
|
|
{
|
|
public function __construct(
|
|
private Config $config,
|
|
private AssetAuthorizer $authorizer,
|
|
private CleanupService $cleanup,
|
|
private LoggerInterface $logger,
|
|
) {
|
|
}
|
|
|
|
public function __invoke(
|
|
ServerRequest $request,
|
|
Response $response,
|
|
array $params,
|
|
): ResponseInterface {
|
|
if (!$this->config->enabled) {
|
|
return $this->denied($response);
|
|
}
|
|
|
|
$station = $request->getStation();
|
|
$token = (string)($params['token'] ?? '');
|
|
$resource = (string)($params['resource'] ?? '');
|
|
$authorized = $this->authorizer->authorize(
|
|
$station->getRadioTempDir(),
|
|
$station->id,
|
|
$token,
|
|
$resource,
|
|
);
|
|
|
|
if (null === $authorized) {
|
|
$this->logger->warning('Rejected on-demand HLS asset request.', [
|
|
'station_id' => $station->id,
|
|
'resource' => $resource,
|
|
'token_fingerprint' => '' !== $token ? substr(hash('sha256', $token), 0, 12) : null,
|
|
]);
|
|
return $this->denied($response);
|
|
}
|
|
|
|
// Keep cleanup request-driven without adding latency to every segment request.
|
|
if (0 === random_int(0, 99)) {
|
|
try {
|
|
$this->cleanup->run($station->getRadioTempDir());
|
|
} catch (Throwable $exception) {
|
|
// Delivery is already authorized; maintenance failures must not
|
|
// interrupt playback or turn a recoverable cleanup issue into 5xx.
|
|
$this->logger->warning('Request-driven on-demand HLS cleanup failed.', [
|
|
'station_id' => $station->id,
|
|
'exception' => $exception,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$accelPath = '/internal/ondemand-hls/' . $station->id
|
|
. '/' . $authorized['session']->cacheKey . '/' . $resource;
|
|
|
|
$contentType = str_ends_with($resource, '.m3u8')
|
|
? 'application/vnd.apple.mpegurl'
|
|
: 'video/mp2t';
|
|
|
|
return $response
|
|
->withHeader('Content-Type', $contentType)
|
|
->withHeader('Content-Disposition', 'inline')
|
|
->withHeader('Cache-Control', 'private, no-store, max-age=0')
|
|
->withHeader('Pragma', 'no-cache')
|
|
->withHeader('X-Content-Type-Options', 'nosniff')
|
|
->withHeader('X-Accel-Redirect', $accelPath);
|
|
}
|
|
|
|
private function denied(Response $response): ResponseInterface
|
|
{
|
|
return $response
|
|
->withJson(['error' => 'Invalid or expired playback session.'], 403)
|
|
->withHeader('Cache-Control', 'no-store');
|
|
}
|
|
}
|