defmodule MPEG.TS.SCTE35 do @behaviour MPEG.TS.Unmarshaler @moduledoc """ SCTE-35 Digital Program Insertion Cueing Message. Handles parsing of SCTE-35 splice information tables used for ad insertion and program signaling in MPEG-TS streams. """ defmodule SpliceInsert do @behaviour MPEG.TS.Unmarshaler defstruct [ :event_id, :cancel_indicator, :out_of_network_indicator, :program_splice_flag, :duration_flag, :splice_immediate_flag, :event_id_compliance_flag, :splice_time, :break_duration, :unique_program_id, :avail_num, :avails_expected ] @impl true def unmarshal(<>, _start_unit) do cmd = %__MODULE__{ event_id: event_id, cancel_indicator: cancel_indicator } cmd = parse_event_cancel_section(cmd, rest) {:ok, cmd} end def unmarshal(_, _), do: {:error, :invalid_splice_insert} defp parse_event_cancel_section( cmd = %{cancel_indicator: 0}, << out_of_network_indicator::1, 1::1, break_duration_flag::1, splice_immediate_flag::1, event_id_compliance_flag::1, 0b111::3, rest::binary >> ) do {splice_time, rest} = parse_splice_time(splice_immediate_flag, rest) {break_duration, rest} = parse_break_duration(break_duration_flag, rest) <> = rest cmd |> put_in([Access.key!(:out_of_network_indicator)], out_of_network_indicator) |> put_in([Access.key!(:program_splice_flag)], 1) |> put_in([Access.key!(:duration_flag)], break_duration_flag) |> put_in([Access.key!(:splice_immediate_flag)], splice_immediate_flag) |> put_in([Access.key!(:event_id_compliance_flag)], event_id_compliance_flag) |> put_in([Access.key!(:splice_time)], splice_time) |> put_in([Access.key!(:break_duration)], break_duration) |> put_in([Access.key!(:unique_program_id)], unique_program_id) |> put_in([Access.key!(:avail_num)], avail_num) |> put_in([Access.key!(:avails_expected)], avails_expected) end defp parse_event_cancel_section(cmd, _), do: cmd defp parse_splice_time(0, <<1::1, 0b111111::6, pts_time::33, rest::binary>>), do: {%{pts: pts_time}, rest} defp parse_splice_time(0, <<0::1, 0b1111111::7, rest::binary>>), do: {nil, rest} defp parse_splice_time(1, rest), do: {nil, rest} defp parse_break_duration( 1, <> ) do {%{auto_return: auto_return, duration: duration}, rest} end defp parse_break_duration(0, rest), do: {nil, rest} defimpl MPEG.TS.Marshaler do def marshal(cmd = %MPEG.TS.SCTE35.SpliceInsert{}) do header = <> if cmd.cancel_indicator == 0 do info = <> splice_time = if cmd.program_splice_flag == 1 and cmd.splice_immediate_flag == 0, do: <<1::1, 0b111111::6, cmd.splice_time.pts::33>>, else: <<>> break_duration = if cmd.duration_flag == 1, do: <>, else: <<>> IO.iodata_to_binary([ header, info, splice_time, break_duration, <> ]) else header end end end end @type splice_command_type_t :: :splice_null | :splice_schedule | :splice_insert | :time_signal | :bandwidth_reservation | :private_command defstruct [ # SCTE-35 header fields :protocol_version, :encrypted_packet, :encryption_algorithm, :pts_adjustment, :cw_index, :tier, :splice_command_type, :splice_command, :splice_descriptors, :e_crc32 ] @type t :: %__MODULE__{ protocol_version: 0..255, encrypted_packet: pos_integer(), encryption_algorithm: 0..63, pts_adjustment: integer(), cw_index: 0..255, tier: 0..0xFFF, splice_command_type: splice_command_type_t(), splice_command: struct(), splice_descriptors: list(), e_crc32: binary() } @impl true def unmarshal(data, true) do unmarshal_table(data) end def unmarshal(_data, _is_unit_start) do {:error, :invalid_data} end @spec unmarshal_table(binary()) :: {:ok, t()} | {:error, :invalid_data} def unmarshal_table(<< protocol_version::8, encrypted_packet_flag::1, encryption_algorithm::6, pts_adjustment::33, cw_index::8, tier::12, splice_command_length::12, splice_command_type::8, splice_info_section::binary-size(splice_command_length), descriptor_loop_length::16, _descriptor_loop::binary-size(descriptor_loop_length), rest::binary >>) do with {:ok, command_type} <- parse_splice_command_type(splice_command_type), {:ok, command} <- parse_splice_command(command_type, splice_info_section) do e_crc32 = if encrypted_packet_flag == 1 do <> = rest e_crc32 else <<>> = rest end {:ok, %__MODULE__{ protocol_version: protocol_version, encrypted_packet: encrypted_packet_flag, encryption_algorithm: encryption_algorithm, pts_adjustment: pts_adjustment, cw_index: cw_index, tier: tier, splice_command_type: command_type, splice_command: command, # TODO: we're not parsing the descriptors! splice_descriptors: [], e_crc32: e_crc32 }} else {:error, reason} -> {:error, reason} end end def unmarshal_table(_), do: {:error, :invalid_data} @splice_type_to_id %{ 0x00 => :splice_null, 0x04 => :splice_schedule, 0x05 => :splice_insert, 0x06 => :time_signal, 0x07 => :bandwidth_reservation } defp parse_splice_command_type(type) do if id = @splice_type_to_id[type] do {:ok, id} else {:error, :unknown_splice_type} end end defp parse_splice_command(:splice_null, _data), do: {:ok, %{}} defp parse_splice_command(:splice_insert, data), do: SpliceInsert.unmarshal(data, true) defp parse_splice_command(_type, _data), do: {:error, :splice_command_not_implemented} defimpl MPEG.TS.Marshaler do @splice_id_to_type %{ :splice_null => 0x00, :splice_schedule => 0x04, :splice_insert => 0x05, :time_signal => 0x06, :bandwidth_reservation => 0x07 } def marshal(scte = %MPEG.TS.SCTE35{splice_descriptors: []}) do splice_command = MPEG.TS.Marshaler.marshal(scte.splice_command) splice_command_type = @splice_id_to_type[scte.splice_command_type] splice_command_size = IO.iodata_length(splice_command) descriptor_loop_length = 0 <> <> splice_command <> <> <> scte.e_crc32 end end end