Merge pull request #534 from arabcoders/dev

Basic hardware acceleration support
This commit is contained in:
Abdulmohsen
2024-08-13 15:02:49 +03:00
committed by GitHub
4 changed files with 179 additions and 18 deletions

View File

@@ -30,9 +30,10 @@ ENV FPM_PORT="${PHP_FPM_PORT}"
# #
RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && \ RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && \
for ext in ${PHP_PACKAGES}; do PACKAGES="${PACKAGES} ${PHP_V}-${ext}"; done && \ for ext in ${PHP_PACKAGES}; do PACKAGES="${PACKAGES} ${PHP_V}-${ext}"; done && \
ARCH=`uname -m` && if [ "${ARCH}" == "x86_64" ]; then PACKAGES="${PACKAGES} intel-media-driver"; fi && \
apk add --no-cache bash caddy icu-data-full nano curl procps net-tools iproute2 ffmpeg \ apk add --no-cache bash caddy icu-data-full nano curl procps net-tools iproute2 ffmpeg \
shadow sqlite redis tzdata gettext fcgi ca-certificates nss mailcap libcap fontconfig \ shadow sqlite redis tzdata gettext fcgi ca-certificates nss mailcap libcap fontconfig ttf-freefont font-noto \
ttf-freefont font-noto terminus-font font-dejavu ${PHP_V} ${PACKAGES} && \ terminus-font font-dejavu libva-utils ${PHP_V} ${PACKAGES} && \
# Delete unused users change users group gid to allow unRaid users to use gid 100 # Delete unused users change users group gid to allow unRaid users to use gid 100
deluser redis && deluser caddy && groupmod -g 1588787 users && \ deluser redis && deluser caddy && groupmod -g 1588787 users && \
# Create our own user. # Create our own user.

View File

@@ -139,14 +139,70 @@
<p class="help"> <p class="help">
<span class="icon"><i class="fas fa-info"></i></span> <span class="icon"><i class="fas fa-info"></i></span>
We recommend using the burn subtitle function only when you are using a picture based subtitles, We recommend using the burn subtitle function only when you are using a picture based subtitles,
Text based subtitles are able to be selected and converted on the fly using the player. Text based subtitles are able to be selected and converted on the fly using the player. We plan to
support direct play of compatible streams in the future.
</p> </p>
</div> </div>
<template v-if="showAdvanced">
<div class="field">
<label class="label">Video transcoding codec.</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="video_codec" @change="e => updateHwAccel(e.target.value)">
<option value="" disabled>Select codec...</option>
<option v-for="item in item.hardware?.codecs" :key="`codec-${item.codec}`"
:value="item.codec" v-text="item.name"/>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-closed-captioning"></i>
</div>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
We don't do pre-checks on codecs, so some of those codecs may not work or you don't have the hardware
for it. the standard <code>H264 (CPU)</code> is the default and should work on most systems.
</p>
</div>
<div class="field" v-if="'h264_vaapi' === config.video_codec">
<label class="label">Select VAAPI rendering device</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="vaapi_device">
<option value="" disabled>Select device...</option>
<option v-for="item in item.hardware?.devices" :key="`codec-${item}`" :value="item"
v-text="basename(item)"/>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-closed-captioning"></i>
</div>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
We don't do pre-checks on codecs, so some of those codecs may not work or you don't have the hardware
for it. the standard <code>H264 (CPU)</code> is the default and should work on most systems.
</p>
</div>
<div class="field">
<label class="label" for="debug">Include debug information in response headers</label>
<div class="control">
<input id="debug" type="checkbox" class="switch is-success" v-model="session_debug">
<label for="debug">Enable</label>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
Useful to know what options and ffmpeg command being run.
</p>
</div>
</template>
<div class="is-justify-content-end field is-grouped" v-if="config?.path"> <div class="is-justify-content-end field is-grouped" v-if="config?.path">
<div class="control"> <div class="control">
<button class="button is-warning" @click="showAdvanced=!showAdvanced" :disabled="true" <button class="button is-warning" @click="showAdvanced=!showAdvanced">
v-tooltip="'Advanced settings not yet implemented.'">
<span class="icon"><i class="fas fa-cog"></i></span> <span class="icon"><i class="fas fa-cog"></i></span>
<span>Advanced settings</span> <span>Advanced settings</span>
</button> </button>
@@ -205,12 +261,20 @@ const isLoading = ref(false)
const isPlaying = ref(false) const isPlaying = ref(false)
const isGenerating = ref(false) const isGenerating = ref(false)
const playUrl = ref('') const playUrl = ref('')
const showAdvanced = ref(false) const showAdvanced = useStorage('play_showAdvanced', false)
const show_page_tips = useStorage('show_page_tips', true) const show_page_tips = useStorage('show_page_tips', true)
const video_codec = useStorage('play_vcodec', 'libx264')
const vaapi_device = useStorage('play_vaapi_device', '')
const session_debug = useStorage('play_debug', false)
const config = ref({ const config = ref({
path: '', path: '',
audio: '', audio: '',
subtitle: '', subtitle: '',
video_codec: video_codec,
vaapi_device: vaapi_device,
hwaccel: false,
debug: session_debug,
}) })
const selectedItem = ref({}) const selectedItem = ref({})
@@ -242,9 +306,16 @@ const generateToken = async () => {
path: config.value.path, path: config.value.path,
config: { config: {
audio: config.value.audio, audio: config.value.audio,
video_codec: config.value.video_codec,
hwaccel: config.value.hwaccel,
debug: Boolean(config.value.debug),
} }
}; };
if (config.value.vaapi_device && 'h264_vaapi' === config.value.video_codec) {
userConfig.config.vaapi_device = config.value.vaapi_device
}
if (config.value.subtitle) { if (config.value.subtitle) {
// -- check if the value is number it's internal subtitle // -- check if the value is number it's internal subtitle
if (String(config.value.subtitle).match(/^\d+$/)) { if (String(config.value.subtitle).match(/^\d+$/)) {
@@ -373,7 +444,18 @@ onMounted(async () => {
playUrl.value = `${useStorage('api_url', '').value}${useStorage('api_path', '/v1/api').value}/player/playlist/${route.query.token}/master.m3u8` playUrl.value = `${useStorage('api_url', '').value}${useStorage('api_path', '/v1/api').value}/player/playlist/${route.query.token}/master.m3u8`
isPlaying.value = true isPlaying.value = true
} }
updateHwAccel(video_codec.value)
}) })
const updateHwAccel = codec => {
const codecInfo = item.value.hardware.codecs.filter(c => c.codec === codec);
console.log(codecInfo)
if (codecInfo.length < 1) {
config.value.hwaccel = false
return;
}
config.value.hwaccel = Boolean(codecInfo[0].hwaccel)
}
onUnmounted(() => window.removeEventListener('popstate', onPopState)) onUnmounted(() => window.removeEventListener('popstate', onPopState))
</script> </script>

View File

@@ -23,6 +23,7 @@ use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\SimpleCache\CacheInterface as iCache; use Psr\SimpleCache\CacheInterface as iCache;
use RuntimeException; use RuntimeException;
use SplFileInfo; use SplFileInfo;
use Throwable;
final class Index final class Index
{ {
@@ -506,6 +507,44 @@ final class Index
} }
$entity['files'] = $ffprobe; $entity['files'] = $ffprobe;
$entity['hardware'] = [
'codecs' => [
[
'codec' => 'libx264',
'name' => 'H.264 (CPU) (All)',
'hwaccel' => false,
],
[
'codec' => 'h264_vaapi',
'name' => 'H.264 (VA-API) (VAAPI)',
'hwaccel' => true,
],
[
'codec' => 'h264_nvenc',
'name' => 'H.264 (NVENC) (Nvidia)',
'hwaccel' => true,
],
[
'codec' => 'h264_qsv',
'name' => 'H.264 (QSV) (Intel)',
'hwaccel' => true,
],
],
'devices' => [],
];
if (is_dir('/dev/dri/') && is_readable('/dev/dri/')) {
try {
foreach (scandir('/dev/dri') as $dev) {
if (false === str_starts_with($dev, 'render')) {
continue;
}
$entity['hardware']['devices'][] = '/dev/dri/' . $dev;
}
} catch (Throwable) {
}
}
} }
return api_response(Status::OK, $entity); return api_response(Status::OK, $entity);

View File

@@ -91,10 +91,11 @@ readonly class Segments
$hwaccel = (bool)$params->get('hwaccel', false); $hwaccel = (bool)$params->get('hwaccel', false);
$vaapi_device = $params->get('vaapi_device', '/dev/dri/renderD128'); $vaapi_device = $params->get('vaapi_device', '/dev/dri/renderD128');
$vCodec = $params->get('video_codec', $hwaccel ? 'h264_vaapi' : 'libx264'); $vCodec = $params->get('video_codec', $hwaccel ? 'h264_vaapi' : 'libx264');
$isIntel = $hwaccel && 'h264_vaapi' === $vCodec; $isVAAPI = $hwaccel && 'h264_vaapi' === $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);
if ($hwaccel && 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);
} }
@@ -146,7 +147,17 @@ readonly class Segments
} }
$cmd[] = '-copyts'; $cmd[] = '-copyts';
if ($isIntel) {
if ($isQSV) {
$cmd[] = '-hwaccel';
$cmd[] = 'qsv';
if ($overlay) {
$cmd[] = '-hwaccel_output_format';
$cmd[] = 'qsv';
}
}
if ($isVAAPI) {
$cmd[] = '-hwaccel'; $cmd[] = '-hwaccel';
$cmd[] = 'vaapi'; $cmd[] = 'vaapi';
$cmd[] = '-vaapi_device'; $cmd[] = '-vaapi_device';
@@ -167,14 +178,14 @@ readonly class Segments
$cmd[] = '-1'; $cmd[] = '-1';
$cmd[] = '-pix_fmt'; $cmd[] = '-pix_fmt';
$cmd[] = $isIntel ? 'vaapi_vld' : 'yuv420p'; $cmd[] = $params->get('pix_fmt', 'yuv420p');
$cmd[] = '-g'; $cmd[] = '-g';
$cmd[] = '52'; $cmd[] = '52';
if ($overlay && empty($external) && null !== $subtitle) { if ($overlay && empty($external) && null !== $subtitle) {
$cmd[] = '-filter_complex'; $cmd[] = '-filter_complex';
if ($isIntel) { if ($isVAAPI) {
$cmd[] = "[0:0]hwdownload,format=nv12[base];[base][0:" . $subtitle . "]overlay[v];[v]hwupload[k]"; $cmd[] = "[0:0]hwdownload,format=nv12[base];[base][0:" . $subtitle . "]overlay[v];[v]hwupload[k]";
$cmd[] = '-map'; $cmd[] = '-map';
$cmd[] = '[k]'; $cmd[] = '[k]';
@@ -190,7 +201,7 @@ readonly class Segments
$cmd[] = '-strict'; $cmd[] = '-strict';
$cmd[] = '-2'; $cmd[] = '-2';
if (empty($external) && $isIntel) { if (empty($external) && $isVAAPI) {
$cmd[] = '-vf'; $cmd[] = '-vf';
$cmd[] = 'format=nv12,hwupload'; $cmd[] = 'format=nv12,hwupload';
} }
@@ -237,7 +248,7 @@ readonly class Segments
symlink($external, $tmpSubFile); symlink($external, $tmpSubFile);
} }
$cmd[] = '-vf'; $cmd[] = '-vf';
$cmd[] = "subtitles={$tmpSubFile}" . ($isIntel ? ',format=nv12,hwupload' : ''); $cmd[] = "subtitles={$tmpSubFile}" . ($isVAAPI ? ',format=nv12,hwupload' : '');
} elseif (null !== $subtitle && !$overlay) { } elseif (null !== $subtitle && !$overlay) {
$subStreamIndex = (int)$subIndex[$subtitle]; $subStreamIndex = (int)$subIndex[$subtitle];
$tmpSubFile = r("{path}/t-{name}-internal-sub-{index}.{type}", [ $tmpSubFile = r("{path}/t-{name}-internal-sub-{index}.{type}", [
@@ -254,7 +265,7 @@ readonly class Segments
); );
$cmd[] = '-vf'; $cmd[] = '-vf';
$cmd[] = "subtitles={$streamLink}" . ($isIntel ? ',format=nv12,hwupload' : ''); $cmd[] = "subtitles={$streamLink}" . ($isVAAPI ? ',format=nv12,hwupload' : '');
} else { } else {
$cmd[] = '-sn'; $cmd[] = '-sn';
} }
@@ -291,9 +302,26 @@ readonly class Segments
] ]
); );
return api_error('Failed to generate segment. check logs.', Status::INTERNAL_SERVER_ERROR, headers: [ $response = api_error(
r("Failed to generate segment. '{error}'", [
'error' => $debug ? $process->getErrorOutput() : 'check logs.',
]),
Status::INTERNAL_SERVER_ERROR,
headers: [
'X-Transcode-Time' => round($end - $start, 6), 'X-Transcode-Time' => round($end - $start, 6),
]); ]
);
if (true === $debug) {
$response = $response
->withHeader('X-Ffmpeg', $process->getCommandLine())
->withHeader(
'X-Transcode-Config',
json_encode($sConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
}
return $response;
} }
$response = api_response(Status::OK, body: Stream::create($process->getOutput()), headers: [ $response = api_response(Status::OK, body: Stream::create($process->getOutput()), headers: [
@@ -330,7 +358,18 @@ readonly class Segments
'trace' => $e->getTrace(), 'trace' => $e->getTrace(),
]); ]);
return 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 (isset($process)) {
$response = $response->withHeader('X-Ffmpeg', $process->getCommandLine());
}
$response = $response->withHeader(
'X-Transcode-Config',
json_encode($sConfig, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
}
return $response;
} finally { } finally {
if (file_exists($tmpVidLock)) { if (file_exists($tmpVidLock)) {
unlink($tmpVidLock); unlink($tmpVidLock);