lib/scte35.ex

defmodule Scte35 do
  def run(m3u8_file) do
    get_scte_string_from(m3u8_file)
    |> analyze_segmentation_descriptor
  end

  def get_scte_string_from(m3u8_file) do
    {:ok, text} = File.read(m3u8_file)

    try do
      text
      |> String.split("\n")
      |> Enum.filter(fn line -> line =~ "SCTE35=" end)
      |> hd
      |> String.split("SCTE35=")
      |> tl
      |> hd
      |> tap(&IO.puts("SCTE35 string: #{&1}\n"))
    rescue
      e ->
        IO.puts("Invalid SCTE35 string (function 1). " <> e.message)
        raise ArgumentError, message: "Invalid SCTE35 string (function 1)"
    end
  end

  def analyze_segmentation_descriptor(scte_string) do
    %{scte_string: scte_string}
    |> till_segmentation_event_id
    |> first_2_bytes
    |> through_component_count
    |> till_segment_num
  end

  def till_segmentation_event_id(map) do
    try do
      after_CUEI =
        map.scte_string
        |> Base.decode64!()
        # identifier = "CUEI"
        |> :binary.split(<<"CUEI">>)
        |> tl
        |> hd

      # pattern match by bytes
      <<segmentation_event_id::binary-size(4), rest::binary>> = after_CUEI

      IO.inspect(segmentation_event_id, label: "segmentation_event_id (decimals)")
      Map.put(map, :remaining_bytes, rest)
    rescue
      e ->
        IO.puts("Invalid SCTE35 string (function 2). " <> e.message)
        raise ArgumentError, message: "Invalid SCTE35 string (function 2)"
    end
  end

  def first_2_bytes(map) do
    IO.puts("\nStart of segmentation_event_cancel_indicator:")
    IO.inspect(map.remaining_bytes, label: "Remaining Bytes (decimals)")

    <<byte1::binary-size(1), byte2::binary-size(1), rest::binary>> = map.remaining_bytes

    {byte1_decimal, byte2_decimal} = {byte1 |> :binary.first(), byte2 |> :binary.first()}
    {byte1_binary, byte2_binary} = {byte1_decimal |> base2, byte2_decimal |> base2}

    IO.puts("\nByte 1: Decimal = #{byte1_decimal}. Binary = #{byte1_binary}")

    # pattern match by bits
    <<segmentation_event_cancel_indicator::1, byte1_reserved_dec::7>> = byte1
    byte1_reserved_hex = byte1_reserved_dec |> base2 |> binary_to_hex

    IO.puts(
      "segmentation_event_cancel_indicator (binary): #{segmentation_event_cancel_indicator}"
    )

    IO.puts("reserved (hex): #{byte1_reserved_hex}\n")

    if segmentation_event_cancel_indicator != 0 do
      IO.puts("segmentation_event_cancel_indicator wrong. No segmentation_upid or segment_num.")
      exit(:shutdown)
    end

    IO.puts("Byte 2: Decimal = #{byte2_decimal}. Binary = #{byte2_binary}")

    <<program_segmentation_flag::1, segmentation_duration_flag::1,
      delivery_not_restricted_flag::1, byte2_reserved_dec::5>> = byte2

    byte2_reserved_hex = byte2_reserved_dec |> base2 |> binary_to_hex

    IO.puts("program_segmentation_flag (binary): #{program_segmentation_flag}")
    IO.puts("segmentation_duration_flag (binary): #{segmentation_duration_flag}")
    IO.puts("delivery_not_restricted_flag (binary): #{delivery_not_restricted_flag}")

    map =
      if delivery_not_restricted_flag != 0 do
        IO.puts("reserved (hex): #{byte2_reserved_hex}")
        Map.put(map, :byte2_reserved, byte2_reserved_hex)
      else
        map
      end

    Map.merge(map, %{
      byte1: byte1_decimal,
      byte1_reserved: byte1_reserved_hex,
      byte2: byte2_decimal,
      segmentation_event_cancel_indicator: segmentation_event_cancel_indicator,
      program_segmentation_flag: program_segmentation_flag,
      segmentation_duration_flag: segmentation_duration_flag,
      delivery_not_restricted_flag: delivery_not_restricted_flag,
      remaining_bytes: rest
    })
  end

  def through_component_count(map) do
    IO.puts("")
    component_count = map.remaining_bytes |> :binary.first()

    rest =
      if map.program_segmentation_flag == 0 && component_count > 0 do
        IO.puts("component_count (decimal): #{component_count}\n")
        Map.put(map, :component_count, component_count)
        total_components_size = 6 * component_count

        <<_component_bytes::binary-size(total_components_size), rest2::binary>> =
          map.remaining_bytes

        rest2
      else
        map.remaining_bytes
      end

    Map.put(map, :remaining_bytes, rest)
  end

  def till_segment_num(map) do
    {segmentation_duration, bytes} =
      if map.segmentation_duration_flag == 1 do
        <<segmentation_duration_dec::binary-size(5), rest::binary>> = map.remaining_bytes
        segmentation_duration_hex = segmentation_duration_dec |> Base.encode16()

        IO.puts("segmentation_duration (hex): #{segmentation_duration_hex}")

        {segmentation_duration_hex, rest}
      else
        {"", map.remaining_bytes}
      end

    <<segmentation_upid_type::binary-size(1), segmentation_upid_length::binary-size(1),
      rest2::binary>> = bytes

    segmentation_upid_length_dec = segmentation_upid_length |> :binary.first()
    IO.puts("segmentation_upid_type (hex): #{segmentation_upid_type |> Base.encode16()}")
    IO.puts("segmentation_upid_length (decimal): #{segmentation_upid_length_dec}\n")

    IO.puts("Start of segmentation_upid:")
    IO.inspect(rest2, label: "Remaining Bytes (decimals)")
    map = Map.put(map, :remaining_bytes, rest2)

    <<segmentation_upid::binary-size(segmentation_upid_length_dec),
      _segmentation_type_id::binary-size(1), segment_num::binary-size(1), _rest3::binary>> = rest2

    segment_num_dec = segment_num |> :binary.first()
    IO.puts("\n--> segmentation_upid: #{segmentation_upid}")
    IO.puts("--> segment_num (decimal): #{segment_num_dec}\n_____________________________\n")

    Map.merge(map, %{
      segmentation_duration: segmentation_duration,
      segmentation_upid: segmentation_upid,
      segment_num: segment_num_dec
    })
  end

  @spec base2(integer) :: binary
  def base2(decimal) do
    Integer.to_string(decimal, 2) |> String.pad_leading(8, "0")
  end

  @spec binary_to_hex(binary) :: binary
  def binary_to_hex(bin) do
    bin
    |> Integer.parse(2)
    |> elem(0)
    |> Integer.to_string(16)
  end
end