Some work to try to get direct play working...

So far it works, but there is an issue with keyframes we need to find a way to either cut the segments based on keyframes for direct play or we should have a background service doing the transcoding.
This commit is contained in:
Abdulmhsen B. A. A.
2024-08-21 20:52:07 +03:00
parent 3be7be7c81
commit 85f3742089
2 changed files with 77 additions and 53 deletions

View File

@@ -449,7 +449,6 @@ onMounted(async () => {
const updateHwAccel = codec => { const updateHwAccel = codec => {
const codecInfo = item.value.hardware.codecs.filter(c => c.codec === codec); const codecInfo = item.value.hardware.codecs.filter(c => c.codec === codec);
console.log(codecInfo)
if (codecInfo.length < 1) { if (codecInfo.length < 1) {
config.value.hwaccel = false config.value.hwaccel = false
return; return;

View File

@@ -94,6 +94,7 @@ readonly class Segments
$isVAAPI = $hwaccel && 'h264_vaapi' === $vCodec; $isVAAPI = $hwaccel && 'h264_vaapi' === $vCodec;
$isQSV = $hwaccel && 'h264_qsv' === $vCodec; $isQSV = $hwaccel && 'h264_qsv' === $vCodec;
$segmentSize = number_format((int)$params->get('segment_size', Playlist::SEGMENT_DUR), 6); $segmentSize = number_format((int)$params->get('segment_size', Playlist::SEGMENT_DUR), 6);
$directPlay = null === $subtitle && null === $external && $params->has('direct_play');
if ($isVAAPI && false === file_exists($vaapi_device)) { if ($isVAAPI && false === file_exists($vaapi_device)) {
return api_error(r("VAAPI device '{device}' not found.", ['device' => $vaapi_device]), Status::BAD_REQUEST); return api_error(r("VAAPI device '{device}' not found.", ['device' => $vaapi_device]), Status::BAD_REQUEST);
@@ -119,17 +120,19 @@ readonly class Segments
usleep(20000); usleep(20000);
} }
$cmd = [ $directPlay = $directPlay && str_ends_with($this->getStream(ag($json, 'streams', []), 0)['codec_name'], '264');
'ffmpeg',
'-ss', $cmd = ['ffmpeg'];
(string)($segment === 0 ? 0 : ($segmentSize * $segment)), if (false === $directPlay) {
'-t', $cmd[] = '-ss';
(string)(ag($request->getQueryParams(), 'sd', $segmentSize)), $cmd[] = (string)($segment === 0 ? 0 : ($segmentSize * $segment));
'-xerror', $cmd[] = '-t';
'-hide_banner', $cmd[] = (string)(ag($request->getQueryParams(), 'sd', $segmentSize));
'-loglevel', }
'error', $cmd[] = '-xerror';
]; $cmd[] = '-hide_banner';
$cmd[] = '-loglevel';
$cmd[] = 'error';
$tmpSubFile = null; $tmpSubFile = null;
$tmpVidFile = r("{path}/t-{name}-vlink.{type}", [ $tmpVidFile = r("{path}/t-{name}-vlink.{type}", [
@@ -146,9 +149,11 @@ readonly class Segments
symlink($path, $tmpVidFile); symlink($path, $tmpVidFile);
} }
$cmd[] = '-copyts'; if (false === $directPlay) {
$cmd[] = '-copyts';
}
if ($isQSV) { if ($isQSV && false === $directPlay) {
$cmd[] = '-hwaccel'; $cmd[] = '-hwaccel';
$cmd[] = 'qsv'; $cmd[] = 'qsv';
if ($overlay) { if ($overlay) {
@@ -157,7 +162,7 @@ readonly class Segments
} }
} }
if ($isVAAPI) { if ($isVAAPI && false === $directPlay) {
$cmd[] = '-hwaccel'; $cmd[] = '-hwaccel';
$cmd[] = 'vaapi'; $cmd[] = 'vaapi';
$cmd[] = '-vaapi_device'; $cmd[] = '-vaapi_device';
@@ -171,6 +176,13 @@ readonly class Segments
$cmd[] = '-i'; $cmd[] = '-i';
$cmd[] = 'file:' . $tmpVidFile; $cmd[] = 'file:' . $tmpVidFile;
if (true === $directPlay) {
$cmd[] = '-ss';
$cmd[] = (string)($segment === 0 ? 0 : ($segmentSize * $segment));
$cmd[] = '-t';
$cmd[] = (string)(ag($request->getQueryParams(), 'sd', $segmentSize));
}
# remove garbage metadata. # remove garbage metadata.
$cmd[] = '-map_metadata'; $cmd[] = '-map_metadata';
$cmd[] = '-1'; $cmd[] = '-1';
@@ -180,8 +192,13 @@ readonly class Segments
$cmd[] = '-pix_fmt'; $cmd[] = '-pix_fmt';
$cmd[] = $params->get('pix_fmt', 'yuv420p'); $cmd[] = $params->get('pix_fmt', 'yuv420p');
$cmd[] = '-g'; if (true === $directPlay) {
$cmd[] = '52'; $cmd[] = '-force_key_frames';
$cmd[] = 'expr:gte(t,n_forced*' . (int)$sConfig['segment_size'] . ')';
} else {
$cmd[] = '-g';
$cmd[] = '52';
}
if ($overlay && empty($external) && null !== $subtitle) { if ($overlay && empty($external) && null !== $subtitle) {
$cmd[] = '-filter_complex'; $cmd[] = '-filter_complex';
@@ -201,28 +218,30 @@ readonly class Segments
$cmd[] = '-strict'; $cmd[] = '-strict';
$cmd[] = '-2'; $cmd[] = '-2';
if (empty($external) && $isVAAPI) { if (empty($external) && $isVAAPI && false === $directPlay) {
$cmd[] = '-vf'; $cmd[] = '-vf';
$cmd[] = 'format=nv12,hwupload'; $cmd[] = 'format=nv12,hwupload';
} }
$cmd[] = '-codec:v'; $cmd[] = '-codec:v';
$cmd[] = $vCodec; $cmd[] = $directPlay ? 'copy' : $vCodec;
$cmd[] = '-crf'; if (false === $directPlay) {
$cmd[] = $params->get('video_crf', '23'); $cmd[] = '-crf';
$cmd[] = '-preset:v'; $cmd[] = $params->get('video_crf', '23');
$cmd[] = $params->get('video_preset', 'fast'); $cmd[] = '-preset:v';
$cmd[] = $params->get('video_preset', 'fast');
if (0 !== (int)$params->get('video_bitrate', 0)) { if (0 !== (int)$params->get('video_bitrate', 0)) {
$cmd[] = '-b:v'; $cmd[] = '-b:v';
$cmd[] = $params->get('video_bitrate', '192k'); $cmd[] = $params->get('video_bitrate', '192k');
}
$cmd[] = '-level';
$cmd[] = $params->get('video_level', '4.1');
$cmd[] = '-profile:v';
$cmd[] = $params->get('video_profile', 'main');
} }
$cmd[] = '-level';
$cmd[] = $params->get('video_level', '4.1');
$cmd[] = '-profile:v';
$cmd[] = $params->get('video_profile', 'main');
// -- audio section. // -- audio section.
$cmd[] = '-map'; $cmd[] = '-map';
$cmd[] = null === $audio ? '0:a:0' : "0:{$audio}"; $cmd[] = null === $audio ? '0:a:0' : "0:{$audio}";
@@ -270,12 +289,16 @@ readonly class Segments
$cmd[] = '-sn'; $cmd[] = '-sn';
} }
if (true === $directPlay) {
$cmd[] = '-output_ts_offset';
$cmd[] = (string)($segment * $segmentSize);
}
$cmd[] = '-muxdelay'; $cmd[] = '-muxdelay';
$cmd[] = '0'; $cmd[] = '0';
$cmd[] = '-f'; $cmd[] = '-f';
$cmd[] = 'mpegts'; $cmd[] = 'mpegts';
$cmd[] = 'pipe:1'; $cmd[] = 'pipe:1';
$debug = (bool)ag($sConfig, 'debug', false); $debug = (bool)ag($sConfig, 'debug', false);
try { try {
@@ -298,7 +321,7 @@ readonly class Segments
'stderr' => $process->getErrorOutput(), 'stderr' => $process->getErrorOutput(),
'Ffmpeg' => $process->getCommandLine(), 'Ffmpeg' => $process->getCommandLine(),
'config' => $sConfig, 'config' => $sConfig,
'command' => implode(' ', $cmd), 'command' => $this->cmdLog($cmd),
] ]
); );
@@ -314,7 +337,7 @@ readonly class Segments
if (true === $debug) { if (true === $debug) {
$response = $response $response = $response
->withHeader('X-Ffmpeg', $process->getCommandLine()) ->withHeader('X-Ffmpeg', $this->cmdLog($cmd))
->withHeader( ->withHeader(
'X-Transcode-Config', 'X-Transcode-Config',
json_encode($sConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) json_encode($sConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
@@ -325,23 +348,24 @@ readonly class Segments
} }
$response = api_response(Status::OK, body: Stream::create($process->getOutput()), headers: [ $response = api_response(Status::OK, body: Stream::create($process->getOutput()), headers: [
// 'Access-Control-Allow-Origin' => '*',
'Content-Type' => 'video/mpegts', 'Content-Type' => 'video/mpegts',
'X-Transcode-Time' => round($end - $start, 6), 'X-Transcode-Time' => round($end - $start, 6),
'X-Emitter-Flush' => 1, 'X-Emitter-Flush' => 1,
'Pragma' => 'public',
'Access-Control-Allow-Origin' => '*',
'Cache-Control' => sprintf('public, max-age=%s', time() + 31536000),
'Last-Modified' => sprintf('%s GMT', gmdate('D, d M Y H:i:s', time())),
'Expires' => sprintf('%s GMT', gmdate('D, d M Y H:i:s', time() + 31536000)),
]); ]);
if (true === $debug) { if (true === $debug) {
$response = $response $response = $response
->withHeader('X-Ffmpeg', $process->getCommandLine()) ->withHeader('X-Ffmpeg', $this->cmdLog($cmd))
->withHeader( ->withHeader(
'X-Transcode-Config', 'X-Transcode-Config',
json_encode($sConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) json_encode($sConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
); );
} else {
$response = $response->withHeader('Pragma', 'public')
->withHeader('Cache-Control', sprintf('public, max-age=%s', time() + 31536000))
->withHeader('Last-Modified', sprintf('%s GMT', gmdate('D, d M Y H:i:s', time())))
->withHeader('Expires', sprintf('%s GMT', gmdate('D, d M Y H:i:s', time() + 31536000)));
} }
return $response; return $response;
@@ -349,7 +373,7 @@ readonly class Segments
$this->logger->error("Failed to generate segment. '{error}' at {file}:{line}", [ $this->logger->error("Failed to generate segment. '{error}' at {file}:{line}", [
'stdout' => isset($process) ? $process->getOutput() : null, 'stdout' => isset($process) ? $process->getOutput() : null,
'stderr' => isset($process) ? $process->getErrorOutput() : null, 'stderr' => isset($process) ? $process->getErrorOutput() : null,
'Ffmpeg' => isset($process) ? $process->getCommandLine() : null, 'Ffmpeg' => $this->cmdLog($cmd),
'config' => $sConfig, 'config' => $sConfig,
'command' => implode(' ', $cmd), 'command' => implode(' ', $cmd),
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@@ -360,14 +384,12 @@ readonly class Segments
$response = api_error('Failed to generate segment. check logs.', Status::INTERNAL_SERVER_ERROR); $response = api_error('Failed to generate segment. check logs.', Status::INTERNAL_SERVER_ERROR);
if (true === $debug) { if (true === $debug) {
if (isset($process)) { $response = $response
$response = $response->withHeader('X-Ffmpeg', $process->getCommandLine()); ->withHeader('X-Ffmpeg', $this->cmdLog($cmd))
} ->withHeader(
'X-Transcode-Config',
$response = $response->withHeader( json_encode($sConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
'X-Transcode-Config', );
json_encode($sConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
} }
return $response; return $response;
} finally { } finally {
@@ -423,8 +445,7 @@ readonly class Segments
$this->logger->error('Failed to extract subtitle.', [ $this->logger->error('Failed to extract subtitle.', [
'stdout' => $process->getOutput(), 'stdout' => $process->getOutput(),
'stderr' => $process->getErrorOutput(), 'stderr' => $process->getErrorOutput(),
'Ffmpeg' => $process->getCommandLine(), 'Ffmpeg' => $this->cmdLog($cmd),
'command' => implode(' ', $cmd),
]); ]);
return "{$path}:stream_index={$stream}"; return "{$path}:stream_index={$stream}";
} }
@@ -440,8 +461,7 @@ readonly class Segments
$this->logger->error("Failed to extract subtitles. '{error}' at {file}:{line}", [ $this->logger->error("Failed to extract subtitles. '{error}' at {file}:{line}", [
'stdout' => isset($process) ? $process->getOutput() : null, 'stdout' => isset($process) ? $process->getOutput() : null,
'stderr' => isset($process) ? $process->getErrorOutput() : null, 'stderr' => isset($process) ? $process->getErrorOutput() : null,
'Ffmpeg' => isset($process) ? $process->getCommandLine() : null, 'Ffmpeg' => $this->cmdLog($cmd),
'command' => implode(' ', $cmd),
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@@ -460,4 +480,9 @@ readonly class Segments
} }
return []; return [];
} }
private function cmdLog(array $cmd): string
{
return implode(' ', array_map(fn($v) => str_contains($v, ' ') ? escapeshellarg($v) : $v, $cmd));
}
} }