Делаем домашнюю ферму для рендеринга видео

В этой статье я расскажу о своём опыте создания отдельного сервера («ферма» уж больно громко сказано) для рендеринга видео в домашних условиях.

Как известно, ренедеринг видео и трёхмерных изображений занимает много времени и требует много ресурсов компьютера. Ещё свежи воспоминания, как будучи студентом я ставил на ночь жужжащий компьютер с запущенной программой сборки фильма, а на утро оказывалось, что либо не хватило места на жёстком диске, либо что-то забыл добавить в ролик и всё приходилось начинать сначала. Сейчас настоящие профессионалы делают эту операцию удалённо. Например, режиссёр Джеймс Камерон во время съёмок фильма «Аватар» специально для себя сделал заказ на создания целого дата-центра, в котором запускались рендеринги сцен. Я пока ещё не настолько крут, чтобы строить свой отдельный дата-центр, но идея того, что эта операция могла бы быть запущена отдельно на другом сервере не давала мне покоя долгое время.

Вторая проблема – это монтаж самого видео. Несмотря на то, что современные компьютеры становятся мощнее, развитие видеокамер тоже не стоит на месте и получившиеся видео файлы становятся всё тяжелее и тяжелее. И, как следствие, их становится проблематично обрабатывать. Когда монтируешь продолжительный фильм, то предпросмотр трека с наложенными фильтрами и переходами начинает подгружать процессор и изображение начинает лагать, делая творческий процесс монтажа утомительным. Из того, что я пробовал, самый продуктивный был iMovie, что предустановлен на всех Маках. Даже фильм продолжительностью в 45 минут можно было редактировать без особых проблем на относительно маломощном макбуке. Можно было применить любой фильтр и увидеть результат в окне предпросмотра без каких-либо лагов. Так что владельцам маков тут повезло. Один минус у iMovie: во время работы он начинает потреблять непомерное количество дискового пространства. Видимо, это связано с агрессивным кэшированием для редактирования и предпросмотра.

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

Инструмент

Kdenlive logo

Если сравнивать с виндой или МакОсью, для линукса как правило бывает не так уж и много десктопных приложений, но выбор именно редакторов видео оказался на удивление широк. Поиграв с нескольким вариантами, я пока остановился на Kdenlive для работы с небольшими видео. При сравнении с другими программами там есть хороший набор функций, но в рамках данной статьи нас интересует всего одна, но приятная особенность: при сведении уже готового фильма, программа предлагает две возможности – как обычно срендерить в файл или сгенерировать скрипт.

Generate script using KDenlive

При этом она просто создаст обычный shell-скрипт, который можно запустить потом из командной строки. В том числе и на другом компьютере. К чему я клоню? Да просто таким образом можно сводить проект на удалённом сервере. Для этого можно использовать облака, свой сервер, ну или даже мощный рабочий компьютер, который остаётся постоянно включённым в офисе (но я, конечно же, этого не говорил). План простой: сводим проект, генерируем скрипт, перекидываем весь проект на сервер, запускаем процесс удалённо.

Но это звучит слишком просто для настоящих бизонов. Добавим немного условий в задачу. Я уже жаловался, что монтаж оригинальных видео-файлов является задачей, требующей много ресурсов. Во время работы мы просматриваем результат на маленьком окошке предпросмотра, десятками раз прокручивая отдельные сюжеты, подгоняя субтитры, меняя фильтры и добавляя музыкальное сопровождение. А что если монтировать проект с маленькими видео-файлами, а уже окончательную сборку проводить с оригиналами? Всё равно мы скорее всего редко раскрываем предпросмотр во весь экран во время работы. В итоге, процесс выглядел бы таким образом:

  1. Для каждого видео исходника мы создадим его легковесный клон.
  2. Оригиналы мы перекинем на сервер.
  3. Монтаж будет производиться на личном компьютере с использованием легковесных копий (я использовал свой лаптоп).
  4. Перед сборкой мы проект перекидываем на сервер, где уже имеются исходники в оригинальном качестве.
  5. Замещаем легковесные клоны оригинальными.
  6. Запускаем рендеринг фильма в высоком разрешении.
  7. Опционально: оповещаем о завершении процесса по емайлу или СМС.
  8. Скачиваем готовый результат на личный компьютер.
  9. Тащимся от готового видео (и собственного мастерства).

Обо всём по порядку.

Легковесные копии

Для линукса есть довольно популярная программа для конвертирования и сжатия видео — HandBrake. Так как задача у нас просто сжать исходники, я особо выдумывать с настройками не буду и просто использую готовые пресеты. Выбираем iPod, которые даёт неплохой результат (напоминаю, видео для домашнего использования) и пробую сжать исходники.

Иногда после отличного отпуска у нас этих исходников могут быть десятки, если не сотни, и нужно эту работу как-то автоматизировать. Конкретно у HandBrake есть свой командный интерфейс. Поэтому можно быстро накатать скриптик, ну что-то типа этого:

#!/bin/bash # convert.sh  quality=30  for file in ls $1/* do  	tempfile="${file##*/}" 	filename="${tempfile%.*}" 	outputFile="$2/$filename.mp4" 	echo -e "\n\n Compress file $file" 	HandBrakeCLI -i $file -o $outputFile --preset="iPod" -q $quality -w 160 done  

Запустив его командой convert.sh ./original-videos ./small-videos, мы создадим для каждого файла его уменьшенную и оптимизированную копию. Единственное условие, чтобы облегчить нам жизнь в будущем, мы условимся, что все файлы и их клоны будут иметь одинаковое имя (хотя, скорее всего, разные расширения). Вот пример того, что у меня получилось. Две папки:

 /small-videos    /original-videos FILE0001.mp4     FILE0001.MOV FILE0002.mp4     FILE0002.MOV FILE0003.mp4     FILE0003.MOV ...              ... 

Таким образом, потом можно по имени программно заменить копии на их оригиналы. После этого папка с оригинальными видео отправляются на сервер, а маленькие копии остаются на нашем лаптопе для дальнейшей работы.

Пример оригинального видео и его легковесного клона:

Эмм… не переборщил ли я со сжатием? Всё только ради чистоты эксперимента!

Проект

Так, хорошо, начали проект. Качество предпросмотра отвратительное, конечно, но что поделать: мы же хотели избавиться от гнусных лагов, тормозящих всю работу. Продолжая плакать и колоться, монтируем фильм. Пока всё работает шустро, особо не напрягая процессор. Критический момент: настало время сведения. Мы жмём заветную кнопку Generate Script…, сохраняем результат. Теперь разберёмся, что именно только что произошло и что программа вообще сохранила.

Kdenlive timeline

В домашней папке KDEnlive (по умолчанию это ~/kdenlive/scripts/) появилось два файла:

  1. shell скрипт my-movie_001.sh
  2. и файл my-movie_001.sh.mlt

Shell-файл my-movie_001.sh имеет короткий набор команд:

#! /bin/sh  SOURCE="/home/w32blaster/kdenlive/scripts/my-movie_001.sh.mlt" TARGET="/home/w32blaster/kdenlive/my-movie.mp4" RENDERER="/usr/bin/kdenlive_render" MELT="/usr/bin/melt" PARAMETERS="-pid:8613 $MELT hdv_1080_50i avformat - $SOURCE $TARGET properties=x264-medium g=120 crf=20 ab=160k threads=1 real_time=-1" $RENDERER $PARAMETERS  

Особой уличной магии тут нет: он просто хранит в себе пути и настройки, а потом запускает рендер с необходимыми параметрами. При желании их можно редактировать, но обратить внимания стоит на один параметр – «threads=1», который, как нетрудно догадаться, устанавливает количество потоков. В зависимости от мощности процессора, это число можно увеличить, значительно повысив производительность рендеринга. Тем более, мы же собираемся запустить его на более мощном компьютере, не нагружая наш лаптоп.

Но особый интерес для этого эксперимента всё же представляет другой файл, который помечен многозначительным объявлением

 SOURCE="/home/w32blaster/kdenlive/scripts/my-movie_001.sh.mlt" 

Что за MLT файл такой?

MLT и описание проекта

MLT Logo

Оказывается (по крайней мере, для меня), в мире открытого программного обеспечения существует такой формат для передачи видео-трансляций, а также одноимённый фреймворк, называемый MLT. На сайте сказано, что это:

MLT is an open source multimedia framework, designed and developed for television broadcasting. It provides a toolkit for broadcasters, video editors, media players, transcoders, web streamers and many more types of applications. The functionality of the system is provided via an assortment of ready to use tools, XML authoring components, and an extensible plug-in based API.

С помощью консольной программы melt, можно сводить видеопроекты прямо в консоли. То есть в теории, можно даже сделать целый фильм, не выходя из консоли. Просто для этого нужно написать и запустить километровую команду, что большинство делать, конечно же, не любит. Вместо этого весь проект описывается специальным форматом MLT, который основан на вездесущем XML. Таким образом, рендеринг видео – это запуск консольной программы melt, которой скармливают как параметры будущего фильма, так и сам проект в MLT формате. Сам же MLT файл генерируется GUI программой. В моём случае, как я уже упоминал, это была KDenlive, с которой кстати поставляется melt в качестве зависимости. Подробнее об использовании этой утилиты в офф.доках.

Теперь давайте глянем на структуру самого MLT файла, сгенерированного из нашего фильма:

<?xml version='1.0' encoding='utf-8'?> <mlt title="Anonymous Submission" version="0.9.3" root="/home/w32blaster" LC_NUMERIC="en_GB.UTF-8">    <producer in="0" out="5367" id="117_4">      <property name="resource">Videos/small/FILE0001.mp4</property>      ... тут идут параметры видео-исходника   </producer>   <playlist id="playlist1">     <entry in="0" out="2033" producer="117_4">     <filter out="1838" id="volume">         ... параметры фильтра ...     </filter>  </playlist>   <tractor title="Anonymous Submission" global_feed="1" in="0" out="17018" id="maintractor">   <property name="meta.volume">1</property>   <track producer="black_track"/>   <track hide="video" producer="playlist1"/>   <track hide="video" producer="playlist2"/>   <track producer="playlist3"/>   <track producer="playlist4"/>   <track producer="playlist5"/>   <transition id="transition0">    <property name="a_track">1</property>    <property name="b_track">2</property>    <property name="mlt_type">transition</property>    <property name="mlt_service">mix</property>    <property name="always_active">1</property>    <property name="combine">1</property>    <property name="internal_added">237</property>   </transition>   .... много-много переходов, наложений и прочих параметров ....  </tractor> </mlt>  

Несмотря на объём и кажущуюся сумбурность, этот файл имеет довольно простую структуру:

  • mlt – главный тэг, который объявляет, что это не что иное, как описание видео в mlt-формате. Так же первыми объявляются параметры проекта, которые удалены из примера для простоты.
  • producer – это объект, являющийся исходником картинки. Это может быть видео, музыка, изображение, просто цветовая заливка, титры и даже другой mlt-файл (об этом чуть ниже). Внутри имеется много параметров, и в случае работы с файлами, важным является путь к ресурсу. Для каждого ресурса, который добавлен в наш проект, генерируется отдельный «продюсер».
  • playlist – плейлист, который состоит из всех «продюсеров». Тут же могут быть фильтры (в примере выше, это фильтр затухания звука). Для каждого ресурса указываем какой именно отрезок следует вставить. То есть когда мы обрезаем у ролика края на timeline, то на уровне формата MLT мы просто устанавливаем параметры in и out у тэга entry. Ну, и сам тэг entry имеет ссылку на «продюсер».
  • tractor – собственно, это и есть весь наш фильм, или timeline, как его ещё принято называть. То, что официально называется нелинейным монтажом, когда мы изменяем порядок и перетаскиваем видео-ролики между собой на трекере – всё это описывается тут. Все переходы, наложения, титры и всё-всё-всё описывается именно тэгом «tractor».

Подробнее о структуре mlt-файла тут.

Итак, мы сгенерировали первый mlt-файл, который будет содержать структуру самого фильма, составленного из сжатых видео-клонов. Каждый раз, когда мы будем готовы рендерить фильм, мы будем генерировать этот файл со структурой. Вроде разобрались с тем, как проект получается на стороне клиента (т.е. на лаптопе), теперь нужно разобраться, как с этим работать на сервере перед собственно самим рендерингом. План состоит в том, чтобы перед началом сборки заменить все «продюсеры» в mlt-файле с маленькими видео на точно такие же, но с оригинальными и уж потом запускать сведение. В идеале, никто не заметит подмены.

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

<producer in="0" out="1327" id="65_2">   <property name="mlt_type">producer</property>   <property name="length">1328</property>   <property name="eof">pause</property>   <property name="resource">Videos/Austria2015-small/FILE1235.mp4</property>   <property name="meta.media.nb_streams">2</property>   <property name="meta.media.0.stream.type">video</property>   <property name="meta.media.0.stream.frame_rate">30</property>   <property name="meta.media.0.stream.sample_aspect_ratio">0</property>   <property name="meta.media.0.codec.width">160</property>   <property name="meta.media.0.codec.height">90</property>   <property name="meta.media.0.codec.frame_rate">90000</property>   <property name="meta.media.0.codec.pix_fmt">yuv420p</property>   <property name="meta.media.0.codec.sample_aspect_ratio">0</property>   <property name="meta.media.0.codec.colorspace">709</property>   <property name="meta.media.0.codec.color_trc">1</property>   <property name="meta.media.0.codec.name">h264</property>   <property name="meta.media.0.codec.long_name">H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10</property>   <property name="meta.media.0.codec.bit_rate">52987</property>   <property name="meta.media.1.stream.type">audio</property>   <property name="meta.media.1.codec.sample_fmt">fltp</property>   <property name="meta.media.1.codec.sample_rate">48000</property>   <property name="meta.media.1.codec.channels">2</property>   <property name="meta.media.1.codec.name">aac</property>   <property name="meta.media.1.codec.long_name">AAC (Advanced Audio Coding)</property>   <property name="meta.media.1.codec.bit_rate">163117</property>   <property name="meta.attr.1.stream.language.markup">eng</property>   <property name="meta.attr.major_brand.markup">mp42</property>   <property name="meta.attr.minor_version.markup">512</property>   <property name="meta.attr.compatible_brands.markup">isomiso2avc1</property>   <property name="meta.attr.encoder.markup">HandBrake 0.10.1 2015030800</property>   <property name="seekable">1</property>   <property name="meta.media.sample_aspect_num">1</property>   <property name="meta.media.sample_aspect_den">1</property>   <property name="aspect_ratio">1</property>   <property name="audio_index">1</property>   <property name="video_index">0</property>   <property name="mlt_service">avformat</property>   <property name="meta.media.frame_rate_num">30</property>   <property name="meta.media.frame_rate_den">1</property>   <property name="meta.media.colorspace">709</property>   <property name="meta.media.width">160</property>   <property name="meta.media.height">90</property>   <property name="meta.media.top_field_first">0</property>   <property name="meta.media.progressive">1</property>   <property name="meta.media.color_trc">1</property>  </producer>  

Не плохо так, да? Нужно как-то заменить все эти параметры ресурса и желательно не вручную. Проблема осложняется ещё и тем, что для mlt нет схемы, таким образом программно трудно проверить корректность файла. Рискнём и постараемся обойтись без схемы.

Как будем замещать мета-данные клонов оригинальными (которые, естественно, будут разными, поскольку сами видео разные)? К счастью, melt достаточно умён, чтобы самому сделать корректный mlt, который он сам же потом и будет потреблять. Когда мы запускаем рендеринг с помощью команды melt нам нужно указать источники данных («producer», как показано выше) и потребителя, куда поток перенаправляется («consumer»). Потребитель бывает разный, все варианты можно посмотреть тут или же набрав команду из консоли:

$ melt -query "consumers" --- consumers:   - decklink   - xgl   - qglsl   - sdl   - sdl_audio   - sdl_preview   - sdl_still   - cbrts   - xml   - sdi   - jack   - avformat   - multi   - null   - gtk2_preview   - blipflash   - rtaudio ... 

Как видно, перенаправить вывод рендеринга можно во что угодно. Например, во время обычного рендеринга в файл используется avformat. Всё, что мы смонтируем, сохранится в один файл. В качестве второго примера, возможно направить вывод сразу же в stdout, используя команду:

 $ melt original.mlt -consumer libdv 

Тогда откроется окно с проигрыванием видео и melt будет сводить фильм, одновременно перенаправляя его на лету в открывшееся окно.

Но если указать в качестве потребителя «xml», а источником указать видео-файлы, то melt перечислит все файлы и их технические параметры и выведет весь контент в XML-формате в красиво оформленный MLT-файл. Вот пример команды:

 $ melt *.MOV -consumer xml:original.mlt 

Она сгенерирует MLT-файл, содержащий только лишь «продюсеры» на видеофайлы в данной директории с разрешением MOV. Естественно, с корректными мета-данными оригинальных (больших) видеофайлов. Так именно это и нужно для нашего коварного плана! Итак:

  1. Мы знаем, где на сервере лежат оригиналы.
  2. На основе этих оригиналов мы генерируем в командной строке MLT-файл, который содержит только одни «продюсеры» с оригинальными видео и их мета-данными. Назовём его original.mlt. В этом файле нет описания фильма (то есть тэга tractor), только одни исходники. Мы его будем использовать как источник т.н. правильных «продюсеров».
  3. Мы перекидываем с лаптопа на сервер текущий MLT-файл, который содержит наш фильм с актуальными изменениями, но с «подпорченными» маленькими копиями. Назовём его small.mlt
  4. Помятуя о том, что оригиналы и копии имеют одинаковые имена (но, возможно, разные расширения), мы можем сопоставить «правильные» продюсеры из первого MLT файла с «обрезанными» из второго MLT файла
  5. Нужно заменить не только сами продюсеры, но и их ID, поскольку они наверняка будут разными.
  6. Можно приступать к сведению.

В результате каждый продюсер наподобие такого:

 <producer>     <property name="resource">Videos/small/FILE1234.mp4</property>      ... его мета-данные  </producer>  

должен быть замещён продюсером подобным этому:

 <producer>     <property name="resource">Videos/original/FILE1234.MOV</property>      ... его мета-данные  </producer>  

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

Листинг 1: код обработчика MLT файла

#!/usr/bin/env python import xml.etree.ElementTree as ET import os.path import sys, getopt  def main(argv):     '''     The main purpose of this programm is to take the given MLT file containing     producers with an compressed ("small") resources (video, images, sound) and     replace them with identical producers with uncompressed ("original") resources     from another MLT file      Example of usage:     python process.py -s ~/Videos/small.sh.mlt -p ~/Videos/original/original.mlt      '''      smallVideosFile, originalVideosFile, homeDir = _extractCLArguments(argv)     if (smallVideosFile == '' or originalVideosFile == ''):         print "Error. Both arguments -s and -p must be provided!"         sys.exit(2)      print "Start MLT file processor. We will modify %s using data from %s" % (smallVideosFile, originalVideosFile)      # Parse the MLT file with small resources     smallTree = ET.parse(smallVideosFile)     smallTreeRoot = smallTree.getroot()          # and MLT file with original (big, full size) resources     originalTree = ET.parse(originalVideosFile)     originalTreeRoot = originalTree.getroot()      # prepare maps "video file name" <==> "producer ID"     mapSmallProducers = _getMapOfProducerIds(smallTreeRoot)     mapOriginalProducers = _getMapOfProducerIds(originalTreeRoot)      # build map "small producer ID" <==> "original producer ID", having the same video resource     mapID = _mergeMaps(mapSmallProducers, mapOriginalProducers)      # update 'root' value in MLT tag     _updateRootTag(homeDir, smallTreeRoot)          # then replace all the producers containing small videos with those with original videos     _replaceAllSmallProducersWithOriginal(mapOriginalProducers, smallTreeRoot, originalTreeRoot)      # print modified MLT to output file     ET.ElementTree(smallTreeRoot).write(smallVideosFile, encoding='utf-8', xml_declaration=True)   def _extractResourceName(fullName):     '''     Extracts only the resource name from full path. For example,     if the fullName is Videos/small/FILE1114.mkv this method returns FILE1114     '''     if "?" in fullName:         withoutParam = fullName.split("?",1)[0]         return os.path.splitext(os.path.basename(withoutParam))[0]     else:                 return os.path.splitext(os.path.basename(fullName))[0]       def _mergeMaps(mapSmallProducers, mapOriginalProducers):     '''     Builds map of producer IDs: each small producer's ID should     match appropriate big producer's ID, having the same resource.     '''      mapID = {}     for fileName, oldProducerId in mapSmallProducers.items():         if (fileName in mapOriginalProducers):             mapID[oldProducerId] = mapOriginalProducers[fileName]     return mapID   def _getMapOfProducerIds(tree):     '''     Builds the map for the given file: "resource (video file name)" <=> "producer ID".     '''          arrProducers = tree.findall('producer')          # Collect the map "resourse (video file name)" <==> "id of the producer"     mapIdToResource = {}     for producer in arrProducers:         resourceType = producer.find("*[@name='mlt_service']").text         isReplacementNeeded = (resourceType == "avformat" or resourceType == "framebuffer" or resourceType == "xml")          if (isReplacementNeeded):             resourceValue = producer.find("*[@name='resource']").text             # extract only filename (without path and extension)             fileName = _extractResourceName(resourceValue)             mapIdToResource[fileName] = producer.attrib.get('id')         else:             print "[Ignored] the producer %s (resource type is %s) is ignored" % (producer.attrib.get('id'), resourceType)             pass              return mapIdToResource   def _replaceAllSmallProducersWithOriginal(mapOriginalProducers, rootToBeModified, rootOriginal):     '''     Replaces all DOM-element 'Producer' containing small resources with     identical producers, containing the original resource     '''      print "Replace old producer having small size with another one having original resource:"     arrSmallProducers = rootToBeModified.findall('producer')      for producer in arrSmallProducers:         resourceType = producer.find("*[@name='mlt_service']").text          if (resourceType == "avformat"):             _replaceProducerAvformat(producer, mapOriginalProducers, rootToBeModified, rootOriginal)          elif (resourceType == "framebuffer"):             _replaceProducerFramebuffer(producer, mapOriginalProducers, rootToBeModified, rootOriginal)          elif (resourceType == "xml"):             _replaceProducerXml(producer, mapOriginalProducers, rootToBeModified, rootOriginal)               def _replaceProducerAvformat(producer, mapOriginalProducers, rootToBeModified, rootOriginal):     '''         Replace entirely one producer, containing small resource         with similar producer from original tree. Resulting producer         should have the same ID, but it must get all the          values from original tree (URLs, codec properties and other technical          information)     '''      resourceValue = producer.find("*[@name='resource']").text     fileName = _extractResourceName(resourceValue)     if(fileName in mapOriginalProducers):         # remember in, out and length from small producer         attrIn = producer.attrib.get('in')         attrOut = producer.attrib.get('out')         length = producer.find("*[@name='length']").text           producerId = producer.attrib.get('id')         originalId = mapOriginalProducers[fileName]                  origProducer = rootOriginal.find('.//producer[@id="' + originalId + '"]')         if origProducer is not None:             rootToBeModified.remove(producer)              # do not touch original node, deal with clone instead             origProducerClone = origProducer.copy()              # update in, out and length to match with small producer             origProducerClone.set('in', attrIn)             origProducerClone.set('out', attrOut)             origProducerClone.set('id', producerId)             origProducerClone.find("*[@name='length']").text = length             rootToBeModified.insert(1, origProducerClone)              print "   - Avformat resource (id=%s) is replaced with resource (id=%s)" % (producerId, originalId)          else:             print "   - [Ignored] Avformat resource (id=%s) was not found. File name is '%s' and found original ID is '%s'" % (producerId, fileName, originalId)   def _replaceProducerFramebuffer(producer, mapOriginalProducers, rootToBeModified, rootOriginal):     '''         Update given producer's resource. The resulting         producer should have the same properties except resource path.         Keep in mind, that we need to keep the parameter after "?" sign.     '''     resourceNode = producer.find("*[@name='resource']")     resourceValue = resourceNode.text     producerId = producer.attrib.get('id')      fileName = _extractResourceName(resourceValue)     if(fileName in mapOriginalProducers):         originalId = mapOriginalProducers[fileName]         origProducer = rootOriginal.find('.//producer[@id="' + originalId + '"]')         if origProducer is not None:             originalResourcePath = origProducer.find("*[@name='resource']").text             extension = resourceValue.split("?",1)[1]             resourceNode.text = originalResourcePath + "?" + extension             print "   - Framebuffer resource (id=%s) is replaced with resource (id=%s)" % (producerId, originalId)      else:             print "   - [Ignored] the Framebuffer resource (id=%s) is ignored!" % (producerId)  def _replaceProducerXml(producer, mapOriginalProducers, rootToBeModified, rootOriginal):     '''     Update given producer in case if it is XML resource.     The resulting producer should have updated resource path     including .mlt extension.     '''     resourceNode = producer.find("*[@name='resource']")     resourceValue = resourceNode.text[:-4] # trim .mlt extension     producerId = producer.attrib.get('id')      fileName = _extractResourceName(resourceValue)     if(fileName in mapOriginalProducers):         originalId = mapOriginalProducers[fileName]         origProducer = rootOriginal.find('.//producer[@id="' + originalId + '"]')         if origProducer is not None:             originalResourcePath = origProducer.find("*[@name='resource']").text             resourceNode.text = originalResourcePath + ".mlt"             print "   - XML resource (id=%s) is replaced with resource (id=%s)" % (producerId, originalId)  def _updateRootTag(homedir, rootToBeModified):     '''         In order to find all the resources, MLT uses the arggument "root"         placed in tag MLT. When we work on different computers, we must also         update this value in order MELT would be able to find resources.     '''     if (homedir != ''):         rootToBeModified.set('root', homedir)  def _extractCLArguments(argv):     inputfile = ''     outputfile = ''     homedir = ''          try:       opts, args = getopt.getopt(argv,"hs:p:u:",["smallfile=","producersfile=", "userhomedir="])     except getopt.GetoptError:       print 'process.py -s <smallFileToBeModified> -p <producersFile>'       sys.exit(2)          for opt, arg in opts:       if opt == '-h':          print '''process.py -s <smallFile> -p <producersFile> -u <userHomeDir>          where "smallFile" is a mlt file to be modified          and "producersFile" is a mlt file containing the list of producers          with original resources.          '''          sys.exit()        elif opt in ("-s", "--smallfile"):          inputfile = arg        elif opt in ("-p", "--producersfile"):          outputfile = arg         elif opt in ("-u", "--userhomedir"):          homedir = arg           return inputfile, outputfile, homedir  if __name__ == "__main__":     main(sys.argv[1:])  

Запускать этот скрипт вот такой командой:

 # python process.py -s small.mlt -p original.mlt 

которая скажет «возьми все продюсеры из файла original.mlt и замени их на соответсвующие в файле small.mlt, но остальное не трогай».

Собираем всё вместе

Ну что же, теперь пора попробовать собрать проект удалённо. Как полагается в линуксе, создадим на серверной стороне понятный и простой shell-скрипт,…

Листинг 2: полный скрипт на серверной стороне

#!/bin/bash  SMALL_RESORCES="Small" ORIGINAL_RESOURCES="Original" KDENLIVE_SCRIPT="austria-2015_001.sh" PY_SCRIPT="/home/ilya/home-workspace/mlt-producer-replacer/process.py" CURRENT_DIR="/home/ilya/Videos/MltProcessor"  rm log  echo -e "\n\n Step 1. Rename all the .mp4.mlt resources to .MOV.mlt" rm $ORIGINAL_RESOURCES/*.MOV.mlt for file in $ORIGINAL_RESOURCES/*.mlt; do     cp "$file" "$ORIGINAL_RESOURCES/`basename $file .mp4.mlt`.MOV.mlt" done  echo -e "\n\n Step 2. Generate fresh list of producers with original sources" rm $CURRENT_DIR/original.mlt cd ~ melt $CURRENT_DIR/$ORIGINAL_RESOURCES/*.{MOV,mp3} -consumer xml:original.mlt cd $CURRENT_DIR mv ~/original.mlt $CURRENT_DIR  echo -e "\n\n Step 3. Update MLT and replace small videos with original ones" # process the main MLT file cp $KDENLIVE_SCRIPT.mlt $KDENLIVE_SCRIPT.mlt-BACKUP python $PY_SCRIPT -s $KDENLIVE_SCRIPT.mlt -p $CURRENT_DIR/original.mlt -u $HOME  echo -e "\n\n Step 4. Update additional MLT files" # process other MLT resources, that are producers with type=xml for i in $ORIGINAL_RESOURCES/*.MOV.mlt; do 	python $PY_SCRIPT -s $i -p $CURRENT_DIR/original.mlt -u $HOME done  echo -e "\n\n Step 5. Run rendering" ./$KDENLIVE_SCRIPT  echo -e "\n\n Step 6. Make it smaller" HandBrakeCLI -i austria.mp4 -o austria.small.m4v --preset="Universal"  echo "FINISHED" >> log  

… который делает всё, что нам нужно: генерирует свежий MLT из оригиналов, запускает питоновский скрипт по замещению исходников и запускает рендеринг. В конце можно воткнуть оповещение по емайлу или СМС.

Примечание. Хотелось бы прокомментировать небольшую тонкость, которая описана в Step 1 и Step 4 в приведённом выше скрипте. Как я уже упоминал выше, помимо видео и картинок в качестве исходников могут выступать также и другие mlt файлы. С такой ситуацией вы можете столкнуться, если воспользоваться одной из т.н. Clip Job. Допустим, вы хотите проиграть клип задом наперёд.

KDEnlive, apply the clip job

В таком случае будет создан отдельный файл, который будет содержать всего один продюсер, с наложенными на видеоролик фильтрами. Называться этот файл будет {fileName}.{extension}.mlt. Использоваться в проекте он будет примерно так:

 <producer id="12_1" in="0" out="375">   <property name="mlt_type">producer</property>   <property name="resource">Videos/MltProcessor/Original/IMG_3764.MOV.mlt</property>   <property name="mlt_service">xml</property>   ....  </producer> 

Поскольку у нас оригиналы и копии могут иметь разные расширения, то нам в Step 1 следует переименовать их, чтобы melt смог корректно найти эти ролики. Скажем, файл IMG_3764.mp4.mlt должен быть переименован в IMG_3764.MOV.mlt. Ну и в завершение нам нужно их обработать нашим питоновским скриптом на шаге Step 4 точно так же, как мы обработали наш основной mlt.

Полная картина

Теперь суммируем, как создание фильма выглядит с самого начала и до конца.

  1. Мы собираем все исходники (можно даже и музыку), создаём уменьшенные клоны.
  2. Создаём новый проект в GUI программе типа Kdenlive, добавляем в него все клоны.
  3. Несмотря на то, что проект ещё не готов, жмём на Generate file, чтобы сгенерировать shell-скрипт. По желанию, можно установить число threads в параметрах. Больше этот файл меняться не будет.
  4. Закидывам на сервер оригиналы видео, питоновский скрипт и shell-скрипт из предыдущего пункта.
  5. Начинаем монтаж фильма. Когда готовы свести фильм и посмотреть его в полном качестве, рендерим заново свежий файл mlt.
  6. Закидываем только один этот файл на сервер и запускаем самописный shell-скрипт (Листинг 2).

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

В итоге, снова напишем совсем небольшой скрипт, который будем запускать каждый раз с лаптопа:

#!/bin/bash  SERVER_HOSTNAME="10.20.30.40" USERNAME="ilya"  REMOTE_PROJECT_DIR="/home/ilya/Videos/MltProcessor" REMOTE_ORIGINAL_DIR="/home/ilya/Videos/MltProcessor/Original"  LOCAL_MLT="/home/w32blaster/kdenlive/scripts/austria-2015_001.sh.mlt" LOCAL_SOURCES_DIR="/home/w32blaster/Videos/Austria2015-small"  # 1. Upload main MLT file scp $LOCAL_MLT $USERNAME@$SERVER_HOSTNAME:$REMOTE_PROJECT_DIR   # 2. Upload all others MLT files, that represents producer sources scp $LOCAL_SOURCES_DIR/*.mlt $USERNAME@$SERVER_HOSTNAME:$REMOTE_ORIGINAL_DIR  # 3. Execute rendering on the remote server ssh $USERNAME@$SERVER_HOSTNAME "$REMOVE_PROJECT_DIR/run.sh" 

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

Вывод

Конечно, на этой основе пока рано начинать свой стартап, придумывать броское название и рисовать логотип с усами-очками. В этом случае я всего лишь попытался воплотить саму идею процесса. Такой способ пока не для общего пользования, а скорее для гиков увлекающихся, потому что нужно сделать слишком много телодвижений, чтобы настроить систему на совместную работу. К тому же для «свободного художника» интерфейс Kdenlive окажется не настолько эстетичным и интуитивным, как у того же iMovie. Но субъективно могу сказать, что процесс создания ролика стал заметно быстрее, особенно если используется большое количество отснятого исходного материала.

Немного мыслей.

  • Вполне возможно, что данный метод работает не со всеми фильтрами Kdenlive, у меня не было возможность протестировать всё
  • Желательно иметь физический доступ к серверу, потому что перекинуть на него все оригиналы в высоком качестве по сети со средним каналом будет долго. Идеальный вариант: перекинуть видео файлы с помощью флешки или переноски.
  • И да, если вы считаете, что я сам придумал проблему и сам же её решил, то в оправдание могу сказать, что я нашёл отличный предлог, чтобы наконец-то ознакомиться с Питоном, чему весьма рад 🙂

Всем хороших выходных! А тем, кто уже парафинит скользяк, точит канты и мечтает о сугробах – удачного катания в будущем сезоне!

ссылка на оригинал статьи http://habrahabr.ru/post/260837/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *