diff --git a/examples/example.exs b/examples/example.exs index 4000e310..02fed4e5 100644 --- a/examples/example.exs +++ b/examples/example.exs @@ -31,7 +31,6 @@ defmodule Peer do Process.send_after(self(), :ws_ping, 1000) {:ok, pc} = PeerConnection.start_link( - bundle_policy: :max_bundle, ice_servers: @ice_servers ) diff --git a/lib/ex_webrtc/media_stream_track.ex b/lib/ex_webrtc/media_stream_track.ex new file mode 100644 index 00000000..62cf9c8e --- /dev/null +++ b/lib/ex_webrtc/media_stream_track.ex @@ -0,0 +1,7 @@ +defmodule ExWebRTC.MediaStreamTrack do + defstruct [] + + def from_transceiver(_tr) do + %__MODULE__{} + end +end diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index cee8caaa..69adc54f 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -7,7 +7,7 @@ defmodule ExWebRTC.PeerConnection do alias __MODULE__.Configuration alias ExICE.ICEAgent - alias ExWebRTC.{IceCandidate, RTPTransceiver, SessionDescription} + alias ExWebRTC.{IceCandidate, MediaStreamTrack, RTPTransceiver, SessionDescription} import ExWebRTC.Utils @@ -28,7 +28,7 @@ defmodule ExWebRTC.PeerConnection do :dtls_client, :dtls_buffered_packets, dtls_finished: false, - transceivers: %{}, + transceivers: [], signaling_state: :stable ] @@ -108,6 +108,11 @@ defmodule ExWebRTC.PeerConnection do GenServer.call(peer_connection, {:add_ice_candidate, candidate}) end + @spec get_transceivers(peer_connection()) :: {:ok, [RTPTransceiver.t()]} + def get_transceivers(peer_connection) do + GenServer.call(peer_connection, :get_transceivers) + end + #### CALLBACKS #### @impl true @@ -232,7 +237,7 @@ defmodule ExWebRTC.PeerConnection do # username_fragment: "vx/1" } - send(state.owner, {:ex_webrtc, {:ice_candidate, candidate}}) + notify(state.owner, {:ice_candidate, candidate}) {:noreply, state} end @@ -293,54 +298,56 @@ defmodule ExWebRTC.PeerConnection do defp apply_remote_description(_type, sdp, state) do # TODO apply steps listed in RFC 8829 5.10 media = hd(sdp.media) - {:ice_ufrag, ufrag} = ExSDP.Media.get_attribute(media, :ice_ufrag) - {:ice_pwd, pwd} = ExSDP.Media.get_attribute(media, :ice_pwd) - - :ok = ICEAgent.set_remote_credentials(state.ice_agent, ufrag, pwd) - :ok = ICEAgent.gather_candidates(state.ice_agent) - - transceivers = - sdp.media - |> Enum.reduce(%{}, fn media -> - {:mid, mid} = ExSDP.Media.get_attribute(media, :mid) - # if there is no direction, the default is sendrecv - # see RFC 3264, sec. 6.1 - direction = get_media_direction(media) || :sendrecv - - if mid == nil or direction == :inactive do - # FIXME instead of raising return an error; - # FIXME what about inactive mlines? - # Should we create transceivers for them too?; - raise "Invalid remote description: missing or invalid mid or direction" - end - - tr = - Map.get(state.transceivers, mid) || - find_or_create_transceiver(mid, direction, state.transceivers) - {tr.mid, tr} - end) - |> Map.merge(state.transceivers) - - {:ok, %{state | current_remote_desc: sdp, transceivers: transceivers}} + with {:ice_ufrag, ufrag} <- ExSDP.Media.get_attribute(media, :ice_ufrag), + {:ice_pwd, pwd} <- ExSDP.Media.get_attribute(media, :ice_pwd), + {:ok, transceivers} <- get_transceivers(sdp, state) do + :ok = ICEAgent.set_remote_credentials(state.ice_agent, ufrag, pwd) + :ok = ICEAgent.gather_candidates(state.ice_agent) + + new_remote_tracks = + transceivers + # only take new transceivers that can receive tracks + |> Enum.filter(fn tr -> + RTPTransceiver.find_by_mid(state.transceivers, tr.mid) == nil and + tr.direction in [:recvonly, :sendrecv] + end) + |> Enum.map(fn tr -> MediaStreamTrack.from_transceiver(tr) end) + + for track <- new_remote_tracks do + notify(state.owner, {:track, track}) + end + + {:ok, %{state | current_remote_desc: sdp, transceivers: transceivers}} + else + nil -> {:error, :missing_ice_ufrag_or_pwd} + end end - defp find_or_create_transceiver(mid, direction, transceivers) - when direction in [:sendrecv, :recvonly] do - Enum.find(transceivers, %RTPTransceiver{mid: mid, direction: :recvonly}, fn - {nil, tr} when tr.direction == direction -> tr - _other -> nil + defp get_transceivers(sdp, state) do + Enum.reduce_while(sdp.media, {:ok, state.transceivers}, fn mline, {:ok, transceivers} -> + case ExSDP.Media.get_attribute(mline, :mid) do + {:mid, mid} -> + transceivers = update_or_create_transceiver(mid, mline, state.transceivers) + {:cont, {:ok, transceivers}} + + _other -> + {:halt, {:error, :missing_mid}} + end end) end - defp find_or_create_transceiver(mid, :sendonly, _transceivers) do - %RTPTransceiver{mid: mid, direction: :recvonly} - end + defp update_or_create_transceiver(mid, mline, transceivers) do + case RTPTransceiver.find_by_mid(transceivers, mid) do + {idx, %RTPTransceiver{} = tr} -> + case RTPTransceiver.update(tr, mline) do + {:ok, tr} -> List.replace_at(transceivers, idx, tr) + {:error, :remove} -> List.delete_at(transceivers, idx) + end - defp get_media_direction(media) do - Enum.find(media.attributes, fn attr -> - attr in [:sendrecv, :sendonly, :recvonly, :inactive] - end) + nil -> + transceivers ++ [%RTPTransceiver{mid: mid, direction: :recvonly}] + end end # Signaling state machine, RFC 8829 3.2 @@ -367,4 +374,6 @@ defmodule ExWebRTC.PeerConnection do defp maybe_next_state(:have_remote_pranswer, :remote, :answer), do: {:ok, :stable} defp maybe_next_state(:have_remote_pranswer, _, _), do: {:error, :invalid_transition} + + defp notify(pid, msg), do: send(pid, {:ex_webrtc, self(), msg}) end diff --git a/lib/ex_webrtc/peer_connection/configuration.ex b/lib/ex_webrtc/peer_connection/configuration.ex index 34fdc038..c197389e 100644 --- a/lib/ex_webrtc/peer_connection/configuration.ex +++ b/lib/ex_webrtc/peer_connection/configuration.ex @@ -33,7 +33,7 @@ defmodule ExWebRTC.PeerConnection.Configuration do rtcp_mux_policy: rtcp_mux_policy() } - defstruct bundle_policy: :balanced, + defstruct bundle_policy: :max_bundle, certificates: nil, ice_candidate_pool_size: 0, ice_servers: [], diff --git a/lib/ex_webrtc/rtp_transceiver.ex b/lib/ex_webrtc/rtp_transceiver.ex index 777e47f2..38240cfc 100644 --- a/lib/ex_webrtc/rtp_transceiver.ex +++ b/lib/ex_webrtc/rtp_transceiver.ex @@ -6,4 +6,20 @@ defmodule ExWebRTC.RTPTransceiver do @enforce_keys [:mid, :direction] defstruct @enforce_keys + + def find_by_mid(transceivers, mid) do + transceivers + |> Enum.with_index(fn tr, idx -> {idx, tr} end) + |> Enum.find(fn {_idx, tr} -> tr.mid == mid end) + end + + def update(transceiver, mline) do + # if there is no direction, the default is sendrecv + # see RFC 3264, sec. 6.1 + + case ExWebRTC.Utils.get_media_direction(mline) || :sendrecv do + :inactive -> {:error, :remove} + other_direction -> {:ok, %__MODULE__{transceiver | direction: other_direction}} + end + end end diff --git a/lib/ex_webrtc/utils.ex b/lib/ex_webrtc/utils.ex index 2d6d9b43..0581524f 100644 --- a/lib/ex_webrtc/utils.ex +++ b/lib/ex_webrtc/utils.ex @@ -7,4 +7,10 @@ defmodule ExWebRTC.Utils do |> :binary.bin_to_list() |> Enum.map_join(":", &Base.encode16(<<&1>>)) end + + def get_media_direction(media) do + Enum.find(media.attributes, fn attr -> + attr in [:sendrecv, :sendonly, :recvonly, :inactive] + end) + end end diff --git a/test/peer_connection_test.exs b/test/peer_connection_test.exs new file mode 100644 index 00000000..ae4cbd13 --- /dev/null +++ b/test/peer_connection_test.exs @@ -0,0 +1,41 @@ +defmodule ExWebRTC.PeerConnectionTest do + use ExUnit.Case, async: true + + alias ExWebRTC.{MediaStreamTrack, PeerConnection, SessionDescription} + + @offer """ + v=0 + o=- 6788894006044524728 2 IN IP4 127.0.0.1 + s=- + t=0 0 + a=group:BUNDLE 0 + a=extmap-allow-mixed + a=msid-semantic: WMS + m=audio 9 UDP/TLS/RTP/SAVPF 111 + c=IN IP4 0.0.0.0 + a=rtcp:9 IN IP4 0.0.0.0 + a=ice-ufrag:cDua + a=ice-pwd:v9SCmZHxJWtgpyzn8Ts1puT6 + a=ice-options:trickle + a=fingerprint:sha-256 11:35:68:66:A4:C3:C0:AA:37:4E:0F:97:D7:9F:76:11:08:DB:56:DA:4B:83:77:50:9A:D2:71:8D:2A:A8:E3:07 + a=setup:actpass + a=mid:0 + a=sendrecv + a=msid:- 54f0751b-086f-433c-af40-79c179182423 + a=rtcp-mux + a=rtpmap:111 opus/48000/2 + a=rtcp-fb:111 transport-cc + a=fmtp:111 minptime=10;useinbandfec=1 + a=ssrc:1463342914 cname:poWwjNZ4I2ZZgzY7 + a=ssrc:1463342914 msid:- 54f0751b-086f-433c-af40-79c179182423 + """ + + test "transceivers" do + {:ok, pc} = PeerConnection.start_link() + + offer = %SessionDescription{type: :offer, sdp: @offer} + :ok = PeerConnection.set_remote_description(pc, offer) + + assert_receive {:ex_webrtc, ^pc, {:track, %MediaStreamTrack{}}} + end +end