Есть ли элегантный способ разделить файл по разделам с помощью ffmpeg?

На этой странице Albert Armea делится кодом для разделения видео по разделам с помощью ffmpeg. Код прямолинейный, но не совсем красивый.

ffmpeg -i "$ SOURCE. $ EXT" 2> & 1 | grep Chapter | sed -E "s/* Chapter # ([0-9]+. [0-9] +): start ([0-9]+. [0-9] +), end ([0-9]+. [0-9] +) / -i\"$ SOURCE. $ EXT \" -vcodec копировать -acodec копию -ss\2 -to\3\"$SOURCE-\1. $ EXT\"/" | xargs -n 11 ffmpeg

Есть ли элегантный способ сделать эту работу?

Ответы

Ответ 1

(Изменение: этот совет пришел с https://github.com/phiresky через эту проблему: https://github.com/harryjackson/ffmpeg_split/issues/2)

Вы можете получить главы, используя:

ffprobe -i fname -print_format json -show_chapters -loglevel error

Если бы я писал это снова, я бы использовал опции ffprobe json

(Оригинальный ответ следует)

Это рабочий скрипт Python. Я проверял это на нескольких видео, и это работало хорошо. Python не мой первый язык, но я заметил, что вы используете его, поэтому я думаю, что написание его на Python может иметь больше смысла. Я добавил это в Github. Если вы хотите улучшить, пожалуйста, отправьте запросы на извлечение.

#!/usr/bin/env python
import os
import re
import subprocess as sp
from subprocess import *
from optparse import OptionParser

def parseChapters(filename):
  chapters = []
  command = [ "ffmpeg", '-i', filename]
  output = ""
  try:
    # ffmpeg requires an output file and so it errors 
    # when it does not get one so we need to capture stderr, 
    # not stdout.
    output = sp.check_output(command, stderr=sp.STDOUT, universal_newlines=True)
  except CalledProcessError, e:
    output = e.output 

  for line in iter(output.splitlines()):
    m = re.match(r".*Chapter #(\d+:\d+): start (\d+\.\d+), end (\d+\.\d+).*", line)
    num = 0 
    if m != None:
      chapters.append({ "name": m.group(1), "start": m.group(2), "end": m.group(3)})
      num += 1
  return chapters

def getChapters():
  parser = OptionParser(usage="usage: %prog [options] filename", version="%prog 1.0")
  parser.add_option("-f", "--file",dest="infile", help="Input File", metavar="FILE")
  (options, args) = parser.parse_args()
  if not options.infile:
    parser.error('Filename required')
  chapters = parseChapters(options.infile)
  fbase, fext = os.path.splitext(options.infile)
  for chap in chapters:
    print "start:" +  chap['start']
    chap['outfile'] = fbase + "-ch-"+ chap['name'] + fext
    chap['origfile'] = options.infile
    print chap['outfile']
  return chapters

def convertChapters(chapters):
  for chap in chapters:
    print "start:" +  chap['start']
    print chap
    command = [
        "ffmpeg", '-i', chap['origfile'],
        '-vcodec', 'copy',
        '-acodec', 'copy',
        '-ss', chap['start'],
        '-to', chap['end'],
        chap['outfile']]
    output = ""
    try:
      # ffmpeg requires an output file and so it errors 
      # when it does not get one
      output = sp.check_output(command, stderr=sp.STDOUT, universal_newlines=True)
    except CalledProcessError, e:
      output = e.output
      raise RuntimeError("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))

if __name__ == '__main__':
  chapters = getChapters()
  convertChapters(chapters)

Ответ 2

ffmpeg -i "$SOURCE.$EXT" 2>&1 \ # get metadata about file
| grep Chapter \ # search for Chapter in metadata and pass the results
| sed -E "s/ *Chapter #([0-9]+.[0-9]+): start ([0-9]+.[0-9]+), end ([0-9]+.[0-9]+)/-i \"$SOURCE.$EXT\" -vcodec copy -acodec copy -ss \2 -to \3 \"$SOURCE-\1.$EXT\"/" \ # filter the results, explicitly defining the timecode markers for each chapter
| xargs -n 11 ffmpeg # construct argument list with maximum of 11 arguments and execute ffmpeg

Ваша команда анализирует метаданные файлов и считывает маркеры тайм-кода для каждой главы. Вы можете сделать это вручную для каждой главы.

ffmpeg -i ORIGINALFILE.mp4 -acodec copy -vcodec copy -ss 0 -t 00:15:00 OUTFILE-1.mp4

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

#!/bin/bash
# Author: http://crunchbang.org/forums/viewtopic.php?id=38748#p414992
# m4bronto

#     Chapter #0:0: start 0.000000, end 1290.013333
#       first   _     _     start    _     end

while [ $# -gt 0 ]; do

ffmpeg -i "$1" 2> tmp.txt

while read -r first _ _ start _ end; do
  if [[ $first = Chapter ]]; then
    read  # discard line with Metadata:
    read _ _ chapter

    ffmpeg -vsync 2 -i "$1" -ss "${start%?}" -to "$end" -vn -ar 44100 -ac 2 -ab 128  -f mp3 "$chapter.mp3" </dev/null

  fi
done <tmp.txt

rm tmp.txt

shift
done

или вы можете использовать HandbrakeCLI, как первоначально упоминалось в этом сообщении, этот пример извлекает главы с 3 по 3.mkv

HandBrakeCLI -c 3 -i originalfile.mkv -o 3.mkv

или другой инструмент упоминается в этом сообщении

mkvmerge -o output.mkv --split chapters:all input.mkv

Ответ 3

Я изменил сценарий Гарри, чтобы использовать имя главы для имени файла. Он выводит в новый каталог с именем входного файла (минус расширение). Он также префиксарует каждое название главы "1 -", "2 -" и т.д., Если есть главы с тем же именем.

#!/usr/bin/env python
import os
import re
import pprint
import sys
import subprocess as sp
from os.path import basename
from subprocess import *
from optparse import OptionParser

def parseChapters(filename):
  chapters = []
  command = [ "ffmpeg", '-i', filename]
  output = ""
  m = None
  title = None
  chapter_match = None
  try:
    # ffmpeg requires an output file and so it errors
    # when it does not get one so we need to capture stderr,
    # not stdout.
    output = sp.check_output(command, stderr=sp.STDOUT, universal_newlines=True)
  except CalledProcessError, e:
    output = e.output

  num = 1

  for line in iter(output.splitlines()):
    x = re.match(r".*title.*: (.*)", line)
    print "x:"
    pprint.pprint(x)

    print "title:"
    pprint.pprint(title)

    if x == None:
      m1 = re.match(r".*Chapter #(\d+:\d+): start (\d+\.\d+), end (\d+\.\d+).*", line)
      title = None
    else:
      title = x.group(1)

    if m1 != None:
      chapter_match = m1

    print "chapter_match:"
    pprint.pprint(chapter_match)

    if title != None and chapter_match != None:
      m = chapter_match
      pprint.pprint(title)
    else:
      m = None

    if m != None:
      chapters.append({ "name": 'num' + " - " + title, "start": m.group(2), "end": m.group(3)})
      num += 1

  return chapters

def getChapters():
  parser = OptionParser(usage="usage: %prog [options] filename", version="%prog 1.0")
  parser.add_option("-f", "--file",dest="infile", help="Input File", metavar="FILE")
  (options, args) = parser.parse_args()
  if not options.infile:
    parser.error('Filename required')
  chapters = parseChapters(options.infile)
  fbase, fext = os.path.splitext(options.infile)
  path, file = os.path.split(options.infile)
  newdir, fext = os.path.splitext( basename(options.infile) )

  os.mkdir(path + "/" + newdir)

  for chap in chapters:
    chap['name'] = chap['name'].replace('/',':')
    chap['name'] = chap['name'].replace("'","\'")
    print "start:" +  chap['start']
    chap['outfile'] = path + "/" + newdir + "/" + re.sub("[^-a-zA-Z0-9_.():' ]+", '', chap['name']) + fext
    chap['origfile'] = options.infile
    print chap['outfile']
  return chapters

def convertChapters(chapters):
  for chap in chapters:
    print "start:" +  chap['start']
    print chap
    command = [
        "ffmpeg", '-i', chap['origfile'],
        '-vcodec', 'copy',
        '-acodec', 'copy',
        '-ss', chap['start'],
        '-to', chap['end'],
        chap['outfile']]
    output = ""
    try:
      # ffmpeg requires an output file and so it errors
      # when it does not get one
      output = sp.check_output(command, stderr=sp.STDOUT, universal_newlines=True)
    except CalledProcessError, e:
      output = e.output
      raise RuntimeError("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))

if __name__ == '__main__':
  chapters = getChapters()
  convertChapters(chapters)

Это заняло много времени, чтобы понять, так как я определенно не парень Python. Это также неэлегантно, так как было много обручей, чтобы прыгать, поскольку он обрабатывает метаданные по строкам. (Т.е. данные заголовка и главы находятся в отдельных циклах через выход метаданных)

Но это работает, и это должно сэкономить вам много времени. Это было для меня!

Ответ 4

Я хотел несколько дополнительных вещей, таких как:

  • извлечение крышки
  • используя имя главы как имя файла
  • префикс счетчика имени файла с ведущими нулями, поэтому алфавитный порядок будет корректно работать в каждом программном обеспечении
  • создание плейлиста
  • изменение метаданных для включения названия главы
  • вывод всех файлов в новый каталог на основе метаданных (year author - title)

Вот мой сценарий (я использовал подсказку с выходом ffprobe json от Harry)

#!/bin/bash
input="input.aax"
EXT2="m4a"

json=$(ffprobe -activation_bytes secret -i "$input" -loglevel error -print_format json -show_format -show_chapters)
title=$(echo $json | jq -r ".format.tags.title")
count=$(echo $json | jq ".chapters | length")
target=$(echo $json | jq -r ".format.tags | .date + \" \" + .artist + \" - \" + .title")
mkdir "$target"

ffmpeg -activation_bytes secret -i $input -vframes 1 -f image2 "$target/cover.jpg"

echo "[playlist]
NumberOfEntries=$count" > "$target/0_Playlist.pls"

for i in $(seq -w 1 $count);
do
  j=$((10#$i))
  n=$(($j-1))
  start=$(echo $json | jq -r ".chapters[$n].start_time")
  end=$(echo $json | jq -r ".chapters[$n].end_time")
  name=$(echo $json | jq -r ".chapters[$n].tags.title")
  ffmpeg -activation_bytes secret -i $input -vn -acodec -map_chapters -1 copy -ss $start -to $end -metadata title="$title $name" "$target/$i $name.$EXT2"
  echo "File$j=$i $name.$EXT2" >> "$target/0_Playlist.pls"
done

Ответ 5

Версия исходного кода оболочки с

  • улучшена эффективность благодаря использованию ffprobe вместо ffmpeg,
  • упрощенное регулярное выражение,
  • повышенная надежность благодаря избеганию xargs и
  • улучшена читаемость благодаря использованию нескольких строк.

В моей версии 4.1 ffprobe номера глав разделены на : которые должны быть заменены на . чтобы ffmpeg жаловался на Protocol not found.

ffprobe "$INPUT" 2>&1 |
sed -En 's/.*Chapter #([0-9]+)[.:]([0-9]+): start ([0-9]+\.[0-9]+), end ([0-9]+\.[0-9]+).*/\1.\2 \3 \4/p' |
while read chapter start end
do
    ffmpeg </dev/null \
        -i "$INPUT" \
        -vcodec copy -acodec copy \
        -ss "$start" -to "$end" \
        "${INPUT%.*}-$chapter.${INPUT##*.}"
done

Ввод ffmpeg перенаправляется, чтобы предотвратить его вмешательство в цикл.

Ответ 6

в питоне

#!/usr/bin/env python3

import sys
import os
import subprocess
import shlex

def split_video(pathToInputVideo):
  command="ffprobe -v quiet -print_format csv -show_chapters "
  args=shlex.split(command)
  args.append(pathToInputVideo)
  output = subprocess.check_output(args, stderr=subprocess.STDOUT, universal_newlines=True)

  cpt=0
  for line in iter(output.splitlines()):
    dec=line.split(",")
    st_time=dec[4]
    end_time=dec[6]
    name=dec[7]

    command="ffmpeg -i _VIDEO_ -ss _START_ -to _STOP_ -vcodec copy -acodec copy"
    args=shlex.split(command)
    args[args.index("_VIDEO_")]=pathToInputVideo
    args[args.index("_START_")]=st_time
    args[args.index("_STOP_")]=end_time

    filename=os.path.basename(pathToInputVideo)
    words=filename.split(".");
    l=len(words)
    ext=words[l-1]

    cpt+=1
    filename=" ".join(words[0:l-1])+" - "+str(cpt)+" - "+name+"."+ext

    args.append(filename)
    subprocess.call(args)

for video in sys.argv[1:]:
  split_video(video)