diff --git a/lib/recording/create_recording.ex b/lib/recording/create_recording.ex new file mode 100644 index 0000000..3416e23 --- /dev/null +++ b/lib/recording/create_recording.ex @@ -0,0 +1,71 @@ +defmodule Onvif.Recording.CreateRecording do + import SweetXml + import XmlBuilder + require Logger + + def soap_action, do: "http://www.onvif.org/ver10/recording/wsdl/CreateRecording" + + def request(device, args) do + Onvif.Recording.request(device, args, __MODULE__) + end + + def request_body(config: %Onvif.Recording.Recording.Configuration{} = config) do + element(:"s:Body", [ + element(:"trc:CreateRecording", [ + element(:"trc:RecordingConfiguration", [ + element(:"tt:Source", [ + gen_source_id(config.source.source_id), + gen_name(config.source.name), + gen_location(config.source.location), + gen_description(config.source.description), + gen_address(config.source.address) + ]), + gen_content(config.content), + gen_maximum_retention_time(config.maximum_retention_time) + ]) + ]) + ]) + end + + def gen_source_id(nil), do: [] + def gen_source_id(""), do: [] + def gen_source_id(source_id), do: element(:"tt:SourceId", source_id) + + def gen_name(nil), do: [] + def gen_name(""), do: [] + def gen_name(name), do: element(:"tt:Name", name) + + def gen_location(nil), do: [] + def gen_location(""), do: [] + def gen_location(location), do: element(:"tt:Location", location) + + def gen_description(nil), do: [] + def gen_description(""), do: [] + def gen_description(description), do: element(:"tt:Description", description) + + def gen_address(nil), do: [] + def gen_address(""), do: [] + def gen_address(address), do: element(:"tt:Address", address) + + def gen_content(nil), do: [] + def gen_content(""), do: [] + def gen_content(content), do: element(:"tt:Content", content) + + def gen_maximum_retention_time(nil), do: [] + def gen_maximum_retention_time(""), do: [] + def gen_maximum_retention_time(maximum_retention_time), do: element(:"tt:MaximumRetentionTime", maximum_retention_time) + + + def response(xml_response_body) do + recording_token = + xml_response_body + |> parse(namespace_conformant: true, quiet: true) + |> xpath( + ~x"//s:Envelope/s:Body/trc:CreateRecordingResponse/trc:RecordingToken/text()"s + |> add_namespace("s", "http://www.w3.org/2003/05/soap-envelope") + |> add_namespace("trc", "http://www.onvif.org/ver10/recording/wsdl") + ) + + {:ok, recording_token} + end +end diff --git a/lib/recording/recording.ex b/lib/recording/recording.ex new file mode 100644 index 0000000..2fe68a2 --- /dev/null +++ b/lib/recording/recording.ex @@ -0,0 +1,168 @@ +defmodule Onvif.Recording.Recording do + @moduledoc """ + Onvif.Recording.Recordings schema. + """ + + use Ecto.Schema + import Ecto.Changeset + import SweetXml + + @primary_key false + @derive Jason.Encoder + @required [:recording_token] + @optional [] + + embedded_schema do + field(:recording_token, :string) + + embeds_one :configuration, Configuration, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:content, :string) + field(:maximum_retention_time, :string) + + embeds_one(:source, Source, primary_key: false, on_replace: :update) do + @derive Jason.Encoder + field(:source_id, :string) + field(:name, :string) + field(:location, :string) + field(:description, :string) + field(:address, :string) + end + end + + embeds_one(:tracks, Tracks, primary_key: false, on_replace: :update) do + @derive Jason.Encoder + embeds_many(:track, Track, primary_key: false, on_replace: :delete) do + @derive Jason.Encoder + field(:track_token, :string) + + embeds_one(:configuration, Configuration, primary_key: false, on_replace: :update) do + @derive Jason.Encoder + field(:track_type, :string) + field(:description, :string) + end + end + end + end + + def parse(nil), do: nil + def parse([]), do: nil + + def parse(doc) do + xmap( + doc, + recording_token: ~x"./tt:RecordingToken/text()"so, + configuration: ~x"./tt:Configuration"eo |> transform_by(&parse_configuration/1), + tracks: ~x"./tt:Tracks"eo |> transform_by(&parse_tracks/1) + ) + end + + def parse_configuration([]), do: nil + def parse_configuration(nil), do: nil + + def parse_configuration(doc) do + xmap( + doc, + content: ~x"./tt:Content/text()"so, + maximum_retention_time: ~x"./tt:MaximumRetentionTime/text()"so, + source: ~x"./tt:Source"eo |> transform_by(&parse_source/1) + ) + end + + def parse_source([]), do: nil + def parse_source(nil), do: nil + + def parse_source(doc) do + xmap( + doc, + source_id: ~x"./tt:SourceId/text()"so, + name: ~x"./tt:Name/text()"so, + location: ~x"./tt:Location/text()"so, + description: ~x"./tt:Description/text()"so, + address: ~x"./tt:Address/text()"so + ) + end + + def parse_tracks([]), do: nil + def parse_tracks(nil), do: nil + + def parse_tracks(doc) do + xmap( + doc, + track: ~x"./tt:Track"elo |> transform_by(&parse_track/1) + ) + end + + def parse_track([]), do: nil + def parse_track(nil), do: nil + + def parse_track(docs) do + Enum.map(docs, fn doc -> + xmap( + doc, + track_token: ~x"./tt:TrackToken/text()"so, + configuration: ~x"./tt:Configuration"eo |> transform_by(&parse_track_configuration/1) + ) + end) + end + + def parse_track_configuration([]), do: nil + def parse_track_configuration(nil), do: nil + + def parse_track_configuration(doc) do + xmap( + doc, + track_type: ~x"./tt:TrackType/text()"so, + description: ~x"./tt:Description/text()"so + ) + end + + def to_struct(parsed) do + %__MODULE__{} + |> changeset(parsed) + |> apply_action(:validate) + end + + @spec to_json(%Onvif.Recording.Recording{}) :: + {: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(:configuration, with: &configuration_changeset/2) + |> cast_embed(:tracks, with: &tracks_changeset/2) + end + + def configuration_changeset(module, attrs) do + cast(module, attrs, [:content, :maximum_retention_time]) + |> cast_embed(:source, with: &source_changeset/2) + end + + def source_changeset(module, attrs) do + cast(module, attrs, [:source_id, :name, :location, :description, :address]) + end + + def tracks_changeset(module, attrs) do + cast(module, attrs, []) + |> cast_embed(:track, with: &track_changeset/2) + end + + def track_changeset(module, attrs) do + cast(module, attrs, [:track_token]) + |> cast_embed(:configuration, with: &track_configuration_changeset/2) + end + + def track_configuration_changeset(module, attrs) do + cast(module, attrs, [:track_type, :description]) + end +end diff --git a/test/recording/create_recording_test.exs b/test/recording/create_recording_test.exs new file mode 100644 index 0000000..65042cd --- /dev/null +++ b/test/recording/create_recording_test.exs @@ -0,0 +1,29 @@ +defmodule Onvif.Recording.CreateRecordingTest do + use ExUnit.Case, async: true + + @moduletag capture_log: true + + describe "CreateRecording/2" do + test "create a recording" do + xml_response = File.read!("test/recording/fixture/create_recording_success.xml") + + device = Onvif.Factory.device() + + Mimic.expect(Tesla, :request, fn _client, _opts -> + {:ok, %{status: 200, body: xml_response}} + end) + + {:ok, response_uri} = Onvif.Recording.CreateRecording.request( + device, + config: %Onvif.Recording.Recording.Configuration{ + content: "test", + maximum_retention_time: "PT1H", + source: %Onvif.Recording.Recording.Configuration.Source{ + name: "test", + } + }) + + assert response_uri == "SD_DISK_20200422_123501_A2388AB3" + end + end +end diff --git a/test/recording/fixture/create_recording_success.xml b/test/recording/fixture/create_recording_success.xml new file mode 100644 index 0000000..9ed4a7d --- /dev/null +++ b/test/recording/fixture/create_recording_success.xml @@ -0,0 +1,59 @@ + + + + + SD_DISK_20200422_123501_A2388AB3 + + + \ No newline at end of file