diff --git a/lib/media/ver10/get_osd_options.ex b/lib/media/ver10/get_osd_options.ex new file mode 100644 index 0000000..ae5c63b --- /dev/null +++ b/lib/media/ver10/get_osd_options.ex @@ -0,0 +1,35 @@ +defmodule Onvif.Media.Ver10.GetOSDOptions do + import SweetXml + import XmlBuilder + + alias Onvif.Device + alias Onvif.Media.Ver10.OSDOptions + + @spec soap_action :: String.t() + def soap_action, do: "http://www.onvif.org/ver10/media/wsdl/GetOSDOptions" + + @spec request(Device.t(), list) :: {:ok, any} | {:error, map()} + def request(device, args), + do: Onvif.Media.Ver10.Media.request(device, args, __MODULE__) + + def request_body(token) do + element(:"s:Body", [ + element(:"trt:GetOSDOptions", [ + element(:"trt:ConfigurationToken", token) + ]) + ]) + end + + def response(xml_response_body) do + xml_response_body + |> parse(namespace_conformant: true, quiet: true) + |> xpath( + ~x"//s:Envelope/s:Body/trt:GetOSDOptionsResponse/trt:OSDOptions"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") + ) + |> OSDOptions.parse() + |> OSDOptions.to_struct() + end +end diff --git a/lib/media/ver10/osd_options.ex b/lib/media/ver10/osd_options.ex new file mode 100644 index 0000000..de6aae4 --- /dev/null +++ b/lib/media/ver10/osd_options.ex @@ -0,0 +1,271 @@ +defmodule Onvif.Media.Ver10.OSDOptions do + @moduledoc """ + OSD (On-Screen Display) Options specification. + """ + + use Ecto.Schema + import Ecto.Changeset + import SweetXml + + @primary_key false + @derive Jason.Encoder + @required [:type, :position_option] + @optional [] + + embedded_schema do + field(:type, {:array, :string}) + field(:position_option, {:array, :string}) + embeds_one :maximum_number_of_osds, MaximumNumberOfOSDs, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:total, :integer) + field(:image, :integer) + field(:plaintext, :integer) + field(:date, :integer) + field(:time, :integer) + field(:date_and_time, :integer) + end + embeds_one :text_option, TextOption, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:type , {:array, :string}) + embeds_one :font_size_range, FontSizeRange, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:min, :integer) + field(:max, :integer) + end + field(:date_format, {:array, :string}) + field(:time_format, {:array, :string}) + embeds_one :font_color, FontColor, primary_key: false, on_replace: :update do + @derive Jason.Encoder + embeds_one :color, Color, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:color_list, {:array, :string}) + embeds_one :color_space_range, ColorSpaceRange, primary_key: false, on_replace: :update do + @derive Jason.Encoder + embeds_one :x , X, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:min, :integer) + field(:max, :integer) + end + embeds_one :y , Y, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:min, :integer) + field(:max, :integer) + end + embeds_one :z , Z, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:min, :integer) + field(:max, :integer) + end + field(:color_space, {:array, :string}) + end + end + embeds_one :transparent, Transparent, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:min, :integer) + field(:max, :integer) + end + end + embeds_one :background_color, BackgroundColor, primary_key: false, on_replace: :update do + @derive Jason.Encoder + embeds_one :color, Color, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:color_list, {:array, :string}) + embeds_one :color_space_range, ColorSpaceRange, primary_key: false, on_replace: :update do + @derive Jason.Encoder + embeds_one :x , X, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:min, :integer) + field(:max, :integer) + end + embeds_one :y , Y, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:min, :integer) + field(:max, :integer) + end + embeds_one :z , Z, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:min, :integer) + field(:max, :integer) + end + field(:color_space, {:array, :string}) + end + end + embeds_one :transparent, Transparent, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:min, :integer) + field(:max, :integer) + end + end + end + embeds_one :image_option, ImageOption, primary_key: false, on_replace: :update do + field(:formats_supported, {:array, :string}) + field(:max_size, :integer) + field(:max_width, :integer) + field(:max_height, :integer) + field(:image_path, :string) + end + end + + def parse(nil), do: nil + def parse([]), do: nil + def parse(doc) do + xmap( + doc, + type: ~x"./tt:Type/text()"slo, + position_option: ~x"./tt:PositionOption/text()"slo, + maximum_number_of_osds: ~x"./tt:MaximumNumberOfOSDs"eo |> transform_by(&parse_maximum_number_of_osds/1), + text_option: ~x"./tt:TextOption"eo |> transform_by(&parse_text_option/1), + image_option: ~x"./tt:ImageOption"eo |> transform_by(&parse_image_option/1) + ) + end + + def parse_maximum_number_of_osds([]), do: nil + def parse_maximum_number_of_osds(nil), do: nil + def parse_maximum_number_of_osds(doc) do + xmap( + doc, + total: ~x"//@Total"so, + image: ~x"//@Image"so, + plaintext: ~x"//@PlainText"so, + date: ~x"//@Date"so, + time: ~x"//@Time"so, + date_and_time: ~x"//@DateAndTime"so + ) + end + + def parse_text_option([]), do: nil + def parse_text_option(nil), do: nil + def parse_text_option(doc) do + xmap( + doc, + type: ~x"./tt:Type/text()"slo, + font_size_range: ~x"./tt:FontSizeRange"eo |> transform_by(&parse_int_range/1), + date_format: ~x"./tt:DateFormat/text()"slo, + time_format: ~x"./tt:TimeFormat/text()"slo, + font_color: ~x"./tt:FontColor"eo |> transform_by(&parse_text_color/1), + background_color: ~x"./tt:BackgroundColor"eo |> transform_by(&parse_text_color/1) + ) + end + + def parse_int_range([]), do: nil + def parse_int_range(nil), do: nil + def parse_int_range(doc) do + xmap( + doc, + min: ~x"./tt:Min/text()"so, + max: ~x"./tt:Max/text()"so + ) + end + + def parse_text_color([]), do: nil + def parse_text_color(nil), do: nil + def parse_text_color(doc) do + xmap( + doc, + color: ~x"./tt:Color"eo |> transform_by(&parse_color/1), + transparent: ~x"./tt:Transparent"eo |> transform_by(&parse_int_range/1) + ) + end + + def parse_color([]), do: nil + def parse_color(nil), do: nil + def parse_color(doc) do + xmap( + doc, + color_list: ~x"//@ColorList"so, + color_space_range: ~x"./tt:ColorSpaceRange"eo |> transform_by(&parse_color_space_range/1) + ) + end + + def parse_color_space_range([]), do: nil + def parse_color_space_range(nil), do: nil + def parse_color_space_range(doc) do + xmap( + doc, + x: ~x"./tt:X"eo |> transform_by(&parse_int_range/1), + y: ~x"./tt:Y"eo |> transform_by(&parse_int_range/1), + z: ~x"./tt:Z"eo |> transform_by(&parse_int_range/1), + color_space: ~x"//@ColorSpace"so + ) + end + + def parse_image_option([]), do: nil + def parse_image_option(nil), do: nil + def parse_image_option(doc) do + xmap( + doc, + formats_supported: ~x"./tt:FormatsSupported/text()"so, + max_size: ~x"//@MaxSize"so, + max_width: ~x"//@MaxWidth"so, + max_height: ~x"//@MaxHeight"so, + image_path: ~x"./tt:ImagePath/text()"so + ) + end + + def to_struct(parsed) do + %__MODULE__{} + |> changeset(parsed) + |> apply_action(:validate) + end + + @spec to_json(%Onvif.Media.Ver10.OSDOptions{}) :: + {: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(osd_options, params \\ %{}) do + osd_options + |> cast(params, @required ++ @optional) + |> validate_required(@required) + |> cast_embed(:maximum_number_of_osds, with: &maximum_number_of_osds_changeset/2) + |> cast_embed(:text_option, with: &text_option_changeset/2) + |> cast_embed(:image_option, with: &image_option_changeset/2) + |> validate_subset(:type, ["Image", "Text", "Extended"]) + |> validate_subset(:position_option, ["UpperLeft", "UpperRight", "LowerLeft", "LowerRight", "Custom"]) + end + + def maximum_number_of_osds_changeset(module, attrs) do + cast(module, attrs, [:total, :image, :plaintext, :date, :time, :date_and_time]) + end + + def text_option_changeset(module, attrs) do + cast(module, attrs, [:type, :date_format, :time_format]) + |> cast_embed(:font_size_range, with: &int_range_changeset/2) + |> cast_embed(:font_color, with: &text_color_changeset/2) + |> cast_embed(:background_color, with: &text_color_changeset/2) + end + + def int_range_changeset(module, attrs) do + cast(module, attrs, [:min, :max]) + end + + def text_color_changeset(module, attrs) do + cast(module, attrs, []) + |> cast_embed(:color, with: &color_changeset/2) + |> cast_embed(:transparent, with: &int_range_changeset/2) + end + + def color_changeset(module, attrs) do + cast(module, attrs, [:color_list]) + |> cast_embed(:color_space_range, with: &color_space_range_changeset/2) + end + + def color_space_range_changeset(module, attrs) do + cast(module, attrs, [:color_space]) + |> cast_embed(:x, with: &int_range_changeset/2) + |> cast_embed(:y, with: &int_range_changeset/2) + |> cast_embed(:z, with: &int_range_changeset/2) + end + + def image_option_changeset(module, attrs) do + cast(module, attrs, [:formats_supported, :max_size, :max_width, :max_height, :image_path]) + end + +end diff --git a/test/media/ver10/fixtures/get_osd_options_valid.xml b/test/media/ver10/fixtures/get_osd_options_valid.xml new file mode 100644 index 0000000..e42cda5 --- /dev/null +++ b/test/media/ver10/fixtures/get_osd_options_valid.xml @@ -0,0 +1,87 @@ + + + + + + + Text + UpperLeft + LowerLeft + Custom + + Plain + Date + Time + DateAndTime + + 16 + 64 + + MM/dd/yyyy + dd/MM/yyyy + yyyy/MM/dd + yyyy-MM-dd + hh:mm:ss tt + HH:mm:ss + + + + + 0.000000 + 255.000000 + + + 0.000000 + 255.000000 + + + 0.000000 + 255.000000 + + http://www.onvif.org/ver10/colorspace/YCbCr + + + + + + + + \ No newline at end of file diff --git a/test/media/ver10/get_osd_options_test.exs b/test/media/ver10/get_osd_options_test.exs new file mode 100644 index 0000000..3552bc1 --- /dev/null +++ b/test/media/ver10/get_osd_options_test.exs @@ -0,0 +1,50 @@ +defmodule Onvif.Media.Ver10.GetOSDOptionsTest do + use ExUnit.Case, async: true + + @moduletag capture_log: true + + describe "GetOSDOptions/1" do + test "should get the OSDOptions for the device" do + xml_response = File.read!("test/media/ver10/fixtures/get_osd_options_valid.xml") + + device = Onvif.Factory.device() + + Mimic.expect(Tesla, :request, fn _client, _opts -> + {:ok, %{status: 200, body: xml_response}} + end) + + {:ok, osdoptions} = Onvif.Media.Ver10.GetOSDOptions.request(device, ["token"]) + + assert osdoptions == %Onvif.Media.Ver10.OSDOptions{ + image_option: nil, + maximum_number_of_osds: %Onvif.Media.Ver10.OSDOptions.MaximumNumberOfOSDs{ + date: 1, + date_and_time: 1, + image: 4, + plaintext: 9, + time: 1, + total: 14 + }, + position_option: ["UpperLeft", "LowerLeft", "Custom"], + text_option: %Onvif.Media.Ver10.OSDOptions.TextOption{ + background_color: nil, + date_format: ["MM/dd/yyyy", "dd/MM/yyyy", "yyyy/MM/dd", "yyyy-MM-dd"], + font_color: %Onvif.Media.Ver10.OSDOptions.TextOption.FontColor{ + color: %Onvif.Media.Ver10.OSDOptions.TextOption.FontColor.Color{ + color_list: nil, + color_space_range: nil + }, + transparent: nil + }, + font_size_range: %Onvif.Media.Ver10.OSDOptions.TextOption.FontSizeRange{ + max: 64, + min: 16 + }, + time_format: ["hh:mm:ss tt", "HH:mm:ss"], + type: ["Plain", "Date", "Time", "DateAndTime"] + }, + type: ["Text"] + } + end + end +end