有没有一种优雅的方法可以使用 ffmpeg 按章节拆分文件?
Is there an elegant way to split a file by chapter using ffmpeg?
在this page中,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 -to \"$SOURCE-.$EXT\"/" |
xargs -n 11 ffmpeg
有没有一种优雅的方式来完成这项工作?
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 -to \"$SOURCE-.$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 "" 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 "" -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,如 this post 中最初提到的那样,此示例将第 3 章提取到 3.mkv
HandBrakeCLI -c 3 -i originalfile.mkv -o 3.mkv
中提及的其他工具
mkvmerge -o output.mkv --split chapters:all input.mkv
(编辑:此提示来自 https://github.com/phiresky via this issue: 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)
我修改了 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)
这花了很多时间才弄清楚,因为我绝对不是 Python 人。它也不优雅,因为它是逐行处理元数据,需要跳过很多环节。 (即,标题和章节数据在元数据输出的单独循环中找到)
但它确实有效,应该可以为您节省很多时间。它对我有用!
我想要一些额外的东西,例如:
- 提取封面
- 使用章节名作为文件名
- 用前导零为文件名添加一个计数器前缀,这样字母顺序在每个软件中都能正常工作
- 制作播放列表
- 修改元数据以包含章节名称
- 根据元数据(作者年份 - 标题)将所有文件输出到新目录
这是我的脚本(我使用了来自 Harry 的 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
原始 shell 代码的一个版本:
- 提高了效率
- 使用
ffprobe
而不是 ffmpeg
- 拆分输入而不是输出
- 通过避免
xargs
和 sed
提高了可靠性
- 使用多行提高了可读性
- 携带多个音频或字幕流
- 从输出文件中删除章节(因为它们是无效的时间码)
- 简化的命令行参数
#!/bin/sh -efu
input=""
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
读取。
在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)
通过使用JSON和jq
比使用sed
提取数据更简单一点:
#!/usr/bin/env bash
# For systems where "bash" in not in "/bin/"
set -efu
videoFile=""
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
是一个整数。
前几天我试图自己拆分 .m4b 有声读物,无意中发现了这个线程和其他线程,但我找不到任何使用批处理脚本的示例。我不知道 python 或 bash,而且我根本不是批处理方面的专家,但我试图阅读如何做,并提出了以下似乎可行的方法.
这会将按章节编号的 MP3 文件导出到与源文件相同的路径:
@echo off
setlocal enabledelayedexpansion
for /f "tokens=2,5,7,8 delims=," %%G in ('c:\ffmpeg\bin\ffprobe -i %1 -print_format csv -show_chapters -loglevel error 2^> nul') do (
set padded=00%%G
"c:\ffmpeg\bin\ffmpeg" -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:\ffmpeg\bin\ffprobe -i %1 -print_format csv -show_chapters -loglevel error 2^> nul') do (
set padded=00%%G
"c:\ffmpeg\bin\ffmpeg" -ss %%H -to %%I -i %1 -c:v copy -c:a copy -metadata title="%%J" -y "%~dpnx1-!padded:~-3!.mkv"
)
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
json 输出 raw
对象将包含具有以下结构的 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"
}
}]
}
这是 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)"
}
在this page中,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 -to \"$SOURCE-.$EXT\"/" |
xargs -n 11 ffmpeg
有没有一种优雅的方式来完成这项工作?
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 -to \"$SOURCE-.$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 "" 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 "" -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,如 this post 中最初提到的那样,此示例将第 3 章提取到 3.mkv
HandBrakeCLI -c 3 -i originalfile.mkv -o 3.mkv
中提及的其他工具
mkvmerge -o output.mkv --split chapters:all input.mkv
(编辑:此提示来自 https://github.com/phiresky via this issue: 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)
我修改了 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)
这花了很多时间才弄清楚,因为我绝对不是 Python 人。它也不优雅,因为它是逐行处理元数据,需要跳过很多环节。 (即,标题和章节数据在元数据输出的单独循环中找到)
但它确实有效,应该可以为您节省很多时间。它对我有用!
我想要一些额外的东西,例如:
- 提取封面
- 使用章节名作为文件名
- 用前导零为文件名添加一个计数器前缀,这样字母顺序在每个软件中都能正常工作
- 制作播放列表
- 修改元数据以包含章节名称
- 根据元数据(作者年份 - 标题)将所有文件输出到新目录
这是我的脚本(我使用了来自 Harry 的 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
原始 shell 代码的一个版本:
- 提高了效率
- 使用
ffprobe
而不是ffmpeg
- 拆分输入而不是输出
- 使用
- 通过避免
xargs
和sed
提高了可靠性
- 使用多行提高了可读性
- 携带多个音频或字幕流
- 从输出文件中删除章节(因为它们是无效的时间码)
- 简化的命令行参数
#!/bin/sh -efu
input=""
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
读取。
在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)
通过使用JSON和jq
比使用sed
提取数据更简单一点:
#!/usr/bin/env bash
# For systems where "bash" in not in "/bin/"
set -efu
videoFile=""
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
是一个整数。
前几天我试图自己拆分 .m4b 有声读物,无意中发现了这个线程和其他线程,但我找不到任何使用批处理脚本的示例。我不知道 python 或 bash,而且我根本不是批处理方面的专家,但我试图阅读如何做,并提出了以下似乎可行的方法.
这会将按章节编号的 MP3 文件导出到与源文件相同的路径:
@echo off
setlocal enabledelayedexpansion
for /f "tokens=2,5,7,8 delims=," %%G in ('c:\ffmpeg\bin\ffprobe -i %1 -print_format csv -show_chapters -loglevel error 2^> nul') do (
set padded=00%%G
"c:\ffmpeg\bin\ffmpeg" -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:\ffmpeg\bin\ffprobe -i %1 -print_format csv -show_chapters -loglevel error 2^> nul') do (
set padded=00%%G
"c:\ffmpeg\bin\ffmpeg" -ss %%H -to %%I -i %1 -c:v copy -c:a copy -metadata title="%%J" -y "%~dpnx1-!padded:~-3!.mkv"
)
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
json 输出 raw
对象将包含具有以下结构的 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"
}
}]
}
这是 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)"
}