递归运行ffprobe获取编解码器类型

Recursively run ffprobe to get codec types

我稍微修改了 @rojo code from here 以查找 h264/AC3 并递归 运行 通过所有子文件夹。我唯一的问题是它总是说视频有 h264 和 AC3,但是当我 运行 手动使用 ffprobe 命令时,它的状态有所不同。我错过了什么吗?

@if (@CodeSection == @Batch) @then
@echo off & setlocal

for /R %%f in (*.mkv, *.mp4) do (
    echo Testing %%f

    set ffprobe=C:\ffmpeg-4.0.2-win64-static\bin\ffprobe -v quiet -show_entries "stream=codec_name,height" -of json "%%f"

    for /f "delims=" %%I in ('%ffprobe% ^| cscript /nologo /e:JScript "%~f0"') do set "%%~I"

    set "pre=-hide_banner -fflags +genpts+discardcorrupt+fastseek -analyzeduration 100M"
    set "pre=%pre% -probesize 50M -hwaccel dxva2 -y -threads 3 -v error -stats"
    set "global="
    set "video=-c:v h264_nvenc"
    set "audio=-c:a ac3"

    if defined h264 if defined ac3 (
        echo %%~nf already in x264 + AC3 format.
    )

    if not defined h264 if not defined ac3 (

        if not defined ac3 (
            echo Already has AC3 audio.  Re-encoding video only.
            set "audio=-c:a copy"
        ) 

        if not defined h264 (
            echo Already has h264 video.  Re-encoding audio only.
            set "video=-c:v copy"
        )

        echo output "%%~df%%~pf%%~nf.new.mkv"
        echo C:\ffmpeg-4.0.2-win64-static\bin\ffmpeg %pre% -i "%%f" %global% %video% %audio% "%%~df%%~pf%%~nf.new.mkv"

        pause

        echo del "%%f" /f /q
        echo ren "%%~df%%~pf%%~nf.new.mkv" "%%f"
    )

)
@end // end Batch / begin JScript

var stdin = WSH.CreateObject('Scripting.FileSystemObject').GetStandardStream(0),
    htmlfile = WSH.CreateObject('htmlfile'),
    JSON;

htmlfile.write('<meta http-equiv="x-ua-compatible" content="IE=9" />');
htmlfile.close(JSON = htmlfile.parentWindow.JSON);

var obj = JSON.parse(stdin.ReadAll());

for (var i = obj.streams.length; i--;) {
    if (/h264/i.test(obj.streams[i].codec_name)) WSH.Echo('h264=true');
    if (/ac3/i.test(obj.streams[i].codec_name)) WSH.Echo('ac3=true');
}

我工作了一秒钟,然后无缘无故地停止了。

@if (@CodeSection == @Batch) @then
@echo off & setlocal & goto run

:run
for /R %%f in (*.mkv, *.mp4) do (
    echo Testing %%f

    set "file=%%f"
    set "drive=%%~df"
    set "dir=%%~pf"
    set "name=%%~nf"
    set "ext=%%~xf"

    for /f "delims=" %%I in ('C:\ffmpeg-4.0.2-win64-static\bin\ffprobe.exe -v quiet -show_entries "stream=codec_name,height" -of json "%%f" ^| cscript /nologo /e:JScript "%~f0"') do (set "%%~I")

    set "pre=-hide_banner -fflags +genpts+discardcorrupt+fastseek -analyzeduration 100M"
    set "pre=%pre% -probesize 50M -hwaccel dxva2 -y -threads 3 -v error -stats"
    set "global="
    set "video=-c:v h264_nvenc"
    set "audio=-c:a ac3"

    if defined ac3 if defined h264 call :both
    if not defined ac3 call :either
    if not defined h264 call :either
)

:both
echo %name% already in x264 + AC3 format.
goto :EOF

:either
if not defined h264 (
    echo Already has AC3 audio.  Re-encoding video only.
    set "audio=-c:a copy"
) 

if not defined ac3 (
    echo Already has h264 video.  Re-encoding audio only.
    set "video=-c:v copy"
)

echo "C:\ffmpeg-4.0.2-win64-static\bin\ffmpeg %pre% -i "%file%" %global% %video% %audio% "%drive%%dir%%name%.new.mkv""
echo del "%file%" /f /q
echo ren "%drive%%dir%%name%.new.mkv" "%name%%ext%"
goto :EOF

@end // end Batch / begin JScript

var stdin = WSH.CreateObject('Scripting.FileSystemObject').GetStandardStream(0),
    htmlfile = WSH.CreateObject('htmlfile'),
    JSON;

htmlfile.write('<meta http-equiv="x-ua-compatible" content="IE=9" />');
htmlfile.close(JSON = htmlfile.parentWindow.JSON);

var obj = JSON.parse(stdin.ReadAll());

for (var i = obj.streams.length; i--;) {
    if (/h264/i.test(obj.streams[i].codec_name)) WSH.Echo('h264=true');
    if (/ac3/i.test(obj.streams[i].codec_name)) WSH.Echo('ac3=true');
}

h264 的 ffprobe 输出

{
    "programs": [

    ],
    "streams": [
        {
            "codec_name": "h264",
            "height": 528
        },
        {
            "codec_name": "aac"
        }
    ]
}

ac3 的输出

{
    "programs": [

    ],
    "streams": [
        {
            "codec_name": "h265",
            "height": 528
        },
        {
            "codec_name": "ac3"
        }
    ]
}

两者的输出 ac3/h264

{
    "programs": [

    ],
    "streams": [
        {
            "codec_name": "h264",
            "height": 528
        },
        {
            "codec_name": "ac3"
        }
    ]
}

看起来 batch file / JScript hybrid script written by rojo 并不是为递归执行目录树中的所有 *.mkv 和 *.mp4 文件而设计的。为此,我完全重写了批处理文件并省略了 JScript 脚本部分。

由于选项 "stream=codec_name,height" 的缘故,看起来 ffprobe 输出的视频高度信息在这里并不是真正需要的,因为每个视频都应该独立于其高度进行处理。因此,ProbeOptions 定义行上的 "stream=codec_name" 应该足以让此任务将 ffprobe 的输出减少一行。

ffprobe 的 JSON 输出也可以在此用例中直接使用 FOR 循环使用逗号 , 作为分隔符进行处理,冒号 :,左方括号 [ 水平制表符 TAB,右方括号 ],左{ 和右大括号 } 正常 space SPACE。在处理 JSON 格式的输出时,可以完全忽略以 { 开头的行。区分大小写的字符串比较用于查明该行是否包含 codec_name 值,并将第一个 coder/decoder 值解释为视频编解码器,将第二个值解释为音频编解码器。

@echo off
setlocal EnableExtensions DisableDelayedExpansion
set "ProgramFolder=C:\ffmpeg-4.0.2-win64-static\bin"
set "ProbeOptions=-v quiet -show_entries "stream^^=codec_name" -of json"
set "MpegOptions=-hide_banner -fflags +genpts+discardcorrupt+fastseek -analyzeduration 100M -probesize 50M -hwaccel dxva2 -y -threads 3 -v error -stats"
set "FilesFound=0"
set "FilesEncoded=0"

for /F "delims=" %%I in ('dir *.mkv *.mp4 /A-D-H /B /S 2^>nul') do (
    set "FullFileName=%%I"
    set "TempFileName=%%~dpnI_new%%~xI"
    set "AudioCodec="
    set "AudioOption=ac3"
    set "VideoCodec="
    set "VideoOption=h264_nvenc"
    set /A FilesFound+=1

    for /F "eol={ tokens=1,2 delims=,:[ ]{} " %%B in ('""%ProgramFolder%\ffprobe.exe" %ProbeOptions% "%%I""') do (
        if "%%~B" == "codec_name" (
            if not defined VideoCodec (
                set "VideoCodec=%%~C"
                if "%%~C" == "h264" set "VideoOption=copy"
            ) else (
                set "AudioCodec=%%~C"
                if "%%~C" == "ac3" set "AudioOption=copy"
            )
        )
    )

    setlocal EnableDelayedExpansion
    echo(
    echo File: !FullFileName!
    echo Video codec: !VideoCodec!
    echo Audio codec: !AudioCodec!
    if not "!VideoOption!" == "!AudioOption!" (
        "%ProgramFolder%\ffmpeg.exe" %MpegOptions% -i "!FullFileName!" -c:v !VideoOption! -c:a !AudioOption! "!TempFileName!"
        if not errorlevel 1 (
            move /Y "!TempFileName!" "!FullFileName!"
            if not errorlevel 1 set /A FilesEncoded+=1
        )
        if exist "!TempFileName!" del "!TempFileName!"
    )
    endlocal
)

if %FilesFound% == 1 (set "PluralS=") else set "PluralS=s"
echo(
echo Re-encoded %FilesEncoded% of %FilesFound% video file%PluralS%.
endlocal
pause

注意:[]之间的白色space必须是批处理文件中的制表符!

批处理文件首先设置一个本地环境,启用此批处理文件所需的命令扩展,并禁用延迟环境变量扩展,以便能够处理文件名或文件路径中带有一个或多个感叹号的正确文件.

接下来定义了一些环境变量,以便稍后在脚本中使用。一些特殊的是变量 ProbeOptions 的定义,因为参数字符串 "stream=codec_name" 稍后必须传递给由 FOR 启动的单独命令进程,需要双重转义等号有两个 ^ 最终将 = 传递给 ffprobe.exe.

外部 FOR 在一个单独的命令进程中执行一次,在后台命令行中以 cmd.exe /C 启动:

dir *.mkv *.mp4 /A-D-H /B /S 2>nul

DIR 输出以处理此命令进程的 STDOUT

  • 只有文件名因为 /B
  • 个非隐藏文件,因为 /A-D-H(属性不是目录且未隐藏)
  • 匹配通配符模式 *.mkv*.mp4
  • 在当前目录及其所有子目录中,因为 /S
  • 完整路径也是因为 /S.

可能是找不到匹配的文件名,导致 DIR 输出错误消息来处理 STDERR。通过将其重定向到设备 NUL.

来抑制此错误消息

阅读有关 Using Command Redirection Operators 的 Microsoft 文章,了解 2>nul 的解释。重定向运算符 > 必须在 FOR 命令行上使用脱字符 ^ 进行转义,以便在 Windows 命令解释器处理此命令时将其解释为文字字符执行命令 FOR 之前的行,它在后台启动的单独命令进程中执行嵌入式 dir 命令行。

FOR 捕获所有输出到后台命令进程 STDOUT 的行,并在启动后处理它们 cmd.exe 终止。所以 FOR 正在处理一个完全限定文件名的列表,在 运行 循环时不会改变。

在 NTFS 驱动器上使用它也很安全:

for /R %%I in (*.mkv *.mp4) do (

这也会导致处理当前目录和所有子目录中的所有非隐藏 *.mkv 和 *.mp4 文件。 NTFS returns 按字母顺序排序的文件列表。但是这种方法在 FAT32 和 ExFAT 驱动器上存在问题,因为在循环的每次迭代中执行的代码都可能导致更新文件分配 table。 FAT32 和 ExFAT return 文件名匹配特定标准,就像当前存储在文件分配 table 中一样,目录中最后修改的文件始终位于目录 table 的底部。这意味着当 FAT32 和 ExFAT 文件系统在第一个、第二个、第三个...文件名 return 上循环 运行 时,文件名列表可能会发生变化。这可能会导致多次处理一个视频文件并跳过其他文件。所以最好在循环迭代开始之前处理一个完全加载到内存中的文件名列表。

FOR with option /F 在这种情况下默认跳过未由 DIR 输出的空行以及以 a 开头的行分号在这里也是不可能的,因为每一行都以驱动器号 C 开头。但是 FOR 会使用普通 space 和水平制表符作为字符串分隔符将每个捕获的行拆分为子字符串(标记),并且只会将第一个 space/tab 分隔的字符串分配给指定的循环变量 I。这里不需要这种行为,因为需要的始终是完整的限定文件名,即使包含一个或多个 spaces。出于这个原因,delims= 用于定义一个空的字符串分隔符列表,导致完全关闭字符串拆分行为并分配给循环变量 I 始终是找到的 *.mkv 或 * 的文件名包含路径、名称和扩展名的 .mp4 文件。

每次循环迭代都会发生以下情况:

  1. 当前*.mkv 或*.mp4 文件的完整文件名已分配给环境变量FullFileName
  2. 当前 *.mkv 或 *.mp4 文件的完全限定文件名 _new 插入到文件扩展名的左侧已分配给环境变量 TempFileName
  3. 环境变量 AudioCodec 如果存在于循环的前一次迭代中,则被删除。
  4. 环境变量 AudioOption 的字符串值 ac3 是所需的音频编解码器。
  5. 如果环境变量 VideoCodec 存在于上一次循环迭代中,则删除该变量。
  6. 环境变量 VideoOption 的字符串值 h264_nvenc 是所需的视频编解码器。
  7. 环境变量 FilesFound 通过命令 SET.
  8. 评估的简单算术表达式递增 1

然后另一个 FOR 用于 运行 ffprobe 后台命令行 cmd.exe /C。在这种特殊情况下,有必要将整个命令行用双引号引起来,因为参数字符串 "stream=codec_name" 可以使整个命令行正确传递给由 FOR 启动的其他命令进程.

内部FOR捕获ffprobe以JSON格式写入的输出来处理started的STDOUT命令进程并逐行处理此输出。感兴趣的只有包含 "codec_name" 的行。因此选项 eol={ 用于完全忽略所有以 { 开头的行。根据 ASCII table,选项 tokens=1,2 导致分配给指定循环变量 B 的第一个子字符串和分配给下一个循环变量 C 的第二个子字符串。使用选项 delims= 指定的分隔符列表导致或多或少地得到 属性 用双引号括起来的名称,如 "codec_name" 和它的值也用双引号括起来,如 "h264" assigned到循环变量 BC.

如果分配给循环变量 B 的字符串没有显式地用双引号引起来是区分大小写的等于字符串 "codec_name",那么这一行是真正有意义的。分配给循环变量 C 的编解码器值在不带双引号的情况下分配给环境变量 VideoCodecAudioCodec,具体取决于已在处理之一的 JSON 输出中找到的视频编解码器线之前。此外,可能稍后使用的视频或音频选项分别设置为 copy 视频或音频编解码器已经是所需的编解码器 h264 ac3

有必要在处理完ffprobe的输出后启用延迟环境变量扩展,以便能够处理之前在同一命令块中定义的环境变量值。阅读 以获取有关命令 SETLOCALENDLOCAL.

的详细信息

输出首先是带有 echo( 的空行,然后是当前视频文件及其当前视频和音频编解码器的完整限定文件名。

IF条件比较区分大小写的视频和音频选项。仅当当前视频文件已经 h264/ac3 编码时,这两个选项字符串才相同,在这种情况下,两个环境变量的值都为 copy。因此,如果两个比较的字符串 相同,则必须使用 ffmpeg 重新编码视频文件以更改视频编解码器或音频编解码器或两个编解码器。

视频文件的重新编码成功 ffmpeg 退出,退出代码 大于或等于 1,即值 0。在这种情况下,如果当前视频文件未被只读属性或 NTFS 权限写保护,则由 ffmpeg 创建的临时视频文件将移动到当前视频文件上并覆盖现有视频文件。

这些操作导致在 FAT32 和 ExFAT 驱动器上更新文件分配 table,这是外部 FOR 运行ning 的原因DIR 在循环迭代之前获取视频文件名列表到内存中。

环境变量 FilesEncoded 增加了一个原始视频文件,可以用重新编码的版本真正成功地替换它。

执行ffmpeg.exe后由ffmpeg创建的临时视频文件最终被删除,以防出现任何错误导致该文件在其他命令行后仍然存在。

最后,在处理所有非隐藏的*.mkv和*.mp4文件后,使用两个计数器环境变量输出摘要信息,并在停止批处理文件执行之前恢复初始环境,以便能够看到所有双击启动批处理文件时的输出。

要了解使用的命令及其工作原理,请打开命令提示符 window,在其中执行以下命令,并仔细阅读为每个命令显示的所有帮助页面。

  • del /?
  • dir /?
  • echo /?
  • endlocal /?
  • for /?
  • if /?
  • move /?
  • pause /?
  • set /?
  • setlocal /?