|
| 1 | +defmodule Boombox.RTP do |
| 2 | + @moduledoc false |
| 3 | + import Membrane.ChildrenSpec |
| 4 | + |
| 5 | + require Membrane.Pad |
| 6 | + |
| 7 | + alias Boombox.Pipeline.Ready |
| 8 | + alias Membrane.RTP |
| 9 | + |
| 10 | + @required_opts [:port, :track_configs] |
| 11 | + @required_encoding_specific_params %{ |
| 12 | + AAC: [bitrate_mode: [require?: true], audio_specific_config: [require?: true]], |
| 13 | + OPUS: [], |
| 14 | + H264: [ppss: [require?: false], spss: [require?: false]], |
| 15 | + H265: [ppss: [require?: false], spss: [require?: false]] |
| 16 | + } |
| 17 | + |
| 18 | + @type parsed_encoding_specific_params :: |
| 19 | + %{bitrate_mode: RTP.AAC.Utils.mode(), audio_specific_config: binary()} |
| 20 | + | %{optional(:ppss) => [binary()], optional(:spss) => [binary()]} |
| 21 | + | %{ |
| 22 | + optional(:vpss) => [binary()], |
| 23 | + optional(:ppss) => [binary()], |
| 24 | + optional(:spss) => [binary()] |
| 25 | + } |
| 26 | + | %{} |
| 27 | + |
| 28 | + @type parsed_track_config :: %{ |
| 29 | + encoding_name: RTP.encoding_name(), |
| 30 | + encoding_specific_params: parsed_encoding_specific_params(), |
| 31 | + payload_type: RTP.payload_type(), |
| 32 | + clock_rate: RTP.clock_rate() |
| 33 | + } |
| 34 | + |
| 35 | + @type parsed_in_opts :: %{ |
| 36 | + port: :inet.port_number(), |
| 37 | + track_configs: %{audio: parsed_track_config(), video: parsed_track_config()} |
| 38 | + } |
| 39 | + |
| 40 | + @spec create_input(Boombox.in_rtp_opts()) :: Ready.t() |
| 41 | + def create_input(opts) do |
| 42 | + parsed_options = validate_and_parse_options(opts) |
| 43 | + payload_type_mapping = get_payload_type_mapping(parsed_options.track_configs) |
| 44 | + |
| 45 | + spec = |
| 46 | + child(:udp_source, %Membrane.UDP.Source{local_port_no: opts[:port]}) |
| 47 | + |> child(:rtp_demuxer, %Membrane.RTP.Demuxer{payload_type_mapping: payload_type_mapping}) |
| 48 | + |
| 49 | + track_builders = |
| 50 | + Map.new(parsed_options.track_configs, fn {media_type, track_config} -> |
| 51 | + {depayloader, parser} = |
| 52 | + case track_config.encoding_name do |
| 53 | + :H264 -> |
| 54 | + ppss = Map.get(track_config.encoding_specific_params, :ppss, []) |
| 55 | + spss = Map.get(track_config.encoding_specific_params, :spss, []) |
| 56 | + {Membrane.RTP.H264.Depayloader, %Membrane.H264.Parser{ppss: ppss, spss: spss}} |
| 57 | + |
| 58 | + :AAC -> |
| 59 | + audio_specific_config = track_config.encoding_specific_params.audio_specific_config |
| 60 | + bitrate_mode = track_config.encoding_specific_params.bitrate_mode |
| 61 | + |
| 62 | + {%Membrane.RTP.AAC.Depayloader{mode: bitrate_mode}, |
| 63 | + %Membrane.AAC.Parser{audio_specific_config: audio_specific_config}} |
| 64 | + |
| 65 | + :OPUS -> |
| 66 | + {Membrane.RTP.Opus.Depayloader, Membrane.Opus.Parser} |
| 67 | + |
| 68 | + :H265 -> |
| 69 | + vpss = Map.get(track_config.encoding_specific_params, :vpss, []) |
| 70 | + ppss = Map.get(track_config.encoding_specific_params, :ppss, []) |
| 71 | + spss = Map.get(track_config.encoding_specific_params, :spss, []) |
| 72 | + |
| 73 | + {Membrane.RTP.H265.Depayloader, |
| 74 | + %Membrane.H265.Parser{vpss: vpss, ppss: ppss, spss: spss}} |
| 75 | + end |
| 76 | + |
| 77 | + spec = |
| 78 | + get_child(:rtp_demuxer) |
| 79 | + |> via_out(:output, options: [stream_id: {:encoding_name, track_config.encoding_name}]) |
| 80 | + |> child({:jitter_buffer, track_config.encoding_name}, %Membrane.RTP.JitterBuffer{ |
| 81 | + clock_rate: track_config.clock_rate |
| 82 | + }) |
| 83 | + |> child({:rtp_depayloader, track_config.encoding_name}, depayloader) |
| 84 | + |> child({:rtp_in_parser, track_config.encoding_name}, parser) |
| 85 | + |
| 86 | + {media_type, spec} |
| 87 | + end) |
| 88 | + |
| 89 | + %Ready{spec_builder: spec, track_builders: track_builders} |
| 90 | + end |
| 91 | + |
| 92 | + @spec validate_and_parse_options(Boombox.in_rtp_opts()) :: parsed_in_opts() |
| 93 | + defp validate_and_parse_options(opts) do |
| 94 | + Enum.each(@required_opts, fn required_option -> |
| 95 | + unless Keyword.has_key?(opts, required_option) do |
| 96 | + raise "Required option #{inspect(required_option)} not present in passed RTP options" |
| 97 | + end |
| 98 | + end) |
| 99 | + |
| 100 | + if opts[:track_configs] == [] do |
| 101 | + raise "No RTP media configured" |
| 102 | + end |
| 103 | + |
| 104 | + parsed_track_configs = |
| 105 | + Map.new(opts[:track_configs], fn {media_type, track_config} -> |
| 106 | + {media_type, validate_and_parse_track_config!(track_config)} |
| 107 | + end) |
| 108 | + |
| 109 | + %{port: opts[:port], track_configs: parsed_track_configs} |
| 110 | + end |
| 111 | + |
| 112 | + @spec validate_and_parse_track_config!(Boombox.rtp_track_config()) :: parsed_track_config() |
| 113 | + defp validate_and_parse_track_config!(track_config) do |
| 114 | + {encoding_name, encoding_specific_params} = |
| 115 | + validate_and_parse_encoding!(track_config[:encoding]) |
| 116 | + |
| 117 | + track_config = Keyword.put(track_config, :encoding_name, encoding_name) |
| 118 | + |
| 119 | + %{payload_type: payload_type, clock_rate: clock_rate} = |
| 120 | + RTP.PayloadFormat.resolve(track_config) |
| 121 | + |
| 122 | + if payload_type == nil do |
| 123 | + raise "payload_type for encoding #{inspect(encoding_name)} not provided with no default value registered" |
| 124 | + end |
| 125 | + |
| 126 | + if clock_rate == nil do |
| 127 | + raise "clock_rate for encoding #{inspect(encoding_name)} and payload_type #{inspect(payload_type)} not provided with no default value registered" |
| 128 | + end |
| 129 | + |
| 130 | + %{ |
| 131 | + encoding_name: encoding_name, |
| 132 | + encoding_specific_params: encoding_specific_params, |
| 133 | + payload_type: payload_type, |
| 134 | + clock_rate: clock_rate |
| 135 | + } |
| 136 | + end |
| 137 | + |
| 138 | + @spec validate_and_parse_encoding!(RTP.encoding_name() | Boombox.rtp_encoding_specific_params()) :: |
| 139 | + {RTP.encoding_name(), %{}} | parsed_encoding_specific_params() |
| 140 | + defp validate_and_parse_encoding!(encoding) do |
| 141 | + case encoding do |
| 142 | + nil -> |
| 143 | + raise "Encoding name not provided" |
| 144 | + |
| 145 | + encoding when is_atom(encoding) -> |
| 146 | + validate_and_parse_encoding!({encoding, []}) |
| 147 | + |
| 148 | + {encoding, encoding_params} when is_atom(encoding) -> |
| 149 | + field_specs = Map.get(@required_encoding_specific_params, encoding, []) |
| 150 | + {:ok, encoding_params} = Bunch.Config.parse(encoding_params, field_specs) |
| 151 | + {encoding, encoding_params} |
| 152 | + end |
| 153 | + end |
| 154 | + |
| 155 | + @spec get_payload_type_mapping(%{audio: parsed_track_config(), video: parsed_track_config()}) :: |
| 156 | + RTP.PayloadFormat.payload_type_mapping() |
| 157 | + defp get_payload_type_mapping(track_configs) do |
| 158 | + Map.new(track_configs, fn {_media_type, track_config} -> |
| 159 | + {track_config.payload_type, |
| 160 | + %{encoding_name: track_config.encoding_name, clock_rate: track_config.clock_rate}} |
| 161 | + end) |
| 162 | + end |
| 163 | +end |
0 commit comments