From c811f38b1700409a26eb7467645bd0495a394bbd Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Mon, 24 Apr 2023 12:53:59 +0200 Subject: [PATCH 1/3] RTSP component + small refactor --- lib/jellyfish/component.ex | 12 +++++++++++- lib/jellyfish/component/hls.ex | 12 ++++++++++++ lib/jellyfish/component/rtsp.ex | 24 ++++++++++++++++++++++++ lib/jellyfish/room.ex | 8 ++++---- test/jellyfish/room_test.exs | 22 +++++++++------------- 5 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 lib/jellyfish/component/hls.ex create mode 100644 lib/jellyfish/component/rtsp.ex diff --git a/lib/jellyfish/component.ex b/lib/jellyfish/component.ex index dca75b9..eecefc6 100644 --- a/lib/jellyfish/component.ex +++ b/lib/jellyfish/component.ex @@ -6,6 +6,7 @@ defmodule Jellyfish.Component do For more information refer to [Jellyfish documentation](https://www.membrane.stream) """ + alias Jellyfish.Component.{HLS, RTSP} alias Jellyfish.Exception.StructureError @enforce_keys [ @@ -30,7 +31,7 @@ defmodule Jellyfish.Component do Type describing component options. For the list of available options, please refer to the [documentation](https://jellyfish-dev.github.io/jellyfish-docs/). """ - @type options :: Keyword.t() + @type options :: HLS.t() | RTSP.t() @typedoc """ Stores information about the component. @@ -57,4 +58,13 @@ defmodule Jellyfish.Component do raise StructureError end end + + @doc false + @spec get_type(struct()) :: String.t() + def get_type(component) do + case component do + %HLS{} -> "hls" + %RTSP{} -> "rtsp" + end + end end diff --git a/lib/jellyfish/component/hls.ex b/lib/jellyfish/component/hls.ex new file mode 100644 index 0000000..7f53ae4 --- /dev/null +++ b/lib/jellyfish/component/hls.ex @@ -0,0 +1,12 @@ +defmodule Jellyfish.Component.HLS do + @moduledoc """ + Options for the HLS component. + + For the description of these options refer to [Jellyfish documentation](https://jellyfish-dev.github.io/jellyfish-docs/getting_started/components/hls) + """ + + @enforce_keys [] + defstruct @enforce_keys ++ [] + + @type t :: %__MODULE__{} +end diff --git a/lib/jellyfish/component/rtsp.ex b/lib/jellyfish/component/rtsp.ex new file mode 100644 index 0000000..5e5c37e --- /dev/null +++ b/lib/jellyfish/component/rtsp.ex @@ -0,0 +1,24 @@ +defmodule Jellyfish.Component.RTSP do + @moduledoc """ + Options for the RTSP component. + + For the description of these options refer to [Jellyfish documentation](https://jellyfish-dev.github.io/jellyfish-docs/getting_started/components/rtsp) + """ + + @enforce_keys [:sourceUri] + defstruct @enforce_keys ++ + [ + rtpPort: 20_000, + reconnectDelay: 15_000, + keepAliveInterval: 15_000, + pierceNat: true + ] + + @type t :: %__MODULE__{ + sourceUri: URI.t(), + rtpPort: 1..65_535, + reconnectDelay: non_neg_integer(), + keepAliveInterval: non_neg_integer(), + pierceNat: boolean() + } +end diff --git a/lib/jellyfish/room.ex b/lib/jellyfish/room.ex index 0fb77cb..27609d1 100644 --- a/lib/jellyfish/room.ex +++ b/lib/jellyfish/room.ex @@ -162,16 +162,16 @@ defmodule Jellyfish.Room do @doc """ Add component to the room with `room_id`. """ - @spec add_component(Client.t(), id(), Component.type(), Component.options()) :: + @spec add_component(Client.t(), id(), Component.options()) :: {:ok, Component.t()} | {:error, atom() | String.t()} - def add_component(client, room_id, type, opts \\ []) do + def add_component(client, room_id, component) do with {:ok, %Env{status: 201, body: body}} <- Tesla.post( client.http_client, "/room/#{room_id}/component", %{ - "type" => type, - "options" => Map.new(opts) + "type" => Component.get_type(component), + "options" => Map.from_struct(component) } ), {:ok, data} <- Map.fetch(body, "data"), diff --git a/test/jellyfish/room_test.exs b/test/jellyfish/room_test.exs index 772a767..dd014dc 100644 --- a/test/jellyfish/room_test.exs +++ b/test/jellyfish/room_test.exs @@ -12,6 +12,7 @@ defmodule Jellyfish.RoomTest do @component_id "mock_component_id" @component_type "hls" + @component_opts %Component.HLS{} @peer_id "mock_peer_id" @peer_type "webrtc" @@ -26,7 +27,9 @@ defmodule Jellyfish.RoomTest do @invalid_peer_type "abc" @invalid_component_id "invalid_component_id" - @invalid_component_type "abc" + defmodule InvalidComponentOpts do + defstruct [:abc, :xdd] + end @error_message "Mock error message" @@ -200,10 +203,9 @@ defmodule Jellyfish.RoomTest do end end - describe "Room.add_component/4" do + describe "Room.add_component/3" do setup do valid_body = Jason.encode!(%{"options" => %{}, "type" => @component_type}) - invalid_body = Jason.encode!(%{"options" => %{}, "type" => @invalid_component_type}) mock(fn %{ @@ -212,24 +214,18 @@ defmodule Jellyfish.RoomTest do body: ^valid_body } -> json(%{"data" => build_component_json()}, status: 201) - - %{ - method: :post, - url: "http://#{@url}/room/#{@room_id}/component", - body: ^invalid_body - } -> - json(%{"errors" => @error_message}, status: 400) end) end test "when request is valid", %{client: client} do - assert {:ok, component} = Room.add_component(client, @room_id, @component_type) + assert {:ok, component} = Room.add_component(client, @room_id, @component_opts) assert component == build_component() end test "when request is invalid", %{client: client} do - assert {:error, "Request failed: #{@error_message}"} = - Room.add_component(client, @room_id, @invalid_component_type) + assert_raise CaseClauseError, fn -> + Room.add_component(client, @room_id, %InvalidComponentOpts{}) + end end end From c3e1e371169e523d21b9f77cc04f273783e8b883 Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Fri, 5 May 2023 12:36:38 +0200 Subject: [PATCH 2/3] Change types to atoms --- README.md | 2 +- lib/jellyfish/component.ex | 30 +++++++++++++++++++++--------- lib/jellyfish/peer.ex | 16 +++++++++++++--- lib/jellyfish/room.ex | 8 ++++---- test/jellyfish/room_test.exs | 8 ++++---- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 93d83e5..51e9f90 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ room_id # => "8878cd13-99a6-40d6-8d7e-8da23d803dab" # Add peer -{:ok, %Jellyfish.Peer{id: peer_id}, peer_token} = Jellyfish.Room.add_peer(client, room_id, "webrtc") +{:ok, %Jellyfish.Peer{id: peer_id}, peer_token} = Jellyfish.Room.add_peer(client, room_id, :webrtc) receive do {:jellyfish, {:peer_connected, ^room_id, ^peer_id}} -> # handle the notification diff --git a/lib/jellyfish/component.ex b/lib/jellyfish/component.ex index eecefc6..bd491b6 100644 --- a/lib/jellyfish/component.ex +++ b/lib/jellyfish/component.ex @@ -25,11 +25,12 @@ defmodule Jellyfish.Component do For more information refer to [Jellyfish documentation](https://jellyfish-dev.github.io/jellyfish-docs/). """ - @type type :: String.t() + @type type :: :hls | :rtsp @typedoc """ - Type describing component options. - For the list of available options, please refer to the [documentation](https://jellyfish-dev.github.io/jellyfish-docs/). + Component-specific options. + + For the list of available options, refer to [Jellyfish documentation](https://jellyfish-dev.github.io/jellyfish-docs/). """ @type options :: HLS.t() | RTSP.t() @@ -41,17 +42,19 @@ defmodule Jellyfish.Component do type: type() } + @valid_type_strings ["hls", "rtsp"] + @doc false @spec from_json(map()) :: t() def from_json(response) do case response do %{ "id" => id, - "type" => type + "type" => type_str } -> %__MODULE__{ id: id, - type: type + type: type_from_string(type_str) } _other -> @@ -60,11 +63,20 @@ defmodule Jellyfish.Component do end @doc false - @spec get_type(struct()) :: String.t() - def get_type(component) do + @spec type_from_options(struct()) :: atom() + def type_from_options(component) do case component do - %HLS{} -> "hls" - %RTSP{} -> "rtsp" + %HLS{} -> :hls + %RTSP{} -> :rtsp + _other -> raise "Invalid component options struct" end end + + @doc false + @spec type_from_string(String.t()) :: atom() + def type_from_string(string) do + if string in @valid_type_strings, + do: String.to_atom(string), + else: raise("Invalid component type string") + end end diff --git a/lib/jellyfish/peer.ex b/lib/jellyfish/peer.ex index 9ab9ded..e9a32ee 100644 --- a/lib/jellyfish/peer.ex +++ b/lib/jellyfish/peer.ex @@ -25,7 +25,7 @@ defmodule Jellyfish.Peer do For more information refer to [Jellyfish documentation](https://jellyfish-dev.github.io/jellyfish-docs/). """ - @type type :: String.t() + @type type :: :webrtc @typedoc """ Stores information about the peer. @@ -35,21 +35,31 @@ defmodule Jellyfish.Peer do type: type() } + @valid_type_strings ["webrtc"] + @doc false @spec from_json(map()) :: t() def from_json(response) do case response do %{ "id" => id, - "type" => type + "type" => type_str } -> %__MODULE__{ id: id, - type: type + type: type_from_string(type_str) } _other -> raise StructureError end end + + @doc false + @spec type_from_string(String.t()) :: atom() + def type_from_string(string) do + if string in @valid_type_strings, + do: String.to_atom(string), + else: raise("Invalid peer type string") + end end diff --git a/lib/jellyfish/room.ex b/lib/jellyfish/room.ex index 27609d1..3dd3f26 100644 --- a/lib/jellyfish/room.ex +++ b/lib/jellyfish/room.ex @@ -13,9 +13,9 @@ defmodule Jellyfish.Room do peers: [] }} - iex> {:ok, peer, peer_token} = Jellyfish.Room.add_peer(client, room.id, "webrtc") + iex> {:ok, peer, peer_token} = Jellyfish.Room.add_peer(client, room.id, :webrtc) {:ok, - %Jellyfish.Peer{id: "5a731f2e-f49f-4d58-8f64-16a5c09b520e", type: "webrtc"}, + %Jellyfish.Peer{id: "5a731f2e-f49f-4d58-8f64-16a5c09b520e", type: :webrtc}, "3LTQ3ZDEtYTRjNy0yZDQyZjU1MDAxY2FkAAdyb29tX2lkbQAAACQ0M"} iex> :ok = Jellyfish.Room.delete(client, room.id) @@ -134,7 +134,7 @@ defmodule Jellyfish.Room do Tesla.post( client.http_client, "/room/#{room_id}/peer", - %{"type" => type} + %{"type" => Atom.to_string(type)} ), {:ok, %{"peer" => peer, "token" => token}} <- Map.fetch(body, "data"), result <- Peer.from_json(peer) do @@ -170,7 +170,7 @@ defmodule Jellyfish.Room do client.http_client, "/room/#{room_id}/component", %{ - "type" => Component.get_type(component), + "type" => Component.type_from_options(component) |> Atom.to_string(), "options" => Map.from_struct(component) } ), diff --git a/test/jellyfish/room_test.exs b/test/jellyfish/room_test.exs index dd014dc..3a1973b 100644 --- a/test/jellyfish/room_test.exs +++ b/test/jellyfish/room_test.exs @@ -11,10 +11,10 @@ defmodule Jellyfish.RoomTest do @invalid_url "invalid-url.com" @component_id "mock_component_id" - @component_type "hls" + @component_type :hls @component_opts %Component.HLS{} @peer_id "mock_peer_id" - @peer_type "webrtc" + @peer_type :webrtc @room_id "mock_room_id" @@ -24,7 +24,7 @@ defmodule Jellyfish.RoomTest do @invalid_max_peers "abc" @invalid_peer_id "invalid_peer_id" - @invalid_peer_type "abc" + @invalid_peer_type :abc @invalid_component_id "invalid_component_id" defmodule InvalidComponentOpts do @@ -223,7 +223,7 @@ defmodule Jellyfish.RoomTest do end test "when request is invalid", %{client: client} do - assert_raise CaseClauseError, fn -> + assert_raise RuntimeError, ~r/invalid.*options/i, fn -> Room.add_component(client, @room_id, %InvalidComponentOpts{}) end end From eac38324608adb1d0b49981ea0ee44fa53480d99 Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Mon, 8 May 2023 13:15:57 +0200 Subject: [PATCH 3/3] review fixes --- README.md | 2 +- lib/jellyfish/component.ex | 9 ++++++-- lib/jellyfish/component/rtsp.ex | 20 ++++++++--------- lib/jellyfish/peer.ex | 24 ++++++++++++++++++++- lib/jellyfish/peer/webrtc.ex | 12 +++++++++++ lib/jellyfish/room.ex | 24 +++++++++++++++------ test/jellyfish/room_test.exs | 38 ++++++++++++++++++++++----------- 7 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 lib/jellyfish/peer/webrtc.ex diff --git a/README.md b/README.md index 51e9f90..5a8260d 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ room_id # => "8878cd13-99a6-40d6-8d7e-8da23d803dab" # Add peer -{:ok, %Jellyfish.Peer{id: peer_id}, peer_token} = Jellyfish.Room.add_peer(client, room_id, :webrtc) +{:ok, %Jellyfish.Peer{id: peer_id}, peer_token} = Jellyfish.Room.add_peer(client, room_id, Jellyfish.Peer.WebRTC) receive do {:jellyfish, {:peer_connected, ^room_id, ^peer_id}} -> # handle the notification diff --git a/lib/jellyfish/component.ex b/lib/jellyfish/component.ex index bd491b6..51eaa7c 100644 --- a/lib/jellyfish/component.ex +++ b/lib/jellyfish/component.ex @@ -34,6 +34,11 @@ defmodule Jellyfish.Component do """ @type options :: HLS.t() | RTSP.t() + @typedoc """ + Component options module. + """ + @type options_module :: HLS | RTSP + @typedoc """ Stores information about the component. """ @@ -63,7 +68,7 @@ defmodule Jellyfish.Component do end @doc false - @spec type_from_options(struct()) :: atom() + @spec type_from_options(struct()) :: type() def type_from_options(component) do case component do %HLS{} -> :hls @@ -73,7 +78,7 @@ defmodule Jellyfish.Component do end @doc false - @spec type_from_string(String.t()) :: atom() + @spec type_from_string(String.t()) :: type() def type_from_string(string) do if string in @valid_type_strings, do: String.to_atom(string), diff --git a/lib/jellyfish/component/rtsp.ex b/lib/jellyfish/component/rtsp.ex index 5e5c37e..21d7579 100644 --- a/lib/jellyfish/component/rtsp.ex +++ b/lib/jellyfish/component/rtsp.ex @@ -5,20 +5,20 @@ defmodule Jellyfish.Component.RTSP do For the description of these options refer to [Jellyfish documentation](https://jellyfish-dev.github.io/jellyfish-docs/getting_started/components/rtsp) """ - @enforce_keys [:sourceUri] + @enforce_keys [:source_uri] defstruct @enforce_keys ++ [ - rtpPort: 20_000, - reconnectDelay: 15_000, - keepAliveInterval: 15_000, - pierceNat: true + rtp_port: 20_000, + reconnect_delay: 15_000, + keep_alive_interval: 15_000, + pierce_nat: true ] @type t :: %__MODULE__{ - sourceUri: URI.t(), - rtpPort: 1..65_535, - reconnectDelay: non_neg_integer(), - keepAliveInterval: non_neg_integer(), - pierceNat: boolean() + source_uri: URI.t(), + rtp_port: 1..65_535, + reconnect_delay: non_neg_integer(), + keep_alive_interval: non_neg_integer(), + pierce_nat: boolean() } end diff --git a/lib/jellyfish/peer.ex b/lib/jellyfish/peer.ex index e9a32ee..9dd59d1 100644 --- a/lib/jellyfish/peer.ex +++ b/lib/jellyfish/peer.ex @@ -8,6 +8,7 @@ defmodule Jellyfish.Peer do """ alias Jellyfish.Exception.StructureError + alias Jellyfish.Peer.WebRTC @enforce_keys [ :id, @@ -27,6 +28,18 @@ defmodule Jellyfish.Peer do """ @type type :: :webrtc + @typedoc """ + Peer-specific options. + + For the list of available options, refer to [Jellyfish documentation](https://jellyfish-dev.github.io/jellyfish-docs/). + """ + @type options :: WebRTC.t() + + @typedoc """ + Peer options module. + """ + @type options_module :: WebRTC + @typedoc """ Stores information about the peer. """ @@ -56,7 +69,16 @@ defmodule Jellyfish.Peer do end @doc false - @spec type_from_string(String.t()) :: atom() + @spec type_from_options(struct()) :: type() + def type_from_options(peer) do + case peer do + %WebRTC{} -> :webrtc + _other -> raise "Invalid peer options struct" + end + end + + @doc false + @spec type_from_string(String.t()) :: type() def type_from_string(string) do if string in @valid_type_strings, do: String.to_atom(string), diff --git a/lib/jellyfish/peer/webrtc.ex b/lib/jellyfish/peer/webrtc.ex new file mode 100644 index 0000000..8dcb394 --- /dev/null +++ b/lib/jellyfish/peer/webrtc.ex @@ -0,0 +1,12 @@ +defmodule Jellyfish.Peer.WebRTC do + @moduledoc """ + Options for the WebRTC peer. + + For the description of these options refer to [Jellyfish documentation](https://jellyfish-dev.github.io/jellyfish-docs/getting_started/peers/webrtc) + """ + + @enforce_keys [] + defstruct @enforce_keys ++ [] + + @type t :: %__MODULE__{} +end diff --git a/lib/jellyfish/room.ex b/lib/jellyfish/room.ex index 3dd3f26..2980ba0 100644 --- a/lib/jellyfish/room.ex +++ b/lib/jellyfish/room.ex @@ -13,7 +13,7 @@ defmodule Jellyfish.Room do peers: [] }} - iex> {:ok, peer, peer_token} = Jellyfish.Room.add_peer(client, room.id, :webrtc) + iex> {:ok, peer, peer_token} = Jellyfish.Room.add_peer(client, room.id, Jellyfish.Peer.WebRTC) {:ok, %Jellyfish.Peer{id: "5a731f2e-f49f-4d58-8f64-16a5c09b520e", type: :webrtc}, "3LTQ3ZDEtYTRjNy0yZDQyZjU1MDAxY2FkAAdyb29tX2lkbQAAACQ0M"} @@ -127,14 +127,16 @@ defmodule Jellyfish.Room do @doc """ Add a peer to the room with `room_id`. """ - @spec add_peer(Client.t(), id(), Peer.type()) :: + @spec add_peer(Client.t(), id(), Peer.options() | Peer.options_module()) :: {:ok, Peer.t(), peer_token()} | {:error, atom() | String.t()} - def add_peer(client, room_id, type) do + def add_peer(client, room_id, peer) do + peer = if is_atom(peer), do: struct!(peer), else: peer + with {:ok, %Env{status: 201, body: body}} <- Tesla.post( client.http_client, "/room/#{room_id}/peer", - %{"type" => Atom.to_string(type)} + %{"type" => Peer.type_from_options(peer) |> Atom.to_string()} ), {:ok, %{"peer" => peer, "token" => token}} <- Map.fetch(body, "data"), result <- Peer.from_json(peer) do @@ -162,16 +164,20 @@ defmodule Jellyfish.Room do @doc """ Add component to the room with `room_id`. """ - @spec add_component(Client.t(), id(), Component.options()) :: + @spec add_component(Client.t(), id(), Component.options() | Component.options_module()) :: {:ok, Component.t()} | {:error, atom() | String.t()} def add_component(client, room_id, component) do + component = if is_atom(component), do: struct!(component), else: component + with {:ok, %Env{status: 201, body: body}} <- Tesla.post( client.http_client, "/room/#{room_id}/component", %{ "type" => Component.type_from_options(component) |> Atom.to_string(), - "options" => Map.from_struct(component) + "options" => + Map.from_struct(component) + |> Map.new(fn {k, v} -> {snake_case_to_camel_case(k), v} end) } ), {:ok, data} <- Map.fetch(body, "data"), @@ -224,4 +230,10 @@ defmodule Jellyfish.Room do defp handle_response_error({:ok, %Env{body: _body}}), do: raise(StructureError) defp handle_response_error({:error, reason}), do: {:error, reason} + + defp snake_case_to_camel_case(atom) do + [first | rest] = Atom.to_string(atom) |> String.split("_") + rest = rest |> Enum.map(&String.capitalize/1) + Enum.join([first | rest]) + end end diff --git a/test/jellyfish/room_test.exs b/test/jellyfish/room_test.exs index 3a1973b..b57abea 100644 --- a/test/jellyfish/room_test.exs +++ b/test/jellyfish/room_test.exs @@ -13,8 +13,11 @@ defmodule Jellyfish.RoomTest do @component_id "mock_component_id" @component_type :hls @component_opts %Component.HLS{} + @component_opts_module Component.HLS @peer_id "mock_peer_id" @peer_type :webrtc + @peer_opts %Peer.WebRTC{} + @peer_opts_module Peer.WebRTC @room_id "mock_room_id" @@ -24,11 +27,13 @@ defmodule Jellyfish.RoomTest do @invalid_max_peers "abc" @invalid_peer_id "invalid_peer_id" - @invalid_peer_type :abc + defmodule InvalidPeerOpts do + defstruct [:qwe, :rty] + end @invalid_component_id "invalid_component_id" defmodule InvalidComponentOpts do - defstruct [:abc, :xdd] + defstruct [:abc, :def] end @error_message "Mock error message" @@ -220,12 +225,19 @@ defmodule Jellyfish.RoomTest do test "when request is valid", %{client: client} do assert {:ok, component} = Room.add_component(client, @room_id, @component_opts) assert component == build_component() + + assert {:ok, component} = Room.add_component(client, @room_id, @component_opts_module) + assert component == build_component() end test "when request is invalid", %{client: client} do assert_raise RuntimeError, ~r/invalid.*options/i, fn -> Room.add_component(client, @room_id, %InvalidComponentOpts{}) end + + assert_raise RuntimeError, ~r/invalid.*options/i, fn -> + Room.add_component(client, @room_id, InvalidComponentOpts) + end end end @@ -259,7 +271,6 @@ defmodule Jellyfish.RoomTest do describe "Room.add_peer/3" do setup do valid_body = Jason.encode!(%{"type" => @peer_type}) - invalid_body = Jason.encode!(%{"type" => @invalid_peer_type}) mock(fn %{ @@ -268,24 +279,25 @@ defmodule Jellyfish.RoomTest do body: ^valid_body } -> json(%{"data" => build_peer_json()}, status: 201) - - %{ - method: :post, - url: "http://#{@url}/room/#{@room_id}/peer", - body: ^invalid_body - } -> - json(%{"errors" => @error_message}, status: 400) end) end test "when request is valid", %{client: client} do - assert {:ok, peer, _peer_token} = Room.add_peer(client, @room_id, @peer_type) + assert {:ok, peer, _peer_token} = Room.add_peer(client, @room_id, @peer_opts) + assert peer == build_peer() + + assert {:ok, peer, _peer_token} = Room.add_peer(client, @room_id, @peer_opts_module) assert peer == build_peer() end test "when request is invalid", %{client: client} do - assert {:error, "Request failed: #{@error_message}"} = - Room.add_peer(client, @room_id, @invalid_peer_type) + assert_raise RuntimeError, ~r/invalid.*options/i, fn -> + Room.add_peer(client, @room_id, %InvalidPeerOpts{}) + end + + assert_raise RuntimeError, ~r/invalid.*options/i, fn -> + Room.add_peer(client, @room_id, InvalidPeerOpts) + end end end