Автоматическая конвертация видео для сайта

По данной схеме возможно автоматически конвертировать видео файлы в воспроизводимые в браузере форматы ogv, mp4, webm:

  • создание базы данных видео файлов
  • настройка скрипта
  • настройка запуска по расписанию (cron)

Создание базы данных видео файлов

video-scheme.sql
-- Таблица с исходными видео файлами
CREATE TABLE `videos` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(64) NOT NULL DEFAULT '', -- название
  `filename` VARCHAR(255) NOT NULL DEFAULT '', -- относительный путь к исходному файлу
  `thumbnail` VARCHAR(255) NOT NULL DEFAULT '', -- картинка предпросмотра (создается)
  `datetime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- дата
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_filename` (`filename`)
) ENGINE=INNODB;
 
-- Таблица с перекодированными видео
CREATE TABLE `convert_videos` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `video_id` INT(11) UNSIGNED NOT NULL, -- внешний ключ на videos
  `format` ENUM('ogv','mp4','webm') NOT NULL, -- формат
  `filename` VARCHAR(255) NOT NULL, -- относительный путь к сконвертированному файлу
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_video_format` (`video_id`,`format`)
) ENGINE=INNODB;
 
-- Таблица с логом перекодирования
CREATE TABLE `convert_log` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `datetime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `action` ENUM('init','ffmpeg','other') NOT NULL,
  `format` ENUM('-','ogv','mp4','webm','thumbnail') NOT NULL DEFAULT '-',
  `src` VARCHAR(255) NOT NULL DEFAULT '',
  `code` SMALLINT(5) NOT NULL DEFAULT '0',
  `message` VARCHAR(32) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=INNODB;

Bash-скрипт для конвертации

video-converting.sh
#!/bin/bash
srcDir='/path/to/video/src/'
destDir='/path/to/webserver/root/'
destOffset='data/video/'
maxDaysLeft=30
 
videoFormats='mp4 ogv webm'
 
 
# action - код логирования (в т.ч. для уровня)
# message - текст
# code - результат (0 - как правило, успех)
 
function log() {
 
  addslashes "$1"
  action=$addslashes
 
  addslashes "$2"
  format=$addslashes
 
  addslashes "$3"
  src=$addslashes
 
  addslashes "$4"
  message=$addslashes
 
  addslashes "$5"
  code=$addslashes
 
  query="INSERT INTO convert_log (action, format, src, message, code) VALUES ('$action', '$format', '$src', '$message', '$code')"
  mysql -u$dbuser -p$dbpass -h$dbhost $dbname -e "$query"
}
 
function getVideoInfo() {
  videoId=$1
  query="SELECT filename FROM videos WHERE id = $videoId"
  filename=`mysql -s -N -u$dbuser -p$dbpass -h$dbhost $dbname -e "$query"`
  md5 $filename
}
 
function convertVideo() {
  videoId=$1
  format=$2
 
  getVideoInfo $videoId
 
  if [ -f "$destDir$destOffset$md5.$format" ]; then
    log "other" "$format" "$filename" "File already exists" 1
    return
  fi
 
  # различные параметры кодирования
  # -b - видеобитрэйт
  # - ab - аудиобитрэйт
  # - threads - количество потоков
  # - qscale - качество (1 - макс, 255 - мин)
  # -y - перезапись видео
  case "$format" in
  "ogv")
    echo y | ffmpeg -i "$srcDir$filename" -mbd rd -flags +ilme+ildct -trellis 2 -cmp 2 -subcmp 2 -g 300 -b 2500k -ab 192k -threads 8 -y "$destDir$destOffset$md5.$format"
    ;;
  "mp4")
    echo y | ffmpeg -i "$srcDir$filename" -mbd rd -flags +ilme+ildct -trellis 2 -cmp 2 -subcmp 2 -g 300 -b 2500k -ab 192k -threads 8 -vcodec libx264 -y "$destDir$destOffset~$md5.$format"
    qt-faststart "$destDir$destOffset~$md5.$format" "$destDir$destOffset$md5.$format"
    rm -f "$destDir$destOffset~$md5.$format"
    ;;
  *)
    echo y | ffmpeg -i "$srcDir$filename" -mbd rd -flags +ilme+ildct -trellis 2 -cmp 2 -subcmp 2 -g 300 -qscale 1 -ab 192k -threads 8 -y "$destDir$destOffset$md5.$format"
    ;;
  esac
 
  result=$?
  if [ "$result" != "0" ]; then
    rm "$destDir$destOffset$md5.$format"
  else
    query="INSERT INTO convert_videos (video_id, format, filename) VALUES ($videoId, '$format', '$destOffset$md5.$format')"
    mysql -s -N -u$dbuser -p$dbpass -h$dbhost $dbname -e "$query"
  fi
  log "ffmpeg" "$format" "$filename" "Make video" "$result"
}
 
function makeThumbnail() {
  # ffmpeg -i ../data/video-src/intro.avi 2>&1 | grep -o -P "(?<=Duration: ).*?(?=,)"
  # ffmpeg -i ../data/video-src/intro.avi -vf  "thumbnail" -frames:v 1 thumb.png
  videoId=$1
 
  getVideoInfo $videoId
 
  echo y | ffmpeg -i "$srcDir$filename" -qscale 1 -ss 00:00:5.000 -f image2 -vframes 1 -y "$destDir$destOffset$md5.jpg"
  result=$?
  if [ "$result" != "0" ]; then
    rm "$destDir$destOffset$md5.jpg"
  else
    query="UPDATE videos SET thumbnail = '$destOffset$md5.jpg' WHERE id = $videoId"
    mysql -s -N -u$dbuser -p$dbpass -h$dbhost $dbname -e "$query"
  fi
  log "ffmpeg" "thumbnail" "$filename" "Make thumbnail $filename" "$result"
}
 
# EXECUTION START
cd $(dirname $(readlink -f $0))
source bash-lib/common.sh
source bash-lib/db_config.sh
amIAlone
if [ $? = "1" ]; then
  echo 'Script is already running'
  exit 1
fi
 
if [ ! -d "$srcDir" ]; then 
  log "init" "-" "-" "Can not open source dir $srcDir" 1
  exit 2
fi
 
if [ ! -d "$destDir$destOffset" ]; then 
  log "init" "-" "-" "Can not open dest dir $destDir$destOffset" 1
  exit 2
fi
 
# Thumbnails
query="SELECT id
       FROM videos
       WHERE thumbnail = ''
       AND datetime >= ADDDATE(NOW(), INTERVAL -$maxDaysLeft DAY)
       LIMIT 10
       "
videoIds=`mysql -s -N -u$dbuser -p$dbpass -h$dbhost $dbname -e "$query"`
 
if [ -n "videoIds" ]; then
  for videoId in $videoIds; do
    makeThumbnail "$videoId"
  done
fi
 
# получаем для каждого формата имя файла, который еще не был сконвертирован
for format in $videoFormats; do
  query="SELECT id
         FROM videos
         WHERE id NOT IN (
            SELECT videos.id
            FROM videos
            JOIN convert_videos ON (videos.id = convert_videos.video_id)
            WHERE format = '$format'
            )
         AND datetime >= ADDDATE(NOW(), INTERVAL -$maxDaysLeft DAY)
         LIMIT 10
         "
  videoIds=`mysql -s -N -u$dbuser -p$dbpass -h$dbhost $dbname -e "$query"`
  if [ -n "videoIds" ]; then
    for videoId in $videoIds; do
      convertVideo "$videoId" "$format"
    done
  fi
done
bash-lib/common.sh
function addslashes() {
  addslashes=`echo "$1" | sed "s/'/\\\\'/g"`
}
 
function md5() {
  md5=`echo -n "$1" | /usr/bin/md5sum | cut -f1 -d" "`
}
 
# проверка на наличие уже запущенной копии
function amIAlone() {
  lock_file=`pwd`"/.tv_lock"
  do_exit() {
    RETURN_VALUE=$?
    rm -f "$lock_file"
    exit $RETURN_VALUE
  }
  lockfile -r 0 "$lock_file" || return 1
  trap do_exit EXIT
  return 0
}
bash-lib/common.sh
dbname='data_base_name'
dbuser='db_user'
dbpass='db_password'
dbhost='db_host'

Настройка скрипта

bash-lib/common.sh - параметры подключения к БД.

video-converting.sh:

Параметр Описание
srcDir абсолютный путь к исходным файлам с видео
destDir абсолютный путь к конечным файлам с видео
destOffset относительный путь - будет записан в БД и выводиться на сайте
maxDaysLeft количество дней, в течение которых данный файл будет обрабатываться
videoFormats конечные видео-форматы

Запуск по расписанию

Данный скрипт возможно повесить на cron:

*/2 * * * * /path/to/script/video-converting.sh > /dev/null 2>&1

Не следует беспокоиться об одновременном запуске нескольких копий - в скрипте есть проверка.

Полезные опции ffmpeg

Проверка на ошибки / повреждения видеофайла:

ffmpeg -v error -i example.mov -f null -