有没有一种优雅的方法来使用ffmpeg按章拆分文件?

Kat*_*ern 22 ffmpeg

这个页面中,Albert Armea分享了一个代码,用章节分割视频ffmpeg.代码很简单,但不太好看.

ffmpeg -i"$ SOURCE.$ EXT"2>&1 | grep章节| 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 \"/"| xargs -n 11 ffmpeg

有一种优雅的方式来完成这项工作吗?

Har*_*rry 25

(编辑:此提示来自https://github.com/phiresky来自此问题:https://github.com/harryjackson/ffmpeg_split/issues/2)

您可以使用以下章节获取章节:

ffprobe -i fname -print_format json -show_chapters -loglevel error
Run Code Online (Sandbox Code Playgroud)

如果我再写这个,我会使用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)
Run Code Online (Sandbox Code Playgroud)


jok*_*oki 13

原始 shell 代码的一个版本:

  • 通过提高效率
    • 使用 ffprobe代替ffmpeg
    • 拆分输入而不是输出
  • 通过避免提高可靠性 xargssed
  • 通过使用多行提高可读性
  • 携带多个音频或字幕流
  • 从输出文件中删除章节(因为它们将是无效的时间码)
  • 简化的命令行参数
#!/bin/sh -efu

input="$1"
ffprobe \
    -print_format csv \
    -show_chapters \
    "$input" |
cut -d ',' -f '5,7,8' |
while IFS=, read start end chapter
do
    ffmpeg \
        -nostdin \
        -ss "$start" -to "$end" \
        -i "$input" \
        -c copy \
        -map 0 \
        -map_chapters -1 \
        "${input%.*}-$chapter.${input##*.}"
done
Run Code Online (Sandbox Code Playgroud)

为了防止它干扰循环,ffmpeg指示不要从stdin.

  • 您可以使用“-nostdin”代替“</dev/null”,使用“-c copy”代替“-vcodec copy -acodec copy -scodec copy”,使用“-map 0”代替“-map 0:a” -map 0:v -map 0:s`。 (4认同)

dav*_*rey 7

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
Run Code Online (Sandbox Code Playgroud)

您的命令将解析文件元数据,并读出每个章节的时间码标记。您可以为每个章节手动执行此操作。

ffmpeg -i ORIGINALFILE.mp4 -acodec copy -vcodec copy -ss 0 -t 00:15:00 OUTFILE-1.mp4
Run Code Online (Sandbox Code Playgroud)

或者,您可以写出章节标记并使用此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
Run Code Online (Sandbox Code Playgroud)

或者您可以使用HandbrakeCLI,如博文中最初提到,本示例将第3章提取到3.mkv。

HandBrakeCLI -c 3 -i originalfile.mkv -o 3.mkv
Run Code Online (Sandbox Code Playgroud)

这篇文章中提到了另一种工具

mkvmerge -o output.mkv --split chapters:all input.mkv
Run Code Online (Sandbox Code Playgroud)

  • 为“mkvmerge”点赞。一张衬垫即可获得所有章节,甚至适用于 Windows (2认同)

Seb*_*bMa 7

sed使用 JSON with提取数据更简单一点jq

#!/bin/sh -efu

videoFile="$1"
ffprobe -hide_banner \
        "$videoFile" \
        -print_format json \
        -show_chapters \
        -loglevel error |
    jq -r '.chapters[] | [ .id, .start_time, .end_time | tostring ] | join(" ")' |
    while read chapter start end; do
        ffmpeg -nostdin \
               -ss "$start" -to "$end" \
               -i "$videoFile" \
               -map 0 \
               -c copy \
               "${videoFile%.*}-$chapter.${videoFile##*.}";
    done
Run Code Online (Sandbox Code Playgroud)

我使用tostring jq函数是因为其中一个chapers[]元素是整数(这里是.id)。


cli*_*fin 5

我修改了 Harry 的脚本以使用章节名称作为文件名。它以输入文件的名称(减去扩展名)输出到一个新目录中。它还在每个章节名称前加上“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)
Run Code Online (Sandbox Code Playgroud)

这花了很多时间才弄清楚,因为我绝对不是 Python 人。它也很不优雅,因为它正在逐行处理元数据,因此需要跳过许多环节。(即,通过元数据输出在单独的循环中找到标题和章节数据)

但它有效,它应该为您节省大量时间。它对我有用!