有没有一种优雅的方法可以使用 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 copy -acodec copy -ss 2 -to 3 "$SOURCE-1.$EXT"/" |
xargs -n 11 ffmpeg

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

(编辑:此提示来自 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)

原始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

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

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

比使用 JSON 和 sed jq 来提取数据简单一点:

#!/usr/bin/env bash 
# For systems where "bash" in not in "/bin/"
set -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 
               -map_chapters -1 
               -c copy 
               -metadata title="$chapter"
               "${videoFile%.*}-$chapter.${videoFile##*.}";
    done

我使用 tostring jq 函数chapers[].id因为它是一个整数。

我修改了哈利的脚本,使用章节名称作为文件名。它输出到具有输入文件名称(减去扩展名(的新目录中。它还在每个章节名称前面加上"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 人。它也很不优雅,因为它正在逐行处理元数据,因此有很多箍需要跳过。(即,标题和章节数据通过元数据输出在单独的循环中找到(

但它有效,应该可以为您节省大量时间。它为我做到了!

前几天我试图自己拆分一本 .m4b 有声读物,偶然发现了这个线程和其他线程,但我找不到任何使用 batch-script 的示例。我不懂 python 或 bash,我根本不是批处理专家,但我试图阅读如何做到这一点,并提出了以下似乎有效的方法。

这会将按章节编号的 MP3 文件导出到与源文件相同的路径:

@echo off
setlocal enabledelayedexpansion
for /f "tokens=2,5,7,8 delims=," %%G in ('c:ffmpegbinffprobe -i %1 -print_format csv -show_chapters -loglevel error  2^> nul') do (
   set padded=00%%G
   "c:ffmpegbinffmpeg" -ss %%H -to %%I -i %1 -vn -c:a libmp3lame -b:a 32k -ac 1 -metadata title="%%J" -id3v2_version 3 -write_id3v1 1 -y "%~dpnx1-!padded:~-3!.mp3"
)

对于您的视频文件文件,我已将其更改为以下内容,以通过直接复制来处理视频和音频数据。我没有带有章节的视频文件,所以我无法测试它,但我希望它有效。

@echo off
setlocal enabledelayedexpansion
for /f "tokens=2,5,7,8 delims=," %%G in ('c:ffmpegbinffprobe -i %1 -print_format csv -show_chapters -loglevel error  2^> nul') do (
   set padded=00%%G
   "c:ffmpegbinffmpeg" -ss %%H -to %%I -i %1 -c:v copy -c:a copy -metadata title="%%J" -y "%~dpnx1-!padded:~-3!.mkv"
)
这是

PowerShell版本

$filePath = 'C:InputVideo.mp4'
$file = Get-Item $filePath
$json = ConvertFrom-Json (ffprobe -i $filePath -print_format json -show_chapters -loglevel error | Out-String)
foreach($chapter in $json.chapters)
{
    ffmpeg -loglevel error -i $filePath -c copy -ss $chapter.start_time -to $chapter.end_time "$($file.DirectoryName)$($chapter.id).$($file.Extension)"
}

我想要一些额外的东西,比如:

  • 提取盖子
  • 使用章节名称作为文件名
  • 文件名前面加上前导零的计数器,因此字母顺序在每个软件中都可以正常工作
  • 制作播放列表
  • 修改元数据以包含章节名称
  • 根据元数据将所有文件输出到新目录(年份作者 - 标题(

这是我的脚本(我使用了哈利的ffprobe json输出的提示(

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

in python

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

NodeJS/JavaScript 中的朴素解决方案

const probe = function (fpath, debug) {
      var self = this;
      return new Promise((resolve, reject) => {
        var loglevel = debug ? 'debug' : 'error';
        const args = [
          '-v', 'quiet',
          '-loglevel', loglevel,
          '-print_format', 'json',
          '-show_chapters',
          '-show_format',
          '-show_streams',
          '-i', fpath
        ];
        const opts = {
          cwd: self._options.tempDir
        };
        const cb = (error, stdout) => {
          if (error)
            return reject(error);
          try {
            const outputObj = JSON.parse(stdout);
            return resolve(outputObj);
          } catch (ex) {
            self.logger.error("probe failed %s", ex);
            return reject(ex);
          }
        };
        console.log(args)
        cp.execFile('ffprobe', args, opts, cb)
          .on('error', reject);
      });
    }//probe

raw对象的 json 输出将包含一个具有以下结构的 chapters 数组:

{
    "chapters": [{
        "id": 0,
        "time_base": "1/1000",
        "start": 0,
        "start_time": "0.000000",
        "end": 145000,
        "end_time": "135.000000",
        "tags": {
            "title": "This is Chapter 1"
        }
    }]
}
调整

了这个答案,使输出视频名称为"[计数]-[章节].xyz">

input="$1"
count=0
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 
        "${count}-$chapter.${input##*.}"
    count=$((count+=1))
done

相关内容

  • 没有找到相关文章

最新更新