Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ssrc attributes to the SDP #185

Merged
merged 3 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 40 additions & 18 deletions lib/ex_webrtc/peer_connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -870,18 +870,19 @@ 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
|> Enum.with_index()
|> 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,
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1803,7 +1809,7 @@ defmodule ExWebRTC.PeerConnection do
end
end

# See W3C WebRTC 4.4.1.5-4.7.10.1
# See W3C WebRTC 4.4.1.4-4.7.10.1
defp process_mlines_local(_mlines, transceivers, :offer, _owner), do: transceivers

defp process_mlines_local([], transceivers, :answer, _owner), do: transceivers
Expand Down Expand Up @@ -1834,26 +1840,36 @@ defmodule ExWebRTC.PeerConnection do
process_mlines_local(mlines, transceivers, :answer, owner)
end

# See W3C WebRTC 4.4.1.5-4.7.10.2
defp process_mlines_remote(mlines, transceivers, sdp_type, config, owner) do
# See W3C WebRTC 4.4.1.4-4.7.10.2
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()

{ssrc, rtx_ssrc} = generate_ssrcs(transceivers, demuxer_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)
Expand All @@ -1867,11 +1883,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

Expand Down Expand Up @@ -2177,15 +2193,21 @@ defmodule ExWebRTC.PeerConnection do
end

defp generate_ssrcs(state) do
rtp_sender_ssrcs = Enum.map(state.transceivers, & &1.sender.ssrc)
ssrcs = MapSet.new(Map.keys(state.demuxer.ssrc_to_mid) ++ rtp_sender_ssrcs)
ssrc = do_generate_ssrc(ssrcs, 200)
rtx_ssrc = do_generate_ssrc(MapSet.put(ssrcs, ssrc), 200)
generate_ssrcs(state.transceivers, Map.keys(state.demuxer.ssrc_to_mid))
end

defp generate_ssrcs(transceivers, demuxer_ssrcs) do
rtp_sender_ssrcs = Enum.map(transceivers, & &1.sender.ssrc)
ssrcs = MapSet.new(demuxer_ssrcs ++ rtp_sender_ssrcs)
ssrc = do_generate_ssrc(ssrcs)
rtx_ssrc = do_generate_ssrc(MapSet.put(ssrcs, ssrc))
{ssrc, rtx_ssrc}
end

# 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
Expand Down
88 changes: 84 additions & 4 deletions lib/ex_webrtc/rtp_sender.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -131,6 +134,83 @@ 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
%MediaStreamTrack{streams: streams} when streams != [] ->
Enum.map(streams, &ExSDP.Attribute.MSID.new(&1, nil))

_other ->
# 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)]
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) || []

fid = %ExSDP.Attribute.SSRCGroup{semantics: "FID", ssrcs: [ssrc, rtx_ssrc]}

ssrc_attrs =
case streams do
[] ->
[
%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)

Enum.reverse(ssrc_attrs) ++ Enum.reverse(rtx_ssrc_attrs)
end

[fid | ssrc_attrs]
end

@doc false
@spec send_packet(sender(), ExRTP.Packet.t(), boolean()) :: {binary(), sender()}
def send_packet(sender, packet, rtx?) do
Expand Down
8 changes: 6 additions & 2 deletions lib/ex_webrtc/rtp_sender/nack_responder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ defmodule ExWebRTC.RTPSender.NACKResponder do

{packets, seq_no} =
seq_nos
|> Enum.map(fn seq_no -> {seq_no, Map.get(responder.packets, rem(seq_no, @max_packets))} end)
|> Enum.filter(fn {seq_no, packet} -> packet != nil and packet.sequence_number == seq_no end)
|> Enum.map(fn seq_no ->
{seq_no, Map.get(responder.packets, rem(seq_no, @max_packets))}
end)
|> Enum.filter(fn {seq_no, packet} ->
packet != nil and packet.sequence_number == seq_no
end)
Comment on lines +31 to +36
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

Formatter?

# ssrc will be assigned by the sender
|> Enum.map_reduce(responder.seq_no, fn {seq_no, packet}, rtx_seq_no ->
rtx_packet = %Packet{
Expand Down
62 changes: 36 additions & 26 deletions lib/ex_webrtc/rtp_transceiver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -526,39 +538,37 @@ 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)},
{:ice_options, Keyword.fetch!(opts, :ice_options)},
{: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)
| # mline must be followed by a cline, which must contain
# 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)
Expand Down
Loading
Loading