tutorial - ¿Hay una manera elegante de dividir un archivo por capítulo usando ffmpeg?
ffmpeg tutorial (5)
(Edición: este consejo provino de https://github.com/phiresky través de este problema: https://github.com/harryjackson/ffmpeg_split/issues/2 )
Puedes obtener capítulos usando:
ffprobe -i fname -print_format json -show_chapters -loglevel error
Si volviera a escribir esto, usaría las opciones json de ffprobe
(La respuesta original sigue)
Este es un script de python de trabajo. Lo probé en varios videos y funcionó bien. Python no es mi primer idioma, pero noté que lo usas, así que creo que escribirlo en Python podría tener más sentido. Lo he añadido a Github . Si desea mejorar por favor envíe solicitudes de extracción.
#!/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)
En esta página , Albert Armea comparte un código para dividir videos por capítulo usando ffmpeg
. El código es sencillo, pero no bastante atractivo.
ffmpeg -i "$ FUENTE. $ EXT" 2> & 1 | Capítulo grep | sed -E "s / * Capítulo # ([0-9] +. [0-9] +): inicio ([0-9] +. [0-9] +), final ([0-9] + . [0-9] +) / - i / "$ SOURCE. $ EXT /" -copia de vcodec -codecec -ss / 2 -to / 3 / "$ SOURCE- / 1. $ EXT /" / "| xargs -n 11 ffmpeg
¿Hay una manera elegante de hacer este trabajo?
Modifiqué el script de Harry para usar el nombre del capítulo para el nombre del archivo. Da salida a un nuevo directorio con el nombre del archivo de entrada (extensión menos). También prefija el nombre de cada capítulo con "1 -", "2 -", etc. en caso de que haya capítulos con el mismo nombre.
#!/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)
Esto tomó un poco de tiempo, ya que definitivamente no soy un tipo de Python. También es poco elegante, ya que había muchos obstáculos para saltar, ya que está procesando los metadatos línea por línea. (Es decir, los datos de título y capítulo se encuentran en bucles separados a través de la salida de metadatos)
Pero funciona y debería ahorrarle mucho tiempo. ¡Lo hizo por mí!
Quería algunas cosas extra como:
- extrayendo la tapa
- usando el nombre del capítulo como nombre de archivo
- prefijando un contador al nombre de archivo con ceros iniciales, para que el orden alfabético funcione correctamente en cada software
- haciendo una lista de reproducción
- Modificar los metadatos para incluir el nombre del capítulo.
- salida de todos los archivos a un nuevo directorio basado en metadatos (año autor - título)
Aquí está mi script (utilicé la sugerencia con la salida de ffprobe json de 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
Una versión del código de shell original con
- mejora la eficiencia utilizando
ffprobe
lugar deffmpeg
, - expresión regular simplificada,
- Fiabilidad mejorada al evitar
xargs
y - Mejor legibilidad mediante el uso de múltiples líneas.
En mi versión 4.1 de ffprobe
, los números de los capítulos están separados por :
cuales deben ser reemplazados por .
para evitar que ffmpeg
queje de un 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
La entrada de ffmpeg
se redirige para evitar que interfiera con el bucle.
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
Su comando analiza los metadatos de los archivos y lee los marcadores de código de tiempo de cada capítulo. Podrías hacer esto manualmente para cada capítulo.
ffmpeg -i ORIGINALFILE.mp4 -acodec copy -vcodec copy -ss 0 -t 00:15:00 OUTFILE-1.mp4
o puede escribir los marcadores de capítulo y ejecutarlos con este script bash que es un poco más fácil de leer.
#!/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
o puede usar HandbrakeCLI, como se mencionó originalmente en esta publicación , este ejemplo extrae el capítulo 3 a 3.mkv
HandBrakeCLI -c 3 -i originalfile.mkv -o 3.mkv
u otra herramienta se menciona en este post
mkvmerge -o output.mkv --split chapters:all input.mkv