jsonffmpegjqtrimmkv

Is there a way to batch split a file by chapter with ffmpeg and then reassemble with mkvmerge in windows?


So I made a batch script originally with the ability to relatively precision trim a video into chapters without having to run by keyframes, but the code looks horrible and I can't get it to loop through all mp4 files nor get mkvmerge to append the files after splitting them. Code is below but be gentle it is my first try.

@echo off
setlocal enableDelayedExpansion

REM CODE BELOW CREATES JSON FILES FOR ALL MP4 FILES WITHIN THE SAME DIRECTORY
ffprobe -v quiet -print_format json -show_chapters -loglevel error "01x01.mp4" > "01x01.json"

REM CODE BELOW SETS VARIABLES FROM EACH SPECIFIC JSON
FOR /F "delims=" %%i in ('jq .chapters[2].start ^< 01x01.json') DO SET /A start1=%%i
FOR /F "delims=" %%j in ('jq .chapters[2].end ^< 01x01.json') DO SET /A end1=%%j

FOR /F "delims=" %%k in ('jq .chapters[4].start ^< 01x01.json') DO SET /A start2=%%k
FOR /F "delims=" %%l in ('jq .chapters[4].end ^< 01x01.json') DO SET /A end2=%%l

FOR /F "delims=" %%m in ('jq .chapters[6].start ^< 01x01.json') DO SET /A start3=%%m
FOR /F "delims=" %%n in ('jq .chapters[6].end ^< 01x01.json') DO SET /A end3=%%n

FOR /F "delims=" %%o in ('jq .chapters[8].start ^< 01x01.json') DO SET /A start4=%%o
FOR /F "delims=" %%p in ('jq .chapters[8].end ^< 01x01.json') DO SET /A end4=%%p

REM SETS THE DURATION OF EACH FILE TO USE PRECISION TIMING FOR START AND STOP TIMES
CALL vbs (%end1%-%start1%)/1000
SET duration1=%val%
CALL vbs (%end2%-%start2%)/1000
SET duration2=%val%
CALL vbs (%end3%-%start3%)/1000
SET duration3=%val%
CALL vbs (%end4%-%start4%)/1000
SET duration4=%val%

REM SETS THE START TIME IN SECONDS VS MILLISECONDS
CALL vbs (%start1%)/1000
SET start1=%val%
CALL vbs (%start2%)/1000
SET start2=%val%
CALL vbs (%start3%)/1000
SET start3=%val%
CALL vbs (%start4%)/1000
SET start4=%val%

REM TRIM AND SPLIT ORIGINAL FILE INTO SEPERATE SECTIONS BASED ON CHAPTER MARKERS
ffmpeg -ss %START1% -i 01x01.mp4 -ss 0 -c copy -to %DURATION1% -avoid_negative_ts make_zero 01x01-1.mp4
ffmpeg -ss %START2% -i 01x01.mp4 -ss 0 -c copy -to %DURATION2% -avoid_negative_ts make_zero 01x01-2.mp4
ffmpeg -ss %START3% -i 01x01.mp4 -ss 0 -c copy -to %DURATION3% -avoid_negative_ts make_zero 01x01-3.mp4
ffmpeg -ss %START4% -i 01x01.mp4 -ss 0 -c copy -to %DURATION4% -avoid_negative_ts make_zero 01x01-4.mp4

REM DELETES UNNEEDED JSON AFTER USE
del /s *.json

REM APPEND ALL MP4 FILES INTO COHESIVE MKV
for /d /r %%D in (*) do (
    pushd %%D
    set files=
    for %%F in (*.mp4) do set files=!files! + ^( "%%F" ^)
    if not "!files!"=="" %mkvmerge% -o "01x01-FINAL.mkv" !files:~2!
    popd
)

REM DELETE UNNEEDED MP4 ORIGINALS AND SPLIT FILES
del /s *.mp4

I know it is super long and every time I try to use a variable or a loop to run through all files it can't read the json file. I've been at this all day and I can use the script as is but I have to make a file for each iteration.

I was also hoping to be able to have it only pull chapters labeled as "video" but I haven't quite figured that one out yet.

I'll add the vbs batch file for the arithmetic section as well as the sample json if it will help.

@echo off
>"%temp%\VBS.vbs" echo Set fso = CreateObject("Scripting.FileSystemObject") : Wscript.echo (%*)
for /f "delims=" %%a in ('cscript /nologo "%temp%\VBS.vbs"') do set "val=%%a"
del "%temp%\VBS.vbs"
{
    "chapters": [
        {
            "id": 0,
            "time_base": "1/1000",
            "start": 0,
            "start_time": "0.000000",
            "end": 5590,
            "end_time": "5.590000",
            "tags": {
                "title": "Video"
            }
        },
        {
            "id": 1,
            "time_base": "1/1000",
            "start": 5590,
            "start_time": "5.590000",
            "end": 13994,
            "end_time": "13.994000",
            "tags": {
                "title": "Advertisement"
            }
        },
        {
            "id": 2,
            "time_base": "1/1000",
            "start": 13994,
            "start_time": "13.994000",
            "end": 163964,
            "end_time": "163.964000",
            "tags": {
                "title": "Video"
            }
        },
        {
            "id": 3,
            "time_base": "1/1000",
            "start": 163964,
            "start_time": "163.964000",
            "end": 195940,
            "end_time": "195.940000",
            "tags": {
                "title": "Advertisement"
            }
        },
        {
            "id": 4,
            "time_base": "1/1000",
            "start": 195940,
            "start_time": "195.940000",
            "end": 547849,
            "end_time": "547.849000",
            "tags": {
                "title": "Video"
            }
        },
        {
            "id": 5,
            "time_base": "1/1000",
            "start": 547849,
            "start_time": "547.849000",
            "end": 595850,
            "end_time": "595.850000",
            "tags": {
                "title": "Advertisement"
            }
        },
        {
            "id": 6,
            "time_base": "1/1000",
            "start": 595850,
            "start_time": "595.850000",
            "end": 1413588,
            "end_time": "1413.588000",
            "tags": {
                "title": "Video"
            }
        },
        {
            "id": 7,
            "time_base": "1/1000",
            "start": 1413588,
            "start_time": "1413.588000",
            "end": 1477569,
            "end_time": "1477.569000",
            "tags": {
                "title": "Advertisement"
            }
        },
        {
            "id": 8,
            "time_base": "1/1000",
            "start": 1477569,
            "start_time": "1477.569000",
            "end": 1529696,
            "end_time": "1529.696000",
            "tags": {
                "title": "Video"
            }
        }
    ]
}

I also tried using the start_time so I didn't have to do extra calculations but jq didn't like that either.

mkvmerge doesn't even try to run when I have it in here and I still need to cut 7 seconds off the end and 12 seconds off the front of it once it is all one file again.

Any help would be appreciated, I know it's a lot but I seem to have hit a roadblock or just sleep deprived at this point.

UPDATE

This works amazing I just need to figure out how to use files with spaces and I'm all set. I guess I could run a batch before hand replacing all spaces with underscores. That would probably work but I would like to not change filenames if I can help it.

@echo off

for %%i in (*.mp4) do (
FOR /F "delims=" %%A IN ('ffprobe -v quiet -print_format json -show_chapters -loglevel error "%%i" ^| xidel - -se "$json/(chapters)()[id!=0 and tags/title='Video']/concat('ffmpeg -ss ',start div 1000,' -i %%i -to ',((end - start) div 1000),' -c copy -avoid_negative_ts make_zero %%~ni-',position(),'.mp4')"') DO %%A
FOR /F "delims=" %%A IN ('xidel -s --xquery "concat('mkvmerge -o &quot;%%~ni-FINAL.mkv&quot; &quot;',join(file:list(.,false(),'%%~ni-*.mp4'),'&quot; + &quot;'),'&quot;')"') DO %%A
)

Solution

  • If I understand correctly, you want to remove the "Advertisement"-chapters. This could probably be done entirely with FFprobe, but without the source video I can't say for sure.
    You might want to take a look at the XML/HTML/JSON parser and command-line tool for this issue.

    First of all, you don't have to save FFprobe's output to a JSON-file. You can just pipe it to Xidel and select the objects / chapters labeled as "Video":

    C:\>ffprobe -v quiet -print_format json -show_chapters -loglevel error "01x01.mp4" | xidel -se "$json/(chapters)()[tags/title='Video']"
    {
      "id": 0,
      "time_base": "1/1000",
      "start": 0,
      "start_time": "0.000000",
      "end": 5590,
      "end_time": "5.590000",
      "tags": {
        "title": "Video"
      }
    }
    [...]
    {
      "id": 8,
      "time_base": "1/1000",
      "start": 1477569,
      "start_time": "1477.569000",
      "end": 1529696,
      "end_time": "1529.696000",
      "tags": {
        "title": "Video"
      }
    }
    

    To calculate the duration (in seconds) of the 4 chapters:

    C:\>ffprobe [...] | xidel -se "$json/(chapters)()[tags/title='Video']/((end - start) div 1000)"
    5.59
    149.97
    351.909
    817.738
    52.127
    

    To generate the FFmpeg strings / commands with these values and the start attribute:

    C:\>ffprobe [...] | xidel -se "$json/(chapters)()[tags/title='Video']/concat('ffmpeg -ss ',start div 1000,' -i \"01x01.mp4\" -to ',((end - start) div 1000),' -c copy -avoid_negative_ts make_zero \"01x01-',position(),'.mp4\"')"
    C:\>ffprobe [...] | xidel -se ^"^
      $json/(chapters)()[tags/title='Video']/concat(^
        'ffmpeg -ss ',start div 1000,^
        ' -i \"01x01.mp4\" -to ',((end - start) div 1000),^
        ' -c copy -avoid_negative_ts make_zero \"01x01-',position(),'.mp4\"'^
      )
    "
    ffmpeg -ss 0 -i "01x01.mp4" -to 5.59 -c copy -avoid_negative_ts make_zero "01x01-1.mp4"
    ffmpeg -ss 13.994 -i "01x01.mp4" -to 149.97 -c copy -avoid_negative_ts make_zero "01x01-2.mp4"
    ffmpeg -ss 195.94 -i "01x01.mp4" -to 351.909 -c copy -avoid_negative_ts make_zero "01x01-3.mp4"
    ffmpeg -ss 595.85 -i "01x01.mp4" -to 817.738 -c copy -avoid_negative_ts make_zero "01x01-4.mp4"
    ffmpeg -ss 1477.569 -i "01x01.mp4" -to 52.127 -c copy -avoid_negative_ts make_zero "01x01-5.mp4"
    

    To execute these FFmpeg commands, simply put the entire command in a FOR-loop:

    C:\>FOR /F "delims=" %A IN ('ffprobe [...] ^| xidel -se "$json/(chapters)()[tags/title='Video']/concat('ffmpeg -ss ',start div 1000,' -i \"01x01.mp4\" -to ',((end - start) div 1000),' -c copy -avoid_negative_ts make_zero \"01x01-'^,position^(^)^,'.mp4\"')"') DO @%A
    C:\>FOR /F "delims=" %A IN ('
      ffprobe [...] ^| xidel -se "
        $json/^(chapters^)^(^)[tags/title^='Video']/concat^(
          'ffmpeg -ss '^,start div 1000^,
          ' -i \"01x01.mp4\" -to '^,^(^(end - start^) div 1000^)^,
          ' -c copy -avoid_negative_ts make_zero \"01x01-',position(),'.mp4\"'
        ^)
      "
    ') DO @%A
    

    (within a Batch-script use %%A of course)

    To generate and execute the MKVMerge strings / commands, with the help of the integrated EXPath File Module:

    C:\>xidel -se "file:list(.,false(),'01x01-*.mp4')"
    01x01-1.mp4
    01x01-2.mp4
    01x01-3.mp4
    01x01-4.mp4
    01x01-5.mp4
    
    C:\>xidel -se "file:list(.,false(),'01x01-*.mp4') ! `\"{.}\"`"
    "01x01-1.mp4"
    "01x01-2.mp4"
    "01x01-3.mp4"
    "01x01-4.mp4"
    "01x01-5.mp4"
    
    C:\>xidel -se "'mkvmerge -o \"01x01-FINAL.mkv\" '||join(file:list(.,false(),'01x01-*.mp4') ! `\"{.}\"`,' + ')"
    mkvmerge -o "01x01-FINAL.mkv" "01x01-1.mp4" + "01x01-2.mp4" + "01x01-3.mp4" + "01x01-4.mp4" + "01x01-5.mp4"
    
    C:\>FOR /F "delims=" %A IN ('xidel -se "'mkvmerge -o \"01x01-FINAL.mkv\" '||join(file:list(.,false(),'01x01-*.mp4') ! `\"{.}\"`,' + ')"') DO @%A
    C:\>FOR /F "delims=" %A IN ('
      xidel -se "
        'mkvmerge -o \"01x01-FINAL.mkv\" '^|^|
        join^(
          file:list^(.^,false^(^)^,'01x01-*.mp4'^) ! `\"{.}\"`^,
          ' + '
        ^)
      "
    ') DO @%A
    

    If you want to process multiple mp4-files, then you could use the following Batch-script:

    FOR %%A IN (*.mp4) DO (
      FOR /F "delims=" %%B IN ('
        ffprobe -v quiet -print_format json -show_chapters -loglevel error "%%A" ^|
        xidel -se "
          $json/^(chapters^)^(^)[tags/title^='Video']/concat^(
            'ffmpeg -ss '^,start div 1000^,
            ' -i \"%%A\" -to '^,^(^(end - start^) div 1000^)^,
            ' -c copy -avoid_negative_ts make_zero \"%%~nA-',position(),'.mp4\"'
          ^)
        "
      ') DO @%%B
      FOR /F "delims=" %%C IN ('
        xidel -se "
          'mkvmerge -o \"%%~nA-FINAL.mkv\" '^|^|
          join^(
            file:list^(.^,false^(^)^,'%%~nA-*.mp4'^) ! `\"{.}\"`^,
            ' + '
          ^)
        "
      ') DO @%%C
    )