Есть ли элегантный способ разделить файл по разделам с помощью 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)