Merge pull request #534 from arabcoders/dev
Basic hardware acceleration support
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [
|
||||
$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);
|
||||
|
||||
Reference in New Issue
Block a user