diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index 29abb4a..2180c30 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -870,7 +870,6 @@ defmodule ExWebRTC.PeerConnection do @impl true def handle_call({:add_track, %MediaStreamTrack{kind: kind} = track}, _from, state) do # we ignore the condition that sender has never been used to send - {ssrc, rtx_ssrc} = generate_ssrcs(state) {transceivers, sender} = state.transceivers @@ -878,10 +877,12 @@ defmodule ExWebRTC.PeerConnection do |> Enum.find(fn {tr, _idx} -> RTPTransceiver.can_add_track?(tr, kind) end) |> case do {tr, idx} -> - tr = RTPTransceiver.add_track(tr, track, ssrc, rtx_ssrc) + tr = RTPTransceiver.add_track(tr, track) {List.replace_at(state.transceivers, idx, tr), tr.sender} nil -> + {ssrc, rtx_ssrc} = generate_ssrcs(state) + options = [ direction: :sendrecv, added_by_add_track: true, @@ -910,8 +911,7 @@ defmodule ExWebRTC.PeerConnection do {:reply, {:error, :invalid_track_type}, state} {tr, idx} when tr.direction in [:sendrecv, :sendonly] -> - {ssrc, rtx_ssrc} = generate_ssrcs(state) - tr = RTPTransceiver.replace_track(tr, track, ssrc, rtx_ssrc) + tr = RTPTransceiver.replace_track(tr, track) transceivers = List.replace_at(state.transceivers, idx, tr) state = %{state | transceivers: transceivers} {:reply, :ok, state} @@ -1649,7 +1649,13 @@ defmodule ExWebRTC.PeerConnection do transceivers = sdp.media |> Enum.reject(&SDPUtils.data_channel?/1) - |> process_mlines_remote(state.transceivers, type, state.config, state.owner) + |> process_mlines_remote( + state.transceivers, + type, + Map.keys(state.demuxer.ssrc_to_mid), + state.config, + state.owner + ) # infer our role from the remote role dtls_role = if dtls_role in [:actpass, :passive], do: :active, else: :passive @@ -1835,25 +1841,40 @@ defmodule ExWebRTC.PeerConnection do end # See W3C WebRTC 4.4.1.5-4.7.10.2 - defp process_mlines_remote(mlines, transceivers, sdp_type, config, owner) do + defp process_mlines_remote(mlines, transceivers, sdp_type, demuxer_ssrcs, config, owner) do mlines_idx = Enum.with_index(mlines) - do_process_mlines_remote(mlines_idx, transceivers, sdp_type, config, owner) + do_process_mlines_remote(mlines_idx, transceivers, sdp_type, demuxer_ssrcs, config, owner) end - defp do_process_mlines_remote([], transceivers, _sdp_type, _config, _owner), do: transceivers + defp do_process_mlines_remote([], transceivers, _sdp_type, _demuxer_ssrcs, _config, _owner), + do: transceivers - defp do_process_mlines_remote([{mline, idx} | mlines], transceivers, sdp_type, config, owner) do + defp do_process_mlines_remote( + [{mline, idx} | mlines], + transceivers, + sdp_type, + demuxer_ssrcs, + config, + owner + ) do direction = if SDPUtils.rejected?(mline), do: :inactive, else: SDPUtils.get_media_direction(mline) |> reverse_direction() + rtp_sender_ssrcs = Enum.map(transceivers, & &1.sender.ssrc) + ssrcs = MapSet.new(demuxer_ssrcs ++ rtp_sender_ssrcs) + + ssrc = do_generate_ssrc(ssrcs) + ssrcs = MapSet.put(ssrcs, ssrc) + rtx_ssrc = do_generate_ssrc(ssrcs) + # Note: in theory we should update transceiver codecs # after processing remote track but this shouldn't have any impact {idx, tr} = case find_transceiver_from_remote(transceivers, mline) do {idx, tr} -> {idx, RTPTransceiver.update(tr, mline, config)} - nil -> {nil, RTPTransceiver.from_mline(mline, idx, config)} + nil -> {nil, RTPTransceiver.from_mline(mline, idx, ssrc, rtx_ssrc, config)} end tr = process_remote_track(tr, direction, owner) @@ -1867,11 +1888,11 @@ defmodule ExWebRTC.PeerConnection do case idx do nil -> transceivers = transceivers ++ [tr] - do_process_mlines_remote(mlines, transceivers, sdp_type, config, owner) + do_process_mlines_remote(mlines, transceivers, sdp_type, demuxer_ssrcs, config, owner) idx -> transceivers = List.replace_at(transceivers, idx, tr) - do_process_mlines_remote(mlines, transceivers, sdp_type, config, owner) + do_process_mlines_remote(mlines, transceivers, sdp_type, demuxer_ssrcs, config, owner) end end @@ -2186,6 +2207,8 @@ defmodule ExWebRTC.PeerConnection do # this is practically impossible so it's easier to raise # than to propagate the error up to the user + defp do_generate_ssrc(ssrcs, max_attempts \\ 200) + defp do_generate_ssrc(_ssrcs, 0), do: raise("Couldn't find free SSRC") defp do_generate_ssrc(ssrcs, max_attempts) do diff --git a/lib/ex_webrtc/rtp_sender.ex b/lib/ex_webrtc/rtp_sender.ex index 62ccec2..fb9ea6d 100644 --- a/lib/ex_webrtc/rtp_sender.ex +++ b/lib/ex_webrtc/rtp_sender.ex @@ -21,8 +21,11 @@ defmodule ExWebRTC.RTPSender do mid: String.t() | nil, pt: non_neg_integer() | nil, rtx_pt: non_neg_integer() | nil, - ssrc: non_neg_integer() | nil, - rtx_ssrc: non_neg_integer() | nil, + # ssrc and rtx_ssrc are always present, even if there is no track + # or transceiver direction is recvonly. + # We preallocate them so they can be included in SDP when needed. + ssrc: non_neg_integer(), + rtx_ssrc: non_neg_integer(), packets_sent: non_neg_integer(), bytes_sent: non_neg_integer(), retransmitted_packets_sent: non_neg_integer(), @@ -68,8 +71,8 @@ defmodule ExWebRTC.RTPSender do RTPCodecParameters.t() | nil, [Extmap.t()], String.t() | nil, - non_neg_integer() | nil, - non_neg_integer() | nil, + non_neg_integer(), + non_neg_integer(), [atom()] ) :: sender() def new(track, codec, rtx_codec, rtp_hdr_exts, mid, ssrc, rtx_ssrc, features) do @@ -131,6 +134,81 @@ defmodule ExWebRTC.RTPSender do } end + @spec get_mline_attrs(sender()) :: [ExSDP.Attribute.t()] + def get_mline_attrs(sender) do + # Don't include track id. See RFC 8829 sec. 5.2.1 + msid_attrs = + case sender.track do + nil -> + # In theory, we should do this "for each MediaStream that was associated with the transceiver" + # but web browsers (chrome, ff), include MSID even when there aren't any MediaStreams + [ExSDP.Attribute.MSID.new("-", nil)] + + %MediaStreamTrack{streams: streams} -> + case Enum.map(streams, &ExSDP.Attribute.MSID.new(&1, nil)) do + [] -> [ExSDP.Attribute.MSID.new("-", nil)] + other -> other + end + end + + ssrc_attrs = + get_ssrc_attrs(sender.pt, sender.rtx_pt, sender.ssrc, sender.rtx_ssrc, sender.track) + + msid_attrs ++ ssrc_attrs + end + + # we didn't manage to negotiate any codec + defp get_ssrc_attrs(nil, _rtx_pt, _ssrc, _rtx_ssrc, _track) do + [] + end + + # we have a codec but not rtx + defp get_ssrc_attrs(_pt, nil, ssrc, _rtx_ssrc, track) do + streams = (track && track.streams) || [] + + case streams do + [] -> + [%ExSDP.Attribute.SSRC{id: ssrc, attribute: "msid", value: "-"}] + + streams -> + Enum.map(streams, fn stream -> + %ExSDP.Attribute.SSRC{id: ssrc, attribute: "msid", value: stream} + end) + end + end + + # we have both codec and rtx + defp get_ssrc_attrs(_pt, _rtx_pt, ssrc, rtx_ssrc, track) do + streams = (track && track.streams) || [] + + case streams do + [] -> + [ + %ExSDP.Attribute.SSRCGroup{semantics: "FID", ssrcs: [ssrc, rtx_ssrc]}, + %ExSDP.Attribute.SSRC{id: ssrc, attribute: "msid", value: "-"}, + %ExSDP.Attribute.SSRC{id: rtx_ssrc, attribute: "msid", value: "-"} + ] + + streams -> + {ssrc_attrs, rtx_ssrc_attrs} = + Enum.reduce(streams, {[], []}, fn stream, {ssrc_attrs, rtx_ssrc_attrs} -> + ssrc_attr = [%ExSDP.Attribute.SSRC{id: ssrc, attribute: "msid", value: stream}] + ssrc_attrs = [ssrc_attr | ssrc_attrs] + + rtx_ssrc_attr = [ + %ExSDP.Attribute.SSRC{id: rtx_ssrc, attribute: "msid", value: stream} + ] + + rtx_ssrc_attrs = [rtx_ssrc_attr | rtx_ssrc_attrs] + + {ssrc_attrs, rtx_ssrc_attrs} + end) + + fid = %ExSDP.Attribute.SSRCGroup{semantics: "FID", ssrcs: [ssrc, rtx_ssrc]} + [fid | Enum.reverse(ssrc_attrs) ++ Enum.reverse(rtx_ssrc_attrs)] + end + end + @doc false @spec send_packet(sender(), ExRTP.Packet.t(), boolean()) :: {binary(), sender()} def send_packet(sender, packet, rtx?) do diff --git a/lib/ex_webrtc/rtp_transceiver.ex b/lib/ex_webrtc/rtp_transceiver.ex index 56b99cb..c9a8cea 100644 --- a/lib/ex_webrtc/rtp_transceiver.ex +++ b/lib/ex_webrtc/rtp_transceiver.ex @@ -174,8 +174,14 @@ defmodule ExWebRTC.RTPTransceiver do end @doc false - @spec from_mline(ExSDP.Media.t(), non_neg_integer(), Configuration.t()) :: transceiver() - def from_mline(mline, mline_idx, config) do + @spec from_mline( + ExSDP.Media.t(), + non_neg_integer(), + non_neg_integer(), + non_neg_integer(), + Configuration.t() + ) :: transceiver() + def from_mline(mline, mline_idx, ssrc, rtx_ssrc, config) do header_extensions = Configuration.intersect_extensions(config, mline) codecs = Configuration.intersect_codecs(config, mline) @@ -205,7 +211,16 @@ defmodule ExWebRTC.RTPTransceiver do receiver = RTPReceiver.new(track, codec, header_extensions, config.features) sender = - RTPSender.new(nil, codec, codec_rtx, header_extensions, mid, nil, nil, config.features) + RTPSender.new( + nil, + codec, + codec_rtx, + header_extensions, + mid, + ssrc, + rtx_ssrc, + config.features + ) %{ id: id, @@ -281,10 +296,9 @@ defmodule ExWebRTC.RTPTransceiver do end @doc false - @spec add_track(transceiver(), MediaStreamTrack.t(), non_neg_integer(), non_neg_integer()) :: - transceiver() - def add_track(transceiver, track, ssrc, rtx_ssrc) do - sender = %{transceiver.sender | track: track, ssrc: ssrc, rtx_ssrc: rtx_ssrc} + @spec add_track(transceiver(), MediaStreamTrack.t()) :: transceiver() + def add_track(transceiver, track) do + sender = %{transceiver.sender | track: track} direction = case transceiver.direction do @@ -297,11 +311,9 @@ defmodule ExWebRTC.RTPTransceiver do end @doc false - @spec replace_track(transceiver(), MediaStreamTrack.t(), non_neg_integer(), non_neg_integer()) :: - transceiver() - def replace_track(transceiver, track, ssrc, rtx_ssrc) do - ssrc = transceiver.sender.ssrc || ssrc - sender = %{transceiver.sender | track: track, ssrc: ssrc, rtx_ssrc: rtx_ssrc} + @spec replace_track(transceiver(), MediaStreamTrack.t()) :: transceiver() + def replace_track(transceiver, track) do + sender = %{transceiver.sender | track: track} %{transceiver | sender: sender} end @@ -526,23 +538,13 @@ defmodule ExWebRTC.RTPTransceiver do [rtp_mapping, codec.sdp_fmtp_line, codec.rtcp_fbs] end) - msids = - case transceiver.sender.track do - nil -> - [] - - %MediaStreamTrack{id: id, streams: streams} -> - case Enum.map(streams, &ExSDP.Attribute.MSID.new(&1, id)) do - [] -> [ExSDP.Attribute.MSID.new("-", id)] - other -> other - end - end + direction = Keyword.get(opts, :direction, transceiver.direction) attributes = if(Keyword.get(opts, :rtcp, false), do: [{"rtcp", "9 IN IP4 0.0.0.0"}], else: []) ++ Keyword.get(opts, :simulcast, []) ++ [ - Keyword.get(opts, :direction, transceiver.direction), + direction, {:mid, transceiver.mid}, {:ice_ufrag, Keyword.fetch!(opts, :ice_ufrag)}, {:ice_pwd, Keyword.fetch!(opts, :ice_pwd)}, @@ -550,7 +552,15 @@ defmodule ExWebRTC.RTPTransceiver do {:fingerprint, Keyword.fetch!(opts, :fingerprint)}, {:setup, Keyword.fetch!(opts, :setup)}, :rtcp_mux - ] ++ transceiver.header_extensions ++ msids + ] ++ transceiver.header_extensions + + # add sender attrs only if we send + sender_attrs = + if direction in [:sendonly, :sendrecv] do + RTPSender.get_mline_attrs(transceiver.sender) + else + [] + end %ExSDP.Media{ ExSDP.Media.new(transceiver.kind, 9, "UDP/TLS/RTP/SAVPF", pt) @@ -558,7 +568,7 @@ defmodule ExWebRTC.RTPTransceiver do # the default value "IN IP4 0.0.0.0" (as there are no candidates yet) connection_data: [%ExSDP.ConnectionData{address: {0, 0, 0, 0}}] } - |> ExSDP.add_attributes(attributes ++ media_formats) + |> ExSDP.add_attributes(attributes ++ media_formats ++ sender_attrs) end # RFC 3264 (6.1) + RFC 8829 (5.3.1)