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 && \
|
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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
'X-Transcode-Time' => round($end - $start, 6),
|
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: [
|
$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);
|
||||||
|
|||||||
Reference in New Issue
Block a user