lib/membrane_mp4/container/schema.ex

defmodule Membrane.MP4.Container.Schema do
  @moduledoc """
  MP4 structure schema used for parsing and serialization.

  Useful resources:
  - https://www.iso.org/standard/79110.html
  - https://www.iso.org/standard/61988.html
  - https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html
  - https://github.com/DicomJ/mpeg-isobase/tree/eb09f82ff6e160715dcb34b2bf473330c7695d3b
  """

  @full_box [
    version: :uint8,
    flags: :uint24
  ]

  @schema_def ftyp: [
                fields: [
                  major_brand: :str32,
                  major_brand_version: :uint32,
                  compatible_brands: {:list, :str32}
                ]
              ],
              moov: [
                mvhd: [
                  version: 0,
                  fields:
                    @full_box ++
                      [
                        creation_time: :uint32,
                        modification_time: :uint32,
                        timescale: :uint32,
                        duration: :uint32,
                        rate: :fp16d16,
                        volume: :fp8d8,
                        reserved: <<0::size(80)>>,
                        matrix_value_A: :fp16d16,
                        matrix_value_B: :fp16d16,
                        matrix_value_U: :fp2d30,
                        matrix_value_C: :fp16d16,
                        matrix_value_D: :fp16d16,
                        matrix_value_V: :fp2d30,
                        matrix_value_X: :fp16d16,
                        matrix_value_Y: :fp16d16,
                        matrix_value_W: :fp2d30,
                        quicktime_preview_time: :uint32,
                        quicktime_preview_duration: :uint32,
                        quicktime_poster_time: :uint32,
                        quicktime_selection_time: :uint32,
                        quicktime_selection_duration: :uint32,
                        quicktime_current_time: :uint32,
                        next_track_id: :uint32
                      ]
                ],
                trak: [
                  tkhd: [
                    version: 0,
                    fields:
                      @full_box ++
                        [
                          creation_time: :uint32,
                          modification_time: :uint32,
                          track_id: :uint32,
                          reserved: <<0::32>>,
                          duration: :uint32,
                          reserved: <<0::64>>,
                          layer: :int16,
                          alternate_group: :int16,
                          volume: :fp8d8,
                          reserved: <<0::16>>,
                          matrix_value_A: :fp16d16,
                          matrix_value_B: :fp16d16,
                          matrix_value_U: :fp2d30,
                          matrix_value_C: :fp16d16,
                          matrix_value_D: :fp16d16,
                          matrix_value_V: :fp2d30,
                          matrix_value_X: :fp16d16,
                          matrix_value_Y: :fp16d16,
                          matrix_value_W: :fp2d30,
                          width: :fp16d16,
                          height: :fp16d16
                        ]
                  ],
                  mdia: [
                    mdhd: [
                      version: 0,
                      fields:
                        @full_box ++
                          [
                            creation_time: :uint32,
                            modification_time: :uint32,
                            timescale: :uint32,
                            duration: :uint32,
                            reserved: <<0::1>>,
                            language: :uint15,
                            reserved: <<0::16>>
                          ]
                    ],
                    hdlr: [
                      version: 0,
                      fields:
                        @full_box ++
                          [
                            reserved: <<0::32>>,
                            handler_type: :str32,
                            reserved: <<0::96>>,
                            name: :str
                          ]
                    ],
                    minf: [
                      vmhd: [
                        version: 0,
                        fields:
                          @full_box ++
                            [
                              graphics_mode: :uint16,
                              opcolor: :uint48
                            ]
                      ],
                      smhd: [
                        version: 0,
                        fields:
                          @full_box ++
                            [
                              balance: :fp8d8,
                              reserved: <<0::16>>
                            ]
                      ],
                      dinf: [
                        dref: [
                          version: 0,
                          fields:
                            @full_box ++
                              [
                                entry_count: :uint32
                              ],
                          url: [
                            version: 0,
                            fields: @full_box
                          ]
                        ]
                      ],
                      stbl: [
                        stsd: [
                          version: 0,
                          fields:
                            @full_box ++
                              [
                                entry_count: :uint32
                              ],
                          avc1: [
                            version: 0,
                            fields:
                              @full_box ++
                                [
                                  num_of_entries: :uint32,
                                  reserved: <<0::128>>,
                                  width: :uint16,
                                  height: :uint16,
                                  horizresolution: :fp16d16,
                                  vertresolution: :fp16d16,
                                  reserved: <<0::32>>,
                                  frame_count: :uint16,
                                  compressor_name: :str256,
                                  depth: :uint16,
                                  reserved: <<-1::16-integer>>
                                ],
                            avcC: [
                              black_box?: true
                            ],
                            pasp: [
                              fields: [
                                h_spacing: :uint32,
                                v_spacing: :uint32
                              ]
                            ]
                          ],
                          mp4a: [
                            fields: [
                              reserved: <<0::6*8>>,
                              data_reference_index: :uint16,
                              encoding_version: :uint16,
                              encoding_revision: :uint16,
                              encoding_vendor: :uint32,
                              channel_count: :uint16,
                              sample_size: :uint16,
                              compression_id: :uint16,
                              packet_size: :uint16,
                              sample_rate: :fp16d16
                            ],
                            esds: [
                              version: 0,
                              fields:
                                @full_box ++
                                  [
                                    elementary_stream_descriptor: :bin
                                  ]
                            ]
                          ],
                          Opus: [
                            version: 0,
                            fields: [
                              reserved: <<0::6*8>>,
                              data_reference_index: :uint16,
                              reserved: <<0::2*32>>,
                              channel_count: :uint16,
                              sample_size: :uint16,
                              # pre_defined
                              reserved: <<0::16>>,
                              reserved: <<0::16>>,
                              sample_rate: :uint32
                            ],
                            dOps: [
                              version: 0,
                              fields: [
                                version: :uint8,
                                output_channel_count: :uint8,
                                pre_skip: :uint16,
                                input_sample_rate: :uint32,
                                output_gain: :int16,
                                channel_mapping_family: :uint8
                              ]
                            ]
                          ]
                        ],
                        stts: [
                          version: 0,
                          fields:
                            @full_box ++
                              [
                                entry_count: :uint32,
                                entry_list:
                                  {:list,
                                   [
                                     sample_count: :uint32,
                                     sample_delta: :uint32
                                   ]}
                              ]
                        ],
                        stss: [
                          version: 0,
                          fields:
                            @full_box ++
                              [
                                entry_count: :uint32,
                                entry_list:
                                  {:list,
                                   [
                                     sample_number: :uint32
                                   ]}
                              ]
                        ],
                        stsc: [
                          version: 0,
                          fields:
                            @full_box ++
                              [
                                entry_count: :uint32,
                                entry_list:
                                  {:list,
                                   [
                                     first_chunk: :uint32,
                                     samples_per_chunk: :uint32,
                                     sample_description_index: :uint32
                                   ]}
                              ]
                        ],
                        stsz: [
                          version: 0,
                          fields:
                            @full_box ++
                              [
                                sample_size: :uint32,
                                sample_count: :uint32,
                                entry_list:
                                  {:list,
                                   [
                                     entry_size: :uint32
                                   ]}
                              ]
                        ],
                        stco: [
                          version: 0,
                          fields:
                            @full_box ++
                              [
                                entry_count: :uint32,
                                entry_list:
                                  {:list,
                                   [
                                     chunk_offset: :uint32
                                   ]}
                              ]
                        ]
                      ]
                    ]
                  ]
                ],
                mvex: [
                  trex: [
                    version: 0,
                    fields:
                      @full_box ++
                        [
                          track_id: :uint32,
                          default_sample_description_index: :uint32,
                          default_sample_duration: :uint32,
                          default_sample_size: :uint32,
                          default_sample_flags: :uint32
                        ]
                  ]
                ]
              ],
              styp: [
                fields: [
                  major_brand: :str32,
                  major_brand_version: :uint32,
                  compatible_brands: {:list, :str32}
                ]
              ],
              sidx: [
                version: 1,
                fields:
                  @full_box ++
                    [
                      reference_id: :uint32,
                      timescale: :uint32,
                      earliest_presentation_time: :uint64,
                      first_offset: :uint64,
                      reserved: <<0::16-integer>>,
                      reference_count: :uint16,
                      # TODO: make a list once list length is supported
                      # reference_list: [
                      #   [
                      #     reference_type: :bin1,
                      #     referenced_size: :uint31,
                      #     subsegment_duration: :uint32,
                      #     starts_with_sap: :bin1,
                      #     sap_type: :uint3,
                      #     sap_delta_time: :uint28
                      #   ],
                      #   length: :reference_count
                      # ]
                      reference_type: :bin1,
                      # from the beginning of moof to the end
                      referenced_size: :uint31,
                      subsegment_duration: :uint32,
                      starts_with_sap: :bin1,
                      sap_type: :uint3,
                      sap_delta_time: :uint28
                    ]
              ],
              moof: [
                mfhd: [
                  version: 0,
                  fields:
                    @full_box ++
                      [
                        sequence_number: :uint32
                      ]
                ],
                traf: [
                  tfhd: [
                    version: 0,
                    fields:
                      @full_box ++
                        [
                          track_id: :uint32,
                          default_sample_duration: :uint32,
                          default_sample_size: :uint32,
                          default_sample_flags: :uint32
                        ]
                  ],
                  tfdt: [
                    version: 1,
                    fields:
                      @full_box ++
                        [
                          base_media_decode_time: :uint64
                        ]
                  ],
                  trun: [
                    version: 0,
                    fields:
                      @full_box ++
                        [
                          sample_count: :uint32,
                          data_offset: :int32,
                          samples:
                            {:list,
                             [
                               sample_duration: :uint32,
                               sample_size: :uint32,
                               sample_flags: :bin32
                               # TODO: handle sample offset, include basing on flags once conditional fields are supported
                               # sample_offset: :uint32
                             ]}
                        ]
                  ]
                ]
              ],
              mdat: [
                black_box?: true
              ]

  @type schema_def_primitive_t :: atom

  @type schema_def_field_t ::
          {:reserved, bitstring}
          | {field_name :: atom,
             schema_def_primitive_t
             | {:list, schema_def_primitive_t | [schema_def_field_t]}
             | [schema_def_field_t]}

  @type schema_def_box_t ::
          {box_name :: atom,
           [{:black_box?, true}]
           | [
               {:version, non_neg_integer}
               | {:fields, [schema_def_field_t]}
               | schema_def_box_t
             ]}

  @typedoc """
  Type describing the schema definition, that is hardcoded in this module.

  It may be useful for improving the schema definition. The actual schema that
  should be operated on, or, in other words, the parsed schema definition is
  specified by `t:#{inspect(__MODULE__)}.t/0`.

  The schema definition differs from the final schema in the following ways:
    - primitives along with their parameters are specified as atoms, for example
    `:int32` instead of `{:int, 32}`
    - child boxes are nested within their parents directly, instead of residing
    under `:children` key.

  The schema definition is the following:
  ```
  #{inspect(@schema_def, pretty: true)}
  ```
  """
  @type schema_def_t :: [schema_def_box_t]

  @typedoc """
  For fields, the following primitive types are supported:
  - `{:int, bit_size}` - a signed integer
  - `{:uint, bit_size}` - an unsigned integer
  - `:bin` - a binary lasting till the end of a box
  - `{:bin, bit_size}` - a binary of given size
  - `:str` - a string terminated with a null byte
  - `{:str, bit_size}` - a string of given size
  - `{:fp, integer_part_bit_size, fractional_part_bit_size}` - a fixed point number
  """
  @type primitive_t ::
          {:int, bit_size :: non_neg_integer}
          | {:uint, bit_size :: non_neg_integer}
          | :bin
          | {:bin, bit_size :: non_neg_integer}
          | :str
          | {:str, bit_size :: non_neg_integer}
          | {:fp, int_bit_size :: non_neg_integer, frac_bit_size :: non_neg_integer}

  @typedoc """
  A box field type.

  It may contain a primitive, a list or nested fields. Lists last till the end of a box.
  """
  @type field_t ::
          {:reserved, bitstring}
          | {field_name :: atom, primitive_t | {:list, any} | [field_t]}

  @typedoc """
  The schema of MP4 structure.

  An MP4 file consists of boxes, that all have the same header and different internal
  structures. Boxes can be nested with one another.

  Each box has at most 4-letter name and may have the following parameters:
  - `black_box?` - if true, the box content is unspecified and is treated as an opaque
  binary. Defaults to false.
  - `version` - the box version. Versions usually differ by the sizes of particular fields.
  - `fields` - a list of key-value parameters
  - `children` - the nested boxes
  """
  @type t :: %{
          (box_name :: atom) =>
            %{black_box?: true}
            | %{
                black_box?: false,
                version: non_neg_integer,
                fields: [field_t],
                children: map
              }
        }

  @schema __MODULE__.Parser.parse(@schema_def)

  @doc """
  Returns `t:#{inspect(__MODULE__)}.t/0`
  """
  @spec schema() :: t
  def schema(), do: @schema
end