From 83b622f89722c11444777b720bf1e366c79927f2 Mon Sep 17 00:00:00 2001 From: Logeshwaran Murugesan <9105503+waranlogesh@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:26:51 +0530 Subject: [PATCH 1/3] Add GetServiceCapabilities for media ver10 --- lib/media/ver10/get_service_capabilities.ex | 44 ++++++ lib/media/ver10/service_capabilities.ex | 128 ++++++++++++++++++ test/media/get_service_capabilities_test.exs | 39 ++++++ .../get_service_capabilities_response.xml | 12 ++ 4 files changed, 223 insertions(+) create mode 100644 lib/media/ver10/get_service_capabilities.ex create mode 100644 lib/media/ver10/service_capabilities.ex create mode 100644 test/media/get_service_capabilities_test.exs create mode 100644 test/media/ver10/fixtures/get_service_capabilities_response.xml diff --git a/lib/media/ver10/get_service_capabilities.ex b/lib/media/ver10/get_service_capabilities.ex new file mode 100644 index 0000000..0d4d18f --- /dev/null +++ b/lib/media/ver10/get_service_capabilities.ex @@ -0,0 +1,44 @@ +defmodule Onvif.Media.Ver10.GetServiceCapabilities do + import SweetXml + import XmlBuilder + + require Logger + + alias Onvif.Device + + def soap_action, do: "http://www.onvif.org/ver10/media/wsdl/GetServiceCapabilities" + + @spec request(Device.t()) :: {:ok, any} | {:error, map()} + def request(device), + do: Onvif.Media.Ver10.Media.request(device, [], __MODULE__) + + def request_body do + element(:"s:Body", [element(:"trt:GetServiceCapabilities")]) + end + + def response(xml_response_body) do + parsed = + xml_response_body + |> parse(namespace_conformant: true, quiet: true) + |> xpath( + ~x"//s:Envelope/s:Body/trt:GetServiceCapabilitiesResponse/trt:Capabilities"e + |> add_namespace("s", "http://www.w3.org/2003/05/soap-envelope") + |> add_namespace("trt", "http://www.onvif.org/ver10/media/wsdl") + |> add_namespace("tt", "http://www.onvif.org/ver10/schema") + ) + |> Onvif.Media.Ver10.ServiceCapabilities.parse() + |> Onvif.Media.Ver10.ServiceCapabilities.to_struct() + + response = + case parsed do + {:ok, sevice} -> + sevice + + {:error, changeset} -> + Logger.error("Discarding invalid service capability: #{inspect(changeset)}") + %Onvif.Media.Ver10.ServiceCapabilities{} + end + + {:ok, response} + end +end diff --git a/lib/media/ver10/service_capabilities.ex b/lib/media/ver10/service_capabilities.ex new file mode 100644 index 0000000..c3f9578 --- /dev/null +++ b/lib/media/ver10/service_capabilities.ex @@ -0,0 +1,128 @@ +defmodule Onvif.Media.Ver10.ServiceCapabilities do + @moduledoc false + + use Ecto.Schema + import Ecto.Changeset + import SweetXml + + @primary_key false + @derive Jason.Encoder + @required [] + @optional [ + :snapshot_uri, + :rotation, + :video_source_mode, + :osd, + :temporary_osd_text, + :exi_compression + ] + embedded_schema do + field(:snapshot_uri, :boolean, default: false) + field(:rotation, :boolean, default: false) + field(:video_source_mode, :boolean, default: false) + field(:osd, :boolean, default: false) + field(:temporary_osd_text, :boolean, default: false) + field(:exi_compression, :boolean, default: false) + + embeds_one :profile_capabilities, ProfileCapailities, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:maximum_number_of_profiles, :integer) + end + + embeds_one :streaming_capabilities, StreamingCapabilities, + primary_key: false, + on_replace: :update do + @derive Jason.Encoder + field(:rtsp_multicast, :boolean, default: false) + field(:rtp_tcp, :boolean, default: false) + field(:rtp_rtsp_tcp, :boolean, default: false) + field(:non_aggregated_control, :boolean, default: false) + field(:no_rtsp_streaming, :boolean, default: false) + end + end + + def to_struct(parsed) do + %__MODULE__{} + |> changeset(parsed) + |> apply_action(:validate) + end + + @spec to_json(%Onvif.Media.Ver10.Profile.VideoEncoderConfiguration{}) :: + {:error, + %{ + :__exception__ => any, + :__struct__ => Jason.EncodeError | Protocol.UndefinedError, + optional(atom) => any + }} + | {:ok, binary} + def to_json(%__MODULE__{} = schema) do + Jason.encode(schema) + end + + def changeset(module, attrs) do + module + |> cast(attrs, @required ++ @optional) + |> validate_required(@required) + |> cast_embed(:profile_capabilities, with: &profile_capabilities_changeset/2) + |> cast_embed(:streaming_capabilities, + with: &streaming_capabilities_changeset/2 + ) + end + + def parse(nil), do: nil + def parse([]), do: nil + + def parse(doc) do + xmap( + doc, + snapshot_uri: ~x"./@SnapshotUri"so, + rotation: ~x"./@Rotation"so, + video_source_mode: ~x"./@VideoSourceMode"so, + osd: ~x"./@OSD"so, + temporary_osd_text: ~x"./@TemporaryOSDText"so, + exi_compression: ~x"./@EXICompression"so, + profile_capabilities: + ~x"./trt:ProfileCapabilities"eo |> transform_by(&parse_profile_capabilities/1), + streaming_capabilities: + ~x"./trt:StreamingCapabilities"eo |> transform_by(&parse_streaming_capabilities/1) + ) + end + + defp profile_capabilities_changeset(module, attrs) do + cast(module, attrs, [:maximum_number_of_profiles]) + end + + defp streaming_capabilities_changeset(module, attrs) do + cast(module, attrs, [ + :rtsp_multicast, + :rtp_tcp, + :rtp_rtsp_tcp, + :non_aggregated_control, + :no_rtsp_streaming + ]) + end + + defp parse_profile_capabilities([]), do: nil + defp parse_profile_capabilities(nil), do: nil + + defp parse_profile_capabilities(doc) do + xmap( + doc, + maximum_number_of_profiles: ~x"./@MaximumNumberOfProfiles"i + ) + end + + defp parse_streaming_capabilities([]), do: nil + defp parse_streaming_capabilities(nil), do: nil + + defp parse_streaming_capabilities(doc) do + xmap( + doc, + rtsp_multicast: ~x"./@RTPMulticast "so, + rtp_tcp: ~x"./@RTP_TCP"so, + rtp_rtsp_tcp: ~x"./@RTP_RTSP_TCP"so, + non_aggregated_control: ~x"./@NonAggregateControl"so, + no_rtsp_streaming: ~x"./@NoRTSPStreaming"so + ) + end +end diff --git a/test/media/get_service_capabilities_test.exs b/test/media/get_service_capabilities_test.exs new file mode 100644 index 0000000..1429925 --- /dev/null +++ b/test/media/get_service_capabilities_test.exs @@ -0,0 +1,39 @@ +defmodule Onvif.Media.Ver10.GetServiceCapabilitiesTest do + use ExUnit.Case, async: true + + @moduletag capture_log: true + + describe "GetServiceCapabilities/1" do + test "should parse with correct dvalues and defaults for non existing attributes" do + xml_response = File.read!("test/media/ver10/fixtures/get_service_capabilities_response.xml") + + device = Onvif.Factory.device() + + Mimic.expect(Tesla, :request, fn _client, _opts -> + {:ok, %{status: 200, body: xml_response}} + end) + + {:ok, service_capabilities} = Onvif.Media.Ver10.GetServiceCapabilities.request(device) + + assert service_capabilities == %Onvif.Media.Ver10.ServiceCapabilities{ + exi_compression: false, + osd: true, + profile_capabilities: %Onvif.Media.Ver10.ServiceCapabilities.ProfileCapailities{ + maximum_number_of_profiles: 24 + }, + rotation: false, + snapshot_uri: true, + streaming_capabilities: + %Onvif.Media.Ver10.ServiceCapabilities.StreamingCapabilities{ + no_rtsp_streaming: false, + non_aggregated_control: false, + rtp_rtsp_tcp: true, + rtp_tcp: false, + rtsp_multicast: false + }, + temporary_osd_text: false, + video_source_mode: false + } + end + end +end diff --git a/test/media/ver10/fixtures/get_service_capabilities_response.xml b/test/media/ver10/fixtures/get_service_capabilities_response.xml new file mode 100644 index 0000000..94e5c38 --- /dev/null +++ b/test/media/ver10/fixtures/get_service_capabilities_response.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + From efc1fc3ac10fc40c1cf61d46f493cd9c3585a10f Mon Sep 17 00:00:00 2001 From: Logeshwaran <9105503+waranlogesh@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:29:38 +0530 Subject: [PATCH 2/3] Update test/media/get_service_capabilities_test.exs --- test/media/get_service_capabilities_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/media/get_service_capabilities_test.exs b/test/media/get_service_capabilities_test.exs index 1429925..7f5dcc0 100644 --- a/test/media/get_service_capabilities_test.exs +++ b/test/media/get_service_capabilities_test.exs @@ -4,7 +4,7 @@ defmodule Onvif.Media.Ver10.GetServiceCapabilitiesTest do @moduletag capture_log: true describe "GetServiceCapabilities/1" do - test "should parse with correct dvalues and defaults for non existing attributes" do + test "should parse with correct values and defaults for non existing attributes" do xml_response = File.read!("test/media/ver10/fixtures/get_service_capabilities_response.xml") device = Onvif.Factory.device() From 3b9848bfa8e3e889d9a22ec253d3342a9e7a2d4e Mon Sep 17 00:00:00 2001 From: Logeshwaran Murugesan <9105503+waranlogesh@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:55:54 +0530 Subject: [PATCH 3/3] Fix typo --- lib/media/ver10/service_capabilities.ex | 2 +- test/media/get_service_capabilities_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/media/ver10/service_capabilities.ex b/lib/media/ver10/service_capabilities.ex index c3f9578..5d4d9b3 100644 --- a/lib/media/ver10/service_capabilities.ex +++ b/lib/media/ver10/service_capabilities.ex @@ -24,7 +24,7 @@ defmodule Onvif.Media.Ver10.ServiceCapabilities do field(:temporary_osd_text, :boolean, default: false) field(:exi_compression, :boolean, default: false) - embeds_one :profile_capabilities, ProfileCapailities, primary_key: false, on_replace: :update do + embeds_one :profile_capabilities, ProfileCapabilities, primary_key: false, on_replace: :update do @derive Jason.Encoder field(:maximum_number_of_profiles, :integer) end diff --git a/test/media/get_service_capabilities_test.exs b/test/media/get_service_capabilities_test.exs index 7f5dcc0..01fa2a2 100644 --- a/test/media/get_service_capabilities_test.exs +++ b/test/media/get_service_capabilities_test.exs @@ -18,7 +18,7 @@ defmodule Onvif.Media.Ver10.GetServiceCapabilitiesTest do assert service_capabilities == %Onvif.Media.Ver10.ServiceCapabilities{ exi_compression: false, osd: true, - profile_capabilities: %Onvif.Media.Ver10.ServiceCapabilities.ProfileCapailities{ + profile_capabilities: %Onvif.Media.Ver10.ServiceCapabilities.ProfileCapabilities{ maximum_number_of_profiles: 24 }, rotation: false,