Skip to content
Mattias Wadman edited this page Oct 23, 2023 · 11 revisions

Show edit lists and stts sums per track and also scale to seconds

#!/usr/bin/env fq -o decode_samples=false -d mp4 -f

# TODO: esds, make fancy printer? shared?
# TODO: handle -1 media_time
# TODO: fragmented mp4

# root
#   moov
#     mvhd (movie header)
#     trak (track)
#       mdia
#         mdhd (media header)
#         hdlr (handler?)
#         minf
#           stbl
#             stsd (sample description)
#       elst (edit list)

( first(.boxes[] | select(.type == "moov")?)
| first(.boxes[] | select(.type == "mvhd")?) as $mvhd
| { duration: $mvhd.duration,
    time_scale: $mvhd.time_scale,
    duration_s: ($mvhd.duration / $mvhd.time_scale),
    tracks:
      [ .boxes[]
      | select(.type == "trak")
      | first(.. | select(.type == "tkhd")?) as $tkhd
      | first(.. | select(.type == "mdhd")?) as $mdhd
      | first(.. | select(.type == "hdlr")?) as $hdlr
      | first(.. | select(.type == "stsd")?) as $stsd
      | (first(.. | select(.type == "elst")?) // null) as $elst
      | first(.. | select(.type == "stts")?) as $stts
      | ([$stts.entries[] | .count * .delta] | add) as $stts_sum
      | { component_type: $hdlr.component_subtype,
          duration: $tkhd.duration,
          duration_s: ($tkhd.duration / $mvhd.time_scale),
          media_duration: $mdhd.duration,
          media_duration_s: ($mdhd.duration / $mdhd.time_scale),
          # the sample descriptors are handled as boxes by the mp4 decoder
          data_format: $stsd.boxes[0].type,
          media_scale: $mdhd.time_scale,
          edit_list:
            ( if $elst then
                [ $elst.entries[]
                | { track_duration: .segment_duration,
                    media_time: .media_time,
                    track_duration_s: (.segment_duration / $mvhd.time_scale),
                    media_time_s: (.media_time / $mdhd.time_scale)
                  }
                ]
              else null
              end
            ),
          stts:
            { sum: $stts_sum,
              sum_s: ($stts_sum / $mdhd.time_scale)
            }
        }
      ]
  }
)

./mp4time.jq file.mp4 Will output something like

{
  "duration": 880128,
  "duration_s": 880.128,
  "time_scale": 1000,
  "tracks": [
    {
      "component_type": "vide",
      "data_format": "avc1",
      "duration": 880113,
      "duration_s": 880.113,
      "edit_list": [
        {
          "media_time": 6006,
          "media_time_s": 0.06673333333333334,
          "track_duration": 880113,
          "track_duration_s": 880.113
        }
      ],
      "media_duration": 79210131,
      "media_duration_s": 880.1125666666667,
      "media_scale": 90000,
      "stts": {
        "sum": 79210131,
        "sum_s": 880.1125666666667
      }
    },
    {
      "component_type": "soun",
      "data_format": "mp4a",
      "duration": 880128,
      "duration_s": 880.128,
      "edit_list": [
        {
          "media_time": 1024,
          "media_time_s": 0.021333333333333333,
          "track_duration": 880128,
          "track_duration_s": 880.128
        }
      ],
      "media_duration": 42246144,
      "media_duration_s": 880.128,
      "media_scale": 48000,
      "stts": {
        "sum": 42247168,
        "sum_s": 880.1493333333333
      }
    }
  ]
}

Summary of mp4 tracks for list of mp4 files

$ fq -n -d mp4 -o decode_samples=false '[inputs | {(input_filename): [grep_by(.type=="trak") | {s: (grep_by(.type=="mdhd") | .duration / .time_scale), comp: (grep_by(.type=="hdlr").component_subtype)}]}] | add' *.mp4
{
  "a.mp4": [
    {
      "comp": "soun",
      "s": 2.1
    },
    {
      "comp": "vide",
      "s": 3.1
    }
  ],
...

Manual decode samples for a track assuming it only has one chunk as samples

nth(0; .. | select(.type=="trak")?) as $trak | first($trak | .. | select(.type=="stco")?).entries[0] as $stco_off | first($trak | .. | select(.type=="stsz")?) as $stsz | (foreach (if $stsz.sample_size != 0 then (range($stsz.entry_count) | $stsz.sample_size) else $stsz.entries[] end) as $sz ([$stco_off,$stco_off]; [.[1], .[1]+$sz]; .)) as $r | tobytesrange[$r[0]:$r[1]] | aac_frame | select(._error)

Debug track drift:

# checks drift for second track in file (nth(1;...))
# assumes constant 1024 samples per frames (aac) and sample rate 44100
fq -r -o decode_samples=false 'nth(1; grep_by(.type == "stts")) | [foreach (.entries[] | range(.count) as $_ | .delta) as $n (0; .+($n-1024);.)] | . as $d | range(length) | "\((.*1024)/44100): \($d[.]/44100)"' file

PTS from stts/ctts:

$ fq -o decode_samples=false 'limit(10; (grep_by(.type=="ctts") | [.entries[] | range(.sample_count) as $_ | .sample_offset]) as $ctts | (grep_by(.type=="stts") | [foreach (.entries[] | range(.count) as $_ | .delta) as $i (0;.+$i;.)]) as $stts | range($stts | length) as $i | $stts[$i]+$ctts[$i] - ($stts[0]+$ctts[0]) )' file.mp4

fmp4 duration:

grep_by(.type=="mdhd").time_scale as $scale | [grep_by(.type=="moof") | grep_by(.type=="tfhd").default_sample_duration as $d | grep_by(.type=="trun").samples[] | .sample_duration // $d] | add / $scale

find mp4s with no ctts box but has a sps with frame reorder > 0

fq -o decode_samples=false '((first(grep_by(.type=="ctts")) | true) // false) as $has_ctts | (first(grep_by(has("max_num_reorder_frames")).max_num_reorder_frames) // null) as $reorder | if $has_ctts == false and $reorder > 0 then {filename: input_filename, $has_ctts, $reorder} else empty end' *.mp4
Clone this wiki locally