web-dev-qa-db-fra.com

Existe-t-il un moyen élégant de diviser un fichier par chapitre en utilisant ffmpeg?

Dans cette page , Albert Armea partage un code permettant de scinder les vidéos par chapitre à l'aide de ffmpeg. Le code est simple, mais pas très beau. 

ffmpeg -i "$ SOURCE. $ EXT" 2> & 1 | chapitre de grep | sed -E "s/* Chapitre # ([0-9] +. [0-9] +): début ([0-9] +. [0-9] +), fin ([0-9] + . [0-9] +)/- i\"$ SOURCE. $ EXT \" -vcodec copy -acodec copy -ss\2 -to\3\"$ SOURCE-\1. $ EXT \"/"| xargs -n 11 ffmpeg

Y a-t-il une manière élégante de faire ce travail?

20
Kattern

(Edit: Ce conseil provient de https://github.com/phiresky via ce numéro: https://github.com/harryjackson/ffmpeg_split/issues/2 )

Vous pouvez obtenir des chapitres en utilisant: 

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

Si j’écrivais encore, j’utiliserais les options json de ffprobe

(Réponse originale suit)

Ceci est un script de travail en python. Je l'ai testé sur plusieurs vidéos et cela a bien fonctionné. Python n’est pas ma langue maternelle, mais j’ai remarqué que vous l’utilisiez. J’imagine que l’écrire en Python aurait plus de sens. Je l'ai ajouté à Github . Si vous souhaitez améliorer, veuillez soumettre des demandes d'extraction.

#!/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 __== '__main__':
  chapters = getChapters()
  convertChapters(chapters)
18
Harry

J'ai modifié le script de Harry pour utiliser le nom du chapitre comme nom de fichier. Il sort dans un nouveau répertoire avec le nom du fichier d'entrée (extension moins). Il préfixe également chaque nom de chapitre avec "1 -", "2 -", etc. au cas où il y aurait des chapitres du même nom.

#!/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 __== '__main__':
  chapters = getChapters()
  convertChapters(chapters)

Cela a pris un peu de temps à comprendre puisque je ne suis définitivement PAS un gars de Python. Il est également inélégant, car il faut franchir de nombreux obstacles dans la mesure où il traite les métadonnées ligne par ligne. (Par exemple, les données de titre et de chapitre se trouvent dans des boucles séparées dans la sortie de métadonnées)

Mais cela fonctionne et cela devrait vous faire gagner beaucoup de temps. C'est fait pour moi!

3
clifgriffin
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

Votre commande parcourt les métadonnées des fichiers et lit les marqueurs de code temporel de chaque chapitre. Vous pouvez le faire manuellement pour chaque chapitre.

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

ou vous pouvez écrire les marqueurs de chapitre et les parcourir avec ce script bash qui est un peu plus facile à lire.

#!/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

ou vous pouvez utiliser HandbrakeCLI, comme mentionné à l’origine dans cet article , cet exemple extrait les chapitres 3 à 3.mkv

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

ou un autre outil est mentionné dans cet article

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

Une version du code Shell d'origine avec

  • amélioration de l'efficacité en utilisant ffprobe au lieu de ffmpeg,
  • expression régulière simplifiée,
  • amélioration de la fiabilité en évitant xargs et
  • lisibilité améliorée en utilisant plusieurs lignes.

Dans ma ffprobe version 4.1, les numéros de chapitre sont séparés par : et doivent être remplacés par . pour empêcher ffmpeg de se plaindre à propos de 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

L'entrée de ffmpeg est redirigée pour l'empêcher d'interférer avec la boucle.

0
joki

Je voulais quelques petites choses comme:

  • extraire la couverture
  • en utilisant le nom du chapitre comme nom de fichier
  • préfixant le compteur au nom de fichier avec des zéros non significatifs, l'ordre alphabétique fonctionnera correctement dans tous les logiciels
  • faire une playlist
  • modifier les métadonnées pour inclure le nom du chapitre
  • la sortie de tous les fichiers dans un nouveau répertoire basé sur les métadonnées (année auteur - titre)

Voici mon script (j'ai utilisé l'allusion avec ffprobe json sortie 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
0
kaefert