Перекодировка DVD-Video и метаданных в H.265

Попался мне один DVD-Video с концертом, который надо было перекодировать в более компактный и удобный формат. Сегодня это H.265 (HEVC) для видео и OPUS для аудиодорожки. Также, хотелось бы иметь удобную навигацию, то есть, возможность перематывать сразу на начало следующего номера. Для этого необходимо вытащить с DVD метаданные и переделать их в формат, понятный кодировщику ffmpeg. Всё действие будет происходить в командной строке. Сразу скажу, что с метаданными-то и возникли проблемы, которые нужно было решить. Поехали.

DVD имел стандартный вид — это папка VIDEO_TS с файлами IFO, где хранится меню, и VOB, где содержится видео в формате MPEG-2, порезанное на части по гигабайту. Полезные файлы VOB имеют индекс VTS_<номер видео>_<номер файла>.VOB, где номера начинаются с единицы. Например, такими файлами будут VTS_01_1.VOB, VTS_01_2.VOB и так далее, или VTS_02_1.VOB, VTS_02_2.VOB и так далее. Их можно легко вычислить по большому размеру (столбец Length):

cd "C:\temp\PETERSON_QUARTET\VIDEO_TS"
dir -Recurse -Include "*.ifo","*.vob"

    Directory: C:\temp\PETERSON_QUARTET\VIDEO_TS

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-ar--          01.04.2025    10:01          16384 VIDEO_TS.IFO
-ar--          01.04.2025    10:01        1198080 VIDEO_TS.VOB
-ar--          01.04.2025    10:01          40960 VTS_01_0.IFO
-ar--          01.04.2025    10:01         149504 VTS_01_0.VOB
-ar--          01.04.2025    11:10     1073664000 VTS_01_1.VOB
-ar--          01.04.2025    11:10     1054126080 VTS_01_2.VOB
-ar--          01.04.2025    10:01          43008 VTS_02_0.IFO
-ar--          01.04.2025    10:01         149504 VTS_02_0.VOB
-ar--          01.04.2025    11:10     1073295360 VTS_02_1.VOB
-ar--          01.04.2025    11:10     1073457152 VTS_02_2.VOB
-ar--          01.04.2025    11:05      228806656 VTS_02_3.VOB

Сразу займёмся метаданными. Нужно сказать, что меню DVD-Video могут иметь самую причудливую и неудобную для обработки структуру. Раньше для вытаскивания пунктов меню из файлов IFO я пользовался программой ChapterXtractor, для которой я сделал шаблон, сразу при сохранении создающий файл метаданных ffmpeg с пересчётом временных меток. Но когда я открыл файлы VTS_01_0.IFO и VTS_02_0.IFO, стало ясно, что здесь так сделать не выйдет.

dvd_chapterXtractor1.png
dvd_chapterXtractor2.png

Как видно из картинок, каждое меню содержит полный набор номеров, но часть из них очень короткие, то есть, это явно заглушки. Само меню на DVD двухстраничное, что, наверное, и обусловило наличие двух меню вместо одного. Концерт на DVD один, поэтому эти два меню нужно как-то совмещать.

dvd_menu1.jpg
dvd_menu2.jpg

Для этого я взял консольную программу vgtmpeg, которая представляет собой видоизменённый ffmpeg, дополненный инструментами по работе с DVD/BluRay и т. п. В частности, эта программа умеет показывать метаданные DVD и даже конвертировать их сразу в формат ffmpeg, но сейчас это нам не подходит, так как нужна предварительная обработка. Вот информация, которую vgtmpeg выдаёт об этом диске:

$vgtmpeg = & C:\scripts\vgtmpeg\vgtmpeg.exe -hide_banner -i 'dvd://.' 2>&1

# Вывести результат
$vgtmpeg

Guessed Channel Layout for Input Stream #0.1 : stereo
Input #0, mpeg, from 'dvd://.?title=1':
  Metadata:
    source_type     : dvd
  Duration: 00:42:15.18, start: 0.000000, bitrate: 6714 kb/s
    Chapter #0:0: start 0.000000, end 513.689289
    Chapter #0:1: start 513.689289, end 989.563244
    Chapter #0:2: start 989.563244, end 1420.233467
    Chapter #0:3: start 1420.233467, end 1768.811800
    Chapter #0:4: start 1768.811800, end 2132.419356
    Chapter #0:5: start 2132.419356, end 2533.942333
    Chapter #0:6: start 2533.942333, end 2534.118556
    Chapter #0:7: start 2534.118556, end 2534.294778
    Chapter #0:8: start 2534.294778, end 2534.471000
    Chapter #0:9: start 2534.471000, end 2534.647222
    Chapter #0:10: start 2534.647222, end 2534.823444
    Chapter #0:11: start 2534.823444, end 2534.999667
    Chapter #0:12: start 2534.999667, end 2535.175889
  Program 1 
    Stream #0:0[0x100e0]: Video: mpeg2video (Main), yuv420p(tv, top first), 720x480 [SAR 8:9 DAR 4:3], 29.97 fps, 29.97 tbr, 90k tbn, 59.94 tbc
    Stream #0:1[0x100a0](und): Audio: pcm_dvd, 48000 Hz, stereo, s16, 1536 kb/s
    Metadata:
      language-iso639_2: und
      language-simple : Unknown
      language-description: Unknown
  No Program
    Stream #0:2[0x100bf]: Data: dvd_nav_packet
Guessed Channel Layout for Input Stream #1.1 : stereo
Input #1, mpeg, from 'dvd://.?title=2':
  Metadata:
    source_type     : dvd
  Duration: 00:45:33.19, start: 0.000000, bitrate: 6953 kb/s
    Chapter #1:0: start 0.000000, end 0.176222
    Chapter #1:1: start 0.176222, end 0.352444
    Chapter #1:2: start 0.352444, end 0.528667
    Chapter #1:3: start 0.528667, end 0.704889
    Chapter #1:4: start 0.704889, end 0.881111
    Chapter #1:5: start 0.881111, end 1.057333
    Chapter #1:6: start 1.057333, end 388.582222
    Chapter #1:7: start 388.582222, end 712.003578
    Chapter #1:8: start 712.003578, end 1017.434489
    Chapter #1:9: start 1017.434489, end 1419.978933
    Chapter #1:10: start 1419.978933, end 1858.760578
    Chapter #1:11: start 1858.760578, end 2302.571778
    Chapter #1:12: start 2302.571778, end 2733.185778
  Program 2 
    Stream #1:0[0x200e0]: Video: mpeg2video (Simple), yuv420p(tv, top first), 720x480 [SAR 8:9 DAR 4:3], 29.97 fps, 29.97 tbr, 90k tbn, 59.94 tbc
    Stream #1:1[0x200a0](und): Audio: pcm_dvd, 48000 Hz, stereo, s16, 1536 kb/s
    Metadata:
      language-iso639_2: und
      language-simple : Unknown
      language-description: Unknown
  No Program
    Stream #1:2[0x200bf]: Data: dvd_nav_packet
At least one output file must be specified

Отлично, всё видно. Нужные строки — со словом chapter. Сделаем из них таблицу и вычислим длительность частей в дополнительной колонке.

$csv = $vgtmpeg -match 'chapter' -replace '.*#(\d+:\d+).*start ([\d\.]+).*end ([\d\.]+)','$1;$2;$3' |
ConvertFrom-Csv -Header Chapter,Start,End -Delimiter ';' |
select *,@{n='Range';e={$_.End - $_.Start}}

# Вывести результат
$csv

Chapter Start       End          Range
------- -----       ---          -----
0:0     0.000000    513.689289  513,69
0:1     513.689289  989.563244  475,87
0:2     989.563244  1420.233467 430,67
0:3     1420.233467 1768.811800 348,58
0:4     1768.811800 2132.419356 363,61
0:5     2132.419356 2533.942333 401,52
0:6     2533.942333 2534.118556   0,18
0:7     2534.118556 2534.294778   0,18
0:8     2534.294778 2534.471000   0,18
0:9     2534.471000 2534.647222   0,18
0:10    2534.647222 2534.823444   0,18
0:11    2534.823444 2534.999667   0,18
0:12    2534.999667 2535.175889   0,18
1:0     0.000000    0.176222      0,18
1:1     0.176222    0.352444      0,18
1:2     0.352444    0.528667      0,18
1:3     0.528667    0.704889      0,18
1:4     0.704889    0.881111      0,18
1:5     0.881111    1.057333      0,18
1:6     1.057333    388.582222  387,52
1:7     388.582222  712.003578  323,42
1:8     712.003578  1017.434489 305,43
1:9     1017.434489 1419.978933 402,54
1:10    1419.978933 1858.760578 438,78
1:11    1858.760578 2302.571778 443,81
1:12    2302.571778 2733.185778 430,61

Вот, стало гораздо лучше. В колонке Range числа хранятся точные, просто при выводе всей таблицы отображаются в сокращённом виде. Например, если вывести второе значение отдельно, то оно будет 475,873955, так что по поводу корректности расчётов можно не беспокоиться. Теперь наглядно видно, что первые 6 частей берутся из первого меню, а последующие — из второго. Из-за того, что начала и окончания частей не идут последовательно по времени, надо их реконструировать. Для этого я взял первую метку начала и дальше прибавлял к ней длину из Range, а потом отсеивал те части, которые меньше 5 секунд.

$timeline = @()
$t = $csv[0].Start
$csv |% {
    $obj = [PSCustomObject]@{
        Start = $t
        End = $t + $_.Range
        Range = $_.Range
    }
    $t = $t + $_.Range
    $timeline += $obj |? Range -gt 5
}

# Вывести результат
$timeline

  Start     End  Range
  -----     ---  -----
   0,00  513,69 513,69
 513,69  989,56 475,87
 989,56 1420,23 430,67
1420,23 1768,81 348,58
1768,81 2132,42 363,61
2132,42 2533,94 401,52
2536,23 2923,76 387,52
2923,76 3247,18 323,42
3247,18 3552,61 305,43
3552,61 3955,15 402,54
3955,15 4393,94 438,78
4393,94 4837,75 443,81
4837,75 5268,36 430,61

# Показать количество строк
$timeline.count
13

Прекрасно! Ровно 13 частей. Можно теперь лепить файл метаданных ffmpeg.

# Названия частей
$titles = ("Cakewalk
Love Ballade
Soft Winds
You Look Good To Me
My One And Only Love
Nigerian Market Place
Cool Walk
I Can't Get Started
Come Sunday
Reunion Blues
If You Only Knew
Sushi Blues
Blues Etude") -split "`n"

# Создание нового файла метаданных meta.txt для ffmpeg, заголовок
";FFMETADATA1
title=Recorded 'LIVE' at Kan-i Hoken Hall, Tokyo on February 28, 1987
artist=Oscar Peterson Featuring Joe Pass - The Quartet Live
" > meta.txt

# Добавление частей с названиями, переделка времени в миллисекунды
$c = 0
$timeline |% {
"[CHAPTER]
TIMEBASE=1/1000
START=$($_.start.tostring("0.000") -replace '\D')
END=$($_.end.tostring("0.000") -replace '\D')
title=$($titles[$c])
"
$c++
} >> meta.txt

Сначала я округлял число до тысячных: [Math]::Round($_.start, 3), и получил вроде бы правдоподобный результат, но выяснилось, что если последней цифрой после запятой оказывался 0, то он, естественно, пропадал и ffmpeg при попытке вшить эти метаданные в видеофайл выдавал ошибку, так как такая метка была на один десятичный разряд меньше и оказывалась по времени раньше предыдущей. Поэтому задействовал .tostring("0.000"), что гарантировало корректный перевод в строку.

Фрагмент полученного файла meta.txt:

;FFMETADATA1
title=Recorded 'LIVE' at Kan-i Hoken Hall, Tokyo on February 28, 1987
artist=Oscar Peterson Featuring Joe Pass - The Quartet Live

[CHAPTER]
TIMEBASE=1/1000
START=0000
END=513689
title=Cakewalk

[CHAPTER]
TIMEBASE=1/1000
START=513689
END=989563
title=Love Ballade

[CHAPTER]
TIMEBASE=1/1000
START=989563
END=1420233
title=Soft Winds

С метаданными разобрались, теперь дело за перекодировкой. Параметры -analyzeduration 100M -probesize 100M нужны, чтобы увеличить глубину определения аудиопотоков, потому что без них ffmpeg иногда не видел в DVD аудиодорожку. На вход подаются все файлы VOB, склеенные через concat: по порядку, и файл meta.txt как метаданные.

DVD-Video — старый формат, и видео там чересстрочное (interlaced), когда кадр делится на полукадры — «поля» (fields) — которые выводятся на экран последовательно. Раньше я всегда оставлял чересстрочную развёртку, когда кодировал в H.264 (AVC), потому что качество работы фильтров деинтерлейса тогда часто вызывало вопросы. H.265 тоже вроде как-то поддерживает чересстрочное видео, но cмысла заниматься этой экзотикой сегодня я не вижу, потому что найден прекрасный фильтр деинтерлейса bwdif. Оказалось, что наилучшие результаты деинтерлейса достигаются при удвоении частоты кадров — видео получается плавное и никакой «гребёнки» и «теней» в кадре не наблюдается. Фильтр сам умеет определять порядок полей, так что в большинстве случаев никаких настроек не требуется. А то, что видео получается 60 кадров/сек, сегодня уже не проблема.

Мой процессор поддерживает аппаратное кодирование в H.265, поэтому используется кодировщик hevc_qsv, качество 26 (настройка в сторону лучшего качества, чем стандартные 28 в софт-варианте libx265), хотя можно поставить, к примеру, и 24. Звуковой кодек — libopus, 192 кбит/сек, чего вполне достаточно для любого стереосигнала. Тэг-идентификатор формата видео я ставлю по старой памяти; полагаю, можно обойтись и без него.

ffmpeg -analyzeduration 100M -probesize 100M `
-i "concat:VTS_01_1.VOB|VTS_01_2.VOB|VTS_02_1.VOB|VTS_02_2.VOB|VTS_02_3.VOB" `
-i meta.txt -map 0:v -map 0:a -map_metadata 1 `
-vf bwdif -c:v hevc_qsv -c:a libopus -b:a 192k `
-global_quality:v 26 -tag:v hvc1 "Oscar Peterson - The Quartet Live (1987).mp4"

Результат: конечный файл занимает 655 МБ против 4,2 ГБ у исходного DVD. Качество никак не пострадало, заголовки и навигация на месте.

Собрал компьютер сыну

Давненько не брал я в руки шашек. Люблю возиться с железом и собирать компьютеры, но такая возможность появляется очень редко. Ну вот, наконец, купил сыну комп по частям и после окончания рабочей недели собрал его.

Сначала думал о чём-то покомпактнее, типа формата mini-ITX, но в результате не стал связываться с корпусами с нестандартными блоками питания и взял обычный microATX в Регарде, где есть удобный конфигуратор системного блока, чтобы не особенно раздумывать о совместимости того или иного железа. Можно было там же заказать и сборку, но мы хотели собирать сами.

Материнская плата MSI A520M-A PRO

Здесь у меня не было особых критериев, выбирал просто по отсутствию сильно устаревших разъёмов, давно знакомому бренду и компоновке задней панели, как это ни удивительно. Рассматривал также Gigabyte и ASRock.

Процессор AMD Ryzen 5 5600X OEM + кулер ID-COOLING FROZN A410 Black

Тут выбор базировался на показателе best value на сайте cpubenchmark.net, где процессор AMD Ryzen 5 5600, который я и хотел взять изначально, находится на первом месте.

PassMark - Price Performance

Я заказал боксовый вариант, но мне перезвонил менеджер (спасибо ему!) и предложил более высокочастотный 5600X и отдельный кулер-башню за те же деньги. Меня беспокоило, не повышен ли теплопакет у 5600X, но оказалось, что это те же 65 Вт, поэтому я согласился и не пожалел. Кулер отличный и даже, пожалуй, избыточный для моего случая, но это вряд ли можно назвать недостатком. Чтобы поставить его, нужно было открутить от материнской платы штатный крепёж и привинтить идущий в комплекте. Шприц с термопастой также прилагался.

Оперативная память Kingston Fury Beast Black 32GB DDR4 3200MHz 2x16GB kit (KF432C16BB1K2/32)

В принципе, хватило бы и 16 ГБ, но пусть уже будет, чтобы потом не возвращаться к этой теме. Тем более, цены на память пока не космические.

Накопитель SSD Samsung 970 EVO Plus 1TB (MZ-V7S1T0BW)

Те же соображения насчёт объёма, что и по памяти. В моём рабочем неттопе стоит та же модель, но вполовину меньше, а стоила она тогда примерно столько же, и это при том, что было совсем другое время и доллар стоил сильно дешевле.

Видеокарта NVIDIA GeForce RTX 3050 MSI 8Gb (RTX 3050 VENTUS 2X XS 8G)

Видеокарту я сначала хотел взять с 6 ГБ памяти, но потом прочёл, что у такой модификации уменьшена пропускная способность шины и соотношение цены и эффективности хуже, поэтому взял с 8 ГБ, где этого недостатка нет, а цена выше незначительно. А ещё у этих видеокарт есть аппаратный кодировщик NVENC, умеющий работать с AVC/HEVC — это пригодится для стримов и просто для кодирования видео.

Wi-Fi адаптер ASUS PCE-AXE5400

Чтобы не тянуть провод от роутера из коридора, занял единственный разъём PCI-E x1 на материнской плате этим устройством. Работает прекрасно.

Блок питания Zalman MegaMax 700W (ZM700-TXIIv2)

Другой блок питания Zalman замечательно работает у меня уже 5 лет в сетевом хранилище и я был рад, что БП этой фирмы доступны для покупки — они тихие и надёжные. Для видеокарты рекомендован БП в 500 Вт, так что, полагаю, мощность адекватная этой конфигурации.

Корпус AeroCool Cs-111 Black

Корпус удобный, с прозрачной дверцей, которая даже не прикручивается винтами, а просто удерживается магнитиком, и внутрь корпуса можно залезть без помех в любой момент. Есть пара мелких недостатков: внутри установлен 120-мм вентилятор без возможности регулировки оборотов, поэтому я заменил его на Aerocool Frost 12 PWM, у которого такая функция есть. Также, у этого корпуса есть два места под вентиляторы на крыше, а мне как раз это не нужно и я хотел бы эту перфорацию заглушить, чтобы туда не оседала пыль. Пока не придумал, как это сделать приличнее нарезания чёрного картона и его прикручивания к внутренней стороне, а 3d-печать мне сейчас недоступна. Каких-то фирменных заглушек в продаже я не нашёл. Ну, покамест накрыли эти отверстия расписанием уроков.

Монитор MSI 24″ Pro MP242A

«Раз уж и материнская плата, и видеокарта этой фирмы, то пусть уж и монитор будет», — подумал я опять в нерациональной манере. Рациональным было то, что этот монитор один из самых дешёвых FullHD с IPS-матрицей и HDMI-разъёмом. Написано «100 герц», хотя я никогда не понимал, какое отношение эти герцы имеют к ЖК-мониторам, ладно ЭЛТ были, там да. Показывает хорошо, и даже есть встроенные динамики.

Колонки Sven SPS-585 Black

Я выбирал колонки, чтобы у них не было эквалайзера, чтобы звук просто шёл напрямую без регулировок тембра. Таких колонок очень немного в потребительском сегменте. Эти оказались очень достойным вариантом. Из-за небольшого размера низкочастотного динамика пресловутые басы недостаточно глубокие, но колонки играют чисто и цену свою оправдывают полностью.

Микрофон Fifine K669 Black

Про микрофоны Фифайн я впервые услышал на Youtube-канале прекрасного Стива Сегуина, автора бесплатного сервиса видеоконференций VDO.Ninja. Микрофоны эти удивляют высоким качеством за свою достаточно скромную цену. Я купил один из самых дешёвых. Конечно, качество звука стало несопоставимо лучше, чем у микрофона, встроенного в веб-камеру Sven IC-950 HD, валявшуюся раньше у меня в ящике.

Прочее

  • Игровая клавиатура механическая проводная Redragon Fizz тихая, RED SWITCH
  • Игровая мышь проводная TechFurn, черный, серый
  • Коврик для компьютерной мышки и клавиатуры Aksholan, 800х300х3мм, «Карта мира», черный

Резюме

Машина-зверь, конечно, на мой неискушённый взгляд — где-то раза в 2-3 мощнее моего рабочего неттопа, который теперь перестанут мучать играми. BIOS материнской платы я сразу прошил на самую свежую бету, лежащую на сайте производителя, операционка установилась со свистом, грузится она за несколько секунд, всё прямо-таки летает. Тестированием не увлекались, бенчмарки всякие не запускали; пока в ходу Roblox и Minecraft, они работают отлично на максимальных настройках. В общем, хорошо получилось, и владелец доволен. Хватило бы лет на семь-восемь — было бы славно. Главное, чтобы все были живы-здоровы.

Новый домашний сервер

4-го августа я переехал со старого неттопа на новый GMKtec NucBox G3. Внутри — процессор Intel N100 Alder Lake 12-го поколения, 32 ГБ памяти Kingston PC4-25600 DDR4, 1 ТБ SSD Samsung 980 Pro и Wi-fi Realtek RTL8852BE (802.11ax), также имеется дополнительный порт для SSD-накопителя формата M.2 2242 (SATA). Я после установки системы Ubuntu 24.04 стал использовать wi-fi на время настройки, планируя потом подключиться к роутеру по проводу, да так на нём и остался — удобно, работает стабильно и скорость хорошая.

Внешний вид сервера Внешний вид сервера

Из минусов — мелкий вентилятор в нижней части корпуса, но пока минус этот скорее теоретический, потому что сейчас шума от него не слышно вообще после следующих настроек в BIOS, которые я подсмотрел на Reddit:

  1. Выставить TDP на 8 Вт (с 10 Вт), макс. потребление будет 19 Вт (с 24 Вт). В покое 8-9 Вт, выключенный — 1,2 Вт. Power -> Power limit select: 8W
  2. Включить Cstates (по умолчанию выключено). Advance -> CPU - Power management control -> C States: enabled
  3. Выключить турборежим процессора при загрузке. Advance -> CPU - Power management control -> Boot performance mode: Max Non-Turbo performance
  4. Повысить порог срабатывания вентилятора. Advance -> Hardware monitor -> Smart fan function -> Fan off: 40, Fan start: 65

Почему я решил поменять железо? Началось с того, что какой-то малолетний кретин бил мне по входной двери ногой и убегал. Чтобы выяснить, кто это делает, я организовал видеонаблюдение через глазок, на который установил альтернативный веб-сервер, позволивший мне получить с глазка потоки RTSP, которые шли на сервер Frigate NVR, поднятый всё в том же Докере. Для установки глазка пришлось немного рассверлить в двери дырку под него, купив очень красивое ступенчатое сверло.

Frigate Frigate

У Frigate есть возможность использовать различные варианты аппаратного видеоускорения и моделей обнаружения объектов, но на старом неттопе была доступна только чисто процессорная обработка. Это работало, но загрузка была довольно приличная, да и в целом эта конфигурация уже устарела — ей восемь лет, последние три из которых она работала круглосуточно. Так что в конце 2023 года я купил новый неттоп, который провалялся без дела до конца июля, когда, наконец, у меня дошли руки перевезти все сервисы со старого.

Сервисы на сегодняший день такие:

  • Реверс-прокси Træfik — обновил c версии 2.6 на 3.1, немного изменился синтаксис ярлыков, теперь там используются регулярные выражения вместо перечисления нескольких суффиксов или имён хоста, а ещё HTTP/3 сменил экспериментальный статус на стабильный.
  • Вышеупомянутый Frigate, у которого я включил видеоускорение VAAPI и модель OpenVino. Позже, наверное, надо бы попробовать добавить к нему Home Assistant как управляющую оболочку.
  • Блог на прекрасном движке Datenstrom Yellow
  • Вики на DokuWiki
  • Файловый сервис Nextcloud
  • Фотосервис Photoprism
  • Генеалогическое древо Webtrees
  • Страничка мониторинга (node_exporter, docker metrics, glances, smartctl_exporter, Prometheus, Grafana)

С мониторингом была сложная история. Я использовал PhpSysInfo в контейнере, и мне хотелось избавиться от довольно кривого способа сбора информации раз в полчаса на самом хосте и подкладывания файлов в веб-каталог приложения. Оказалось, что PhpSysInfo умеет работать через SSH, поэтому я создал на хосте выделенного пользователя и разрешил ему выполнять команды sensors и docker stats через sudo без запроса пароля.

sudo visudo /etc/sudoers.d/phpsysinfo

Cmnd_Alias PHPSYSINFO=/usr/bin/docker stats*, /usr/sbin/smartctl
phpsysinfo ALL = NOPASSWD: PHPSYSINFO

Пришлось открыть пару тем в репозитории Гитхаба из-за того, что какие-то моменты были непонятны, а какие-то не работали, но автор приложения, как и раньше, невероятно отзывчив и решает проблемы просто молниеносно, так что всё более-менее работало и прочитанные/записанные данные диска теперь показывались в понятных гигабайтах, а не в секторах, как раньше. Тем не менее, теперь вывод страницы после захода на неё стал очень тормозной — PhpSysInfo каждый раз лезет на хост по SSH и собирает данные (на полях отмечу утилиту sshpass, позволяющую автоматически подставлять пароль для входа). Вдобавок, перестало нормально работать отображение скопившихся системных обновлений, так что я начал искать альтернативу.

Glances Glances

Через некоторое время я нашёл Glances, у которого мне очень понравился внешний вид, но проект пока сыроват и документация у него достаточно фрагментарная, поэтому я решил вернуться к давней идее, на которую у старого неттопа не хватало ресурсов — Prometheus + Grafana. В конце концов, популярная вещь и по работе пригодится. Провозившись несколько дней, вспоминая, как там всё устроено, я нарисовал страницу более-менее в соответствии с моими предпочтениями. Что до производительности, то неттоп легко справляется с новым мониторингом. Glances я оставил как сборщик данных о контейнерах: он умеет экспортировать метрики для Prometheus.

Grafana Grafana

Все контейнеры с БД я обновил до MariaDB 11.4 с версии 10.6, завелось без проблем. PHP был обновлён на версию 8.3 ещё на старом неттопе. Docker volumes переделал просто в mount points — мне кажется, мигрировать данные и делать резервные копии так удобнее, каких-то преимуществ docker named volumes в моём случае я не вижу.

Малолетний кретин, кстати, в дверь мне больше не бил, поэтому я так и не знаю, кто это. Заснял, как в дверь плевали в ноябре 2023 года — возможно, это тот самый, но с тех пор ничего плохого с дверью не происходило.

Пока мне нравится, поживём — увидим.

Удаление плавающей заставки из мультсериала

Волею судеб с некоторых пор я последовательно скачиваю серии мультиков про Наруто. После скачивания для экономии места на сетевом хранилище я пережимаю эти серии в H.265, оставляя только само содержимое, удаляя финальные титры и песню-заставку (opening).

В первой порции этого сериала из 220 серий песня звучала в самом начале, поэтому вырезать её было достаточно просто. Теперь же, скачав пачку новых эпизодов, я обнаружил, что заставка не имеет постоянного времени начала и воспользоваться старым способом уже нельзя — нужно как-то обнаруживать её местоположение в видеофайле.

Ffmpeg справляется и с этой задачей. Механизм заключается в том, что видеофайл прогоняется через фильтр, сравнивающий кадры с образцом на скриншоте, выдавая статистику, из которой можно извлечь нужную метку времени t:.

frame=12549 fps=353 q=-0.0 size=N/A time=00:08:24.36 bitrate=N/A speed=14.2x    
frame=12719 fps=353 q=-0.0 size=N/A time=00:08:31.16 bitrate=N/A speed=14.2x    
[Parsed_blackframe_1 @ 0000023f584beec0] frame:12737 pblack:100 pts:12798 t:511.920000 type:I last_keyframe:12737
[Parsed_blackframe_1 @ 0000023f584beec0] frame:12738 pblack:100 pts:12799 t:511.960000 type:P last_keyframe:12737
...
[Parsed_blackframe_1 @ 0000023f584beec0] frame:12846 pblack:88 pts:12907 t:516.280000 type:P last_keyframe:12845
[Parsed_blackframe_1 @ 0000023f584beec0] frame:12847 pblack:89 pts:12908 t:516.320000 type:P last_keyframe:12845
frame=12891 fps=353 q=-0.0 size=N/A time=00:08:38.08 bitrate=N/A speed=14.2x    
frame=13088 fps=353 q=-0.0 size=N/A time=00:08:45.92 bitrate=N/A speed=14.2x

Это нужно сделать 2 раза: для кадра начала заставки и кадра конца, а затем уже на основании полученных меток времени приступать к обработке и перекодированию. Конечно, операция гораздо более трудоёмкая, чем раньше, но положительные эмоции от найденного работающего решения того стоят, да и надо же чем-то себя занимать на этой бессмысленной новогодней неделе.

Ситуация дополнительно усложнялась следующими факторами:

  1. Кадры заставки от серии к серии могут немного меняться и фильтр перестаёт воспринимать кадр как похожий на образец на скриншоте. Ниже примеры, где один образец кадра наложен на другой и видно, что логотип был сдвинут или увеличен.
naruto_opening5_end.jpg
naruto_opening1_end.jpg
  1. Заставка сама по себе меняется примерно каждые 25 серий, так что тут придётся пробежаться по эпизодам сериала, записать их диапазоны с теми или иными заставками и наделать пары скриншотов для каждого диапазона.
  2. Серии (именно их содержательная часть) длятся неодинаковое время. У одних финальные титры начинаются в 20:06, а у других — на минуту позже.
  3. Наличие квадратных скобок в именах файлов и путях. Почему-то многие владельцы раздач на торрент-трекерах питают слабость к подобным символам, поэтому приходится добавлять в скрипт дополнительные ухищрения.

Чтобы фильтр не был таким дотошным и допускал мелкие изменения в кадре по сравнению с образцом на картинке, надо понизить ему порог чувствительности. Но вскоре выяснилось, что если в образце преобладает чёрный цвет, то фильтр с увеличенной погрешностью начинал ошибочно определять обычные чёрные кадры вместо искомого. Вот пример подобной картинки:

naruto_opening3_start.jpg

В целом получилось так: для полноцветных кадров я выставлял blackframe=80 при стандартном значении 98, а для кадров на чёрном фоне оставлял по умолчанию blackframe.

Чтобы определить длительность серий, я сделал быстрый просмотр, выбирая по 10 серий, чтобы индекс массива для удобства совпадал с последней цифрой серии, и нажимая F8 в Powershell ISE на соответствующей строке.

$episodes = 80..89 |% {$_.ToString("000")}
$e = dir *.avi |? name -match "$($episodes -join '|')"

ffplay -i $e[0].Name -ss 20:06 -an
ffplay -i $e[1].Name -ss 20:06 -an
ffplay -i $e[2].Name -ss 20:06 -an
ffplay -i $e[3].Name -ss 20:06 -an
ffplay -i $e[4].Name -ss 20:06 -an
ffplay -i $e[5].Name -ss 20:06 -an
ffplay -i $e[6].Name -ss 20:06 -an
ffplay -i $e[7].Name -ss 20:06 -an
ffplay -i $e[8].Name -ss 20:06 -an
ffplay -i $e[9].Name -ss 20:06 -an

Результаты я заносил в список прямо в скрипте, подвёрстывая их к диапазонам серий с видами заставок, чтобы потом последовательно раскомментировать нужные строки.

Чтобы в случае чего можно было вернуться к перекодированию без возни с повторным отлавливанием заставки, временные метки записываются в файл CSV:

Name;Start;End
Naruto_Shippuuden_TV2_[001_of_XXX]_[Rus_Jap]_[NIKITOS].avi;511.920000;601.760000
Naruto_Shippuuden_TV2_[002_of_XXX]_[Rus_Jap]_[NIKITOS].avi;119.000000;208.800000
Naruto_Shippuuden_TV2_[003_of_XXX]_[Rus_Jap]_[NIKITOS].avi;130.960000;220.800000
...
Naruto_Shippuuden_TV2_[131_of_XXX]_[Rus_Jap]_[NIKITOS].avi;448.489708;536.827964782715
Naruto_Shippuuden_TV2_[132_of_XXX]_[Rus_Jap]_[NIKITOS].avi;383.299583;471.637840270996
Naruto_Shippuuden_TV2_[133_of_XXX]_[Rus_Jap]_[NIKITOS].avi;1.501500;89.8814591169357

В файле CSV первые конечные метки отличаются от тех, что идут позже — у них меньше десятичных знаков. Дело в том, что сначала я указывал ffmpeg искать в первых 10 минутах видеофайла и начальный, и конечный кадр — в функции была одна команда. Потом я догадался, что конечный кадр ведь идёт после начального, поэтому нужно начинать искать конечный кадр с того места, где был начальный, а так как все заставки идут не более полутора минут, то ими надо поиск и ограничить. Длительность заставок выяснилась путём анализа того же CSV-файла.

Import-Csv 'C:\temp\log.txt' -Delimiter ';' |select *,@{n='Duration';e={$_.end - $_.start}}

Name                                                       Start      End        Duration
----                                                       -----      ---        -------
Naruto_Shippuuden_TV2_[001_of_XXX]_[Rus_Jap]_[NIKITOS].avi 511.920000 601.760000 89,84
Naruto_Shippuuden_TV2_[002_of_XXX]_[Rus_Jap]_[NIKITOS].avi 119.000000 208.800000  89,8
Naruto_Shippuuden_TV2_[003_of_XXX]_[Rus_Jap]_[NIKITOS].avi 130.960000 220.800000 89,84
Naruto_Shippuuden_TV2_[004_of_XXX]_[Rus_Jap]_[NIKITOS].avi 170.000000 259.840000 89,84
Naruto_Shippuuden_TV2_[005_of_XXX]_[Rus_Jap]_[NIKITOS].avi 81.040000  170.760000 89,72
Naruto_Shippuuden_TV2_[006_of_XXX]_[Rus_Jap]_[NIKITOS].avi 191.960000 281.760000  89,8
Naruto_Shippuuden_TV2_[007_of_XXX]_[Rus_Jap]_[NIKITOS].avi 0.000000   89.840000  89,84

Тем самым я убил двух зайцев: уменьшил время обработки и убрал дополнительную функцию, вычисляющую время между найденными кадрами и пишущую в лог ошибку и пропускающую перекодирование, если это время превышало 90 секунд. Но так как значение местонахождения конечного кадра было уже относительным к начальному (например, для начального теперь оно могло быть 150, а для конечного — 88), поэтому для команды кодирования нужно эти значения складывать, и это вычисленное значение даёт большее число знаков после запятой.

Для лучшего ориентирования я добавил вывод информации на экран. Здесь видно, что поиск начала занимает 43 секунды, а поиск конца уже 5.

2024.01.02 17:49:45 Поиск начала заставки в Naruto_Shippuuden_TV2_[147_of_XXX]_[Rus_Jap]_[NIKITOS].avi...
2024.01.02 17:50:28 Начало заставки найдено на 384.509125 сек.
2024.01.02 17:50:28 Поиск конца заставки в Naruto_Shippuuden_TV2_[147_of_XXX]_[Rus_Jap]_[NIKITOS].avi...
2024.01.02 17:50:33 Конец заставки найден на 472.847373962402 сек.

Есть и журнал ошибок. Какие-то файлы всё же приходится обрезать самому в Avidemux, пока компьютер перелопачивает остальные, к тому же, среди набора серий попадается неформат — склеенные или разделённые на несколько частей эпизоды. Но это всё равно лучше, чем обрабатывать руками все 235 видеофайлов.

2024.01.02 14:28:50 Конец заставки не найден в Naruto_Shippuuden_TV2_[125_of_XXX]_[Rus_Jap]_[NIKITOS].avi
2024.01.02 14:29:20 Начало заставки не найдено в Naruto_Shippuuden_TV2_[126_of_XXX]_[Rus_Jap]_[NIKITOS].avi
2024.01.03 07:23:11 Начало заставки не найдено в Naruto_Shippuuden_TV2_[183_of_XXX]_[Rus_Jap]_[NIKITOS].avi

Собственно, скрипт.

## Заставка 1 рассвет
#$episodes = 1..30
## Заставка 2 глаз
#$episodes = 31..53
## Заставка 3 птица
#$episodes = 54..77
#$episodesLong = ,55+71
## Заставка 4 облака
$episodes = 78..102                         # Диапазон обрабатываемых эпизодов
$episodesLong = ,79+80+82+85+91+92+100+102  # Эпизоды с увеличенной длиной
## Заставка 5 водопад
#$episodes = 103..128
#$episodesLong = ,104+106+107+111..115+120+123..125
## Заставка 6 мороженое
#$episodes = 129..153
#$episodesLong = ,131+133+135+138..141+143+145+148+149..155
## Заставка 7 зонтик
#$episodes = 154..179
#$episodesLong = 149..155+157..159+162+167+170..172+174..179
## Заставка 8 лицо
#$episodes = 180..196+199..205
#$episodesLong = 180..184+186..208
## Заставка 9 летающие острова
#$episodes = 206..230
#$episodesLong = ,208+210+211+213..235
## Заставка 10 скала
#$episodes = 231..235
#$episodesLong = 213..235

$inFolder = 'C:\Users\User\downloads\Naruto Shippuuden TV2 `[NIKITOS`] HWP' # Каталог с исходниками
$outFolder = 'C:\temp'             # Каталог с обработанными файлами
$logFolder = 'C:\temp'             # Каталог с логами
$csv = "$logFolder\log.txt"        # Журнал/CSV 
$logError = "$logFolder\error.txt" # Журнал ошибок
$ext = '*.avi'                     # Маска и расширение исходных файлов 
$episodes = $episodes |% {$_.ToString("000")}              # Переделать номера в трёхзначные для поиска файлов
$openingStartPic = Get-Item "$inFolder\opening4_start.jpg" # Картинка начального кадра
$openingEndPic = Get-Item "$inFolder\opening4_end.jpg"     # Картинка конечного кадра
$openingMax = 90          # Длина заставки (сек.), глубина поиска конечного кадра
$vidDuration = 1206       # Стандартная длина эпизода (сек.)
$vidDurationLong = 1266   # Увеличенная длина эпизода (сек.)

# Функция поиска кадров
function Find-Frame ($file,$pic,$startPoint) {
    if ($startPoint) {
        # Конечный кадр
        (& ffmpeg.exe -hwaccel_output_format qsv -ss $startPoint -t $openingMax -an -i $file -loop 1 -i $pic -filter_complex "blend=difference:shortest=1,blackframe=80" -f null - 2>&1) -match 'blackframe.*type:I'
    }
    else {
        # Начальный кадр
        (& ffmpeg.exe -hwaccel_output_format qsv -t 10:00 -an -i $file -loop 1 -i $pic -filter_complex "blend=difference:shortest=1,blackframe=80" -f null - 2>&1) -match 'blackframe.*type:I'
    }
}

# Функция логирования/вывода
function log ($text,$file,$color) {
    $t = (get-date).tostring("yyyy.MM.dd HH:mm:ss")
    if ($file) {
        Tee-Object -InputObject "$t $text" -FilePath $file -Append
    }
    else {
        if (-not $color) {$color = "white"}
        Write-Host -fore $color "$t $text"
    }
}

cd $inFolder

dir $ext |? name -match "$($episodes -join '|')" |% {

# Поиск начала заставки
log -text "Поиск начала заставки в $($_.name)..." -color yellow 
$openingStart = Find-Frame -file $_.name -pic $openingStartPic.fullname
if ($openingStart) {
    $openingStartFrame = ($openingStart[0] -split ' ')[-3] -replace "t:"
    log -text "Начало заставки найдено на $openingStartFrame сек." -color green
}
else {
    log -text "Начало заставки не найдено в $($_.name)" -file $logError
    continue
}

# Поиск конца заставки
log -text "Поиск конца заставки в $($_.name)..." -color yellow
$openingEnd = Find-Frame -file $_.name -pic $openingEndPic.fullname -startPoint $openingStartFrame
if ($openingEnd) {
    $openingEndFrame = [single]$openingStartFrame + [single](($openingEnd[-1] -split ' ')[-3] -replace "t:")
    log -text "Конец заставки найден на $openingEndFrame сек." -color green
}
else {
    log -text "Конец заставки не найден в $($_.name)" -file $logError
    continue
}

# CSV
"$($_.name);$openingStartFrame;$openingEndFrame" |Out-File $csv -Encoding default -Append

# Выбор длины серии в целом
if ($_.basename -match "$($episodesLong -join '|')") {$tail = $vidDurationLong}
else {$tail = $vidDuration}

# Обработка/кодирование
& ffmpeg.exe -y -hide_banner -hwaccel_output_format qsv -i $_.name `
-filter_complex `
"[0:0]trim=start=0:end=$($openingStartFrame),setpts=PTS-STARTPTS[av];
 [0:1]atrim=start=0:end=$($openingStartFrame),asetpts=PTS-STARTPTS[aa];
 [0:0]trim=start=$($openingEndFrame):end=$($tail),setpts=PTS-STARTPTS[bv];
 [0:1]atrim=start=$($openingEndFrame):end=$($tail),asetpts=PTS-STARTPTS[ba];
 [av][bv]concat[outv];[aa][ba]concat=v=0:a=1[outa]" `
-map [outv] -map [outa] -c:v hevc_qsv -global_quality:v 28 `
-c:a libopus -ac 1 -b:a 64k `
"$outFolder\$($_.basename).mp4"

Clear-Variable openingStart,openingEnd,openingStartFrame,openingEndFrame
}

После перекодирования всех серий они стали занимать 9,5 ГБ вместо исходных 67.

С новым годом, желаю вам мира и спокойствия.

Хорошо не наступает

Самое большое количество суицидов — 45-47 лет, это тот самый «кризис среднего возраста». Когда мы растём, то нам говорят: если будешь делать так и так, то будет хорошо. И вот человек хорошо учится, старается всё делать правильно, находит плюс-минус интересную работу, поднимается по иерархической лестнице, у него семья, дети.

И вот уже дожил до 40, вроде бы всё делает правильно, но только «хорошо» не наступает и он понимает, что вроде бы уже и не наступит. Он попадает в ситуацию переосмысления и вспоминает, чего на самом деле хотел.

Кто дожил до 50, тому уже более-менее стало полегче, пока не дойдёт где-то до 70. Никто не говорит о суицидах пожилых людей, а их очень много. Люди оказываются в ситуации одиночества, экономических кризисов, болезней. Сложнее всего с изоляцией, потому что людям часто просто не с кем общаться; из-за проблем, например, с суставами лишний раз на улицу не выйти. Никто об этом особенно не задумывается.