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 && \
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 \
shadow sqlite redis tzdata gettext fcgi ca-certificates nss mailcap libcap fontconfig \
ttf-freefont font-noto terminus-font font-dejavu ${PHP_V} ${PACKAGES} && \
shadow sqlite redis tzdata gettext fcgi ca-certificates nss mailcap libcap fontconfig ttf-freefont font-noto \
terminus-font font-dejavu libva-utils ${PHP_V} ${PACKAGES} && \
# Delete unused users change users group gid to allow unRaid users to use gid 100
deluser redis && deluser caddy && groupmod -g 1588787 users && \
# Create our own user.

View File

@@ -139,14 +139,70 @@
<p class="help">
<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,
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>
</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="control">
<button class="button is-warning" @click="showAdvanced=!showAdvanced" :disabled="true"
v-tooltip="'Advanced settings not yet implemented.'">
<button class="button is-warning" @click="showAdvanced=!showAdvanced">
<span class="icon"><i class="fas fa-cog"></i></span>
<span>Advanced settings</span>
</button>
@@ -205,12 +261,20 @@ const isLoading = ref(false)
const isPlaying = ref(false)
const isGenerating = ref(false)
const playUrl = ref('')
const showAdvanced = ref(false)
const showAdvanced = useStorage('play_showAdvanced', false)
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({
path: '',
audio: '',
subtitle: '',
video_codec: video_codec,
vaapi_device: vaapi_device,
hwaccel: false,
debug: session_debug,
})
const selectedItem = ref({})
@@ -242,9 +306,16 @@ const generateToken = async () => {
path: config.value.path,
config: {
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) {
// -- check if the value is number it's internal subtitle
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`
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))
</script>

View File

@@ -23,6 +23,7 @@ use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\SimpleCache\CacheInterface as iCache;
use RuntimeException;
use SplFileInfo;
use Throwable;
final class Index
{
@@ -506,6 +507,44 @@ final class Index
}
$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);

View File

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