From 050db0faaaf2714cf41c55d204a023fb6f32dc81 Mon Sep 17 00:00:00 2001 From: Logeshwaran <9105503+waranlogesh@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:02:21 +0530 Subject: [PATCH] Add GetServiceCapabilities for media ver10 (#70) --- 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..5d4d9b3 --- /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, ProfileCapabilities, 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..01fa2a2 --- /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 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() + + 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.ProfileCapabilities{ + 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 @@ + + + + + + + + + + + +