Skip to content

Commit 4ec0161

Browse files
authored
Search: get recording search results (#113)
1 parent df0db71 commit 4ec0161

File tree

6 files changed

+422
-0
lines changed

6 files changed

+422
-0
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
defmodule Onvif.Search.GetRecordingSearchResults do
2+
@moduledoc """
3+
GetRecordingSearchResults acquires the results from a recording search session previously initiated
4+
by a `Onvif.Search.FindRecordings.request/2` operation. The response shall not include results already
5+
returned in previous requests for the same session.
6+
7+
If MaxResults is specified, the response shall not contain more than MaxResults results.
8+
The number of results relates to the number of recordings. For viewing individual recorded data
9+
for a signal track use the FindEvents method.
10+
11+
GetRecordingSearchResults shall block until:
12+
* MaxResults results are available for the response if MaxResults is specified.
13+
* MinResults results are available for the response if MinResults is specified.
14+
* WaitTime has expired.
15+
* Search is completed or stopped.
16+
"""
17+
18+
import SweetXml
19+
import XmlBuilder
20+
21+
require Logger
22+
23+
alias Onvif.Search.Schemas.{FindRecordingResult, GetRecordingSearchResults}
24+
25+
def soap_action, do: "http://www.onvif.org/ver10/search/wsdl/GetRecordingSearchResults"
26+
27+
@spec request(Onvif.Device.t(), GetRecordingSearchResults.t()) :: any()
28+
def request(device, args) do
29+
Onvif.Search.request(device, args, __MODULE__)
30+
end
31+
32+
def request_body(%GetRecordingSearchResults{} = find_recordings) do
33+
element(:"s:Body", [GetRecordingSearchResults.to_xml(find_recordings)])
34+
end
35+
36+
def response(xml_response_body) do
37+
xml_response_body
38+
|> parse(namespace_conformant: true, quiet: true)
39+
|> xpath(
40+
~x"//tse:ResultList"e
41+
|> add_namespace("s", "http://www.w3.org/2003/05/soap-envelope")
42+
|> add_namespace("tse", "http://www.onvif.org/ver10/search/wsdl")
43+
|> add_namespace("tt", "http://www.onvif.org/ver10/schema")
44+
)
45+
|> FindRecordingResult.parse()
46+
|> FindRecordingResult.to_struct()
47+
end
48+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
defmodule Onvif.Search.Schemas.FindRecordingResult do
2+
@moduledoc """
3+
A module describing results from `Onvif.Search.GetRecordingSearchResults.request/2`.
4+
"""
5+
6+
use Ecto.Schema
7+
8+
import Ecto.Changeset
9+
import SweetXml
10+
11+
alias Onvif.Search.Schemas.RecordingInformation
12+
13+
@type t :: %__MODULE__{}
14+
15+
@primary_key false
16+
@derive Jason.Encoder
17+
embedded_schema do
18+
field(:search_state, Ecto.Enum,
19+
values: [
20+
queued: "Queued",
21+
searching: "Searching",
22+
completed: "Completed",
23+
unknown: "Unknown"
24+
]
25+
)
26+
27+
embeds_many(:recording_information, RecordingInformation)
28+
end
29+
30+
def parse(doc) do
31+
xmap(
32+
doc,
33+
search_state: ~x"./tt:SearchState/text()"s,
34+
recording_information:
35+
~x"./tt:RecordingInformation"el |> transform_by(&RecordingInformation.parse/1)
36+
)
37+
end
38+
39+
@spec to_struct(map()) :: {:error, Ecto.Changeset.t()} | {:ok, __MODULE__.t()}
40+
def to_struct(parsed) do
41+
%__MODULE__{}
42+
|> changeset(parsed)
43+
|> apply_action(:validate)
44+
end
45+
46+
@spec to_json(__MODULE__.t()) :: {:error, Jason.EncodeError.t() | Exception.t()} | {:ok, binary}
47+
def to_json(%__MODULE__{} = schema) do
48+
Jason.encode(schema)
49+
end
50+
51+
defp changeset(module, attrs) do
52+
module
53+
|> cast(attrs, [:search_state])
54+
|> cast_embed(:recording_information)
55+
end
56+
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
defmodule Onvif.Search.Schemas.GetRecordingSearchResults do
2+
@moduledoc """
3+
Module describing the request to `Onvif.Search.GetRecordingSearchResults`.
4+
"""
5+
6+
use Ecto.Schema
7+
8+
import Ecto.Changeset
9+
import XmlBuilder
10+
11+
@type t :: %__MODULE__{}
12+
13+
@primary_key false
14+
@derive Jason.Encoder
15+
embedded_schema do
16+
field(:search_token, :string)
17+
field(:min_results, :integer)
18+
field(:max_results, :integer)
19+
field(:wait_time, :integer)
20+
end
21+
22+
def to_struct(parsed) do
23+
%__MODULE__{}
24+
|> changeset(parsed)
25+
|> apply_action(:validate)
26+
end
27+
28+
@spec to_json(__MODULE__.t()) :: {:error, Jason.EncodeError.t() | Exception.t()} | {:ok, binary}
29+
def to_json(%__MODULE__{} = schema) do
30+
Jason.encode(schema)
31+
end
32+
33+
def to_xml(%__MODULE__{} = schema) do
34+
element(
35+
:"tse:GetRecordingSearchResults",
36+
[element(:"tse:SearchToken", schema.search_token)]
37+
|> xml_min_results(schema.min_results)
38+
|> xml_max_results(schema.max_results)
39+
|> xml_wait_time(schema.wait_time)
40+
)
41+
end
42+
43+
def changeset(module, attrs) do
44+
module
45+
|> cast(attrs, __MODULE__.__schema__(:fields))
46+
|> validate_required([:search_token])
47+
end
48+
49+
defp xml_max_results(body, nil), do: body
50+
51+
defp xml_max_results(body, max_results) do
52+
[element(:"tse:MaxResults", max_results) | body]
53+
end
54+
55+
defp xml_min_results(body, nil), do: body
56+
57+
defp xml_min_results(body, min_results) do
58+
[element(:"tse:MinResults", min_results) | body]
59+
end
60+
61+
defp xml_wait_time(body, nil), do: body
62+
63+
defp xml_wait_time(body, time) do
64+
[element(:"tse:WaitTime", "PT#{time}S") | body]
65+
end
66+
end
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
defmodule Onvif.Search.Schemas.RecordingInformation do
2+
@moduledoc """
3+
A module describing recording information.
4+
"""
5+
6+
use Ecto.Schema
7+
8+
import Ecto.Changeset
9+
import SweetXml
10+
11+
@type t :: %__MODULE__{}
12+
13+
@primary_key false
14+
@derive Jason.Encoder
15+
embedded_schema do
16+
field(:recording_token, :string)
17+
18+
embeds_one :source, RecordingSourceInformation, primary_key: false do
19+
@derive Jason.Encoder
20+
field(:source_id, :string)
21+
field(:name, :string)
22+
field(:location, :string)
23+
field(:description, :string)
24+
field(:address, :string)
25+
end
26+
27+
field(:earliest_recording, :utc_datetime)
28+
field(:latest_recording, :utc_datetime)
29+
field(:content, :string)
30+
31+
embeds_many :tracks, TrackInformation, primary_key: false, on_replace: :delete do
32+
@derive Jason.Encoder
33+
field(:track_token, :string)
34+
35+
field(:track_type, Ecto.Enum, values: [audio: "Audio", video: "Video", metadata: "Metadata"])
36+
37+
field(:description, :string)
38+
field(:data_from, :utc_datetime)
39+
field(:data_to, :utc_datetime)
40+
end
41+
42+
field(:recording_status, Ecto.Enum,
43+
values: [
44+
initiated: "Initiated",
45+
recording: "Recording",
46+
stopped: "Stopped",
47+
removing: "Removing",
48+
removed: "Removed",
49+
unknown: "Unknown"
50+
]
51+
)
52+
end
53+
54+
def parse(docs) do
55+
docs
56+
|> List.wrap()
57+
|> Enum.map(fn doc ->
58+
xmap(
59+
doc,
60+
recording_token: ~x"./tt:RecordingToken/text()"s,
61+
source: [
62+
~x"./tt:Source"e,
63+
source_id: ~x"./tt:SourceId/text()"s,
64+
name: ~x"./tt:Name/text()"s,
65+
location: ~x"./tt:Location/text()"s,
66+
description: ~x"./tt:Description/text()"s,
67+
address: ~x"./tt:Address/text()"s
68+
],
69+
earliest_recording: ~x"./tt:EarliestRecording/text()"s,
70+
latest_recording: ~x"./tt:LatestRecording/text()"s,
71+
content: ~x"./tt:Content/text()"s,
72+
recording_status: ~x"./tt:RecordingStatus/text()"s,
73+
tracks: ~x"./tt:Track"el |> transform_by(&parse_track_information/1)
74+
)
75+
end)
76+
end
77+
78+
def to_struct(parsed) do
79+
%__MODULE__{}
80+
|> changeset(parsed)
81+
|> apply_action(:validate)
82+
end
83+
84+
@spec to_json(__MODULE__.t()) :: {:error, Jason.EncodeError.t() | Exception.t()} | {:ok, binary}
85+
def to_json(%__MODULE__{} = schema) do
86+
Jason.encode(schema)
87+
end
88+
89+
def changeset(module, attrs) do
90+
module
91+
|> cast(attrs, [
92+
:recording_token,
93+
:earliest_recording,
94+
:latest_recording,
95+
:content,
96+
:recording_status
97+
])
98+
|> cast_embed(:source, with: &recording_source_changeset/2)
99+
|> cast_embed(:tracks, with: &track_information_changeset/2)
100+
end
101+
102+
defp parse_track_information(docs) do
103+
docs
104+
|> List.wrap()
105+
|> Enum.map(fn doc ->
106+
xmap(
107+
doc,
108+
track_token: ~x"./tt:TrackToken/text()"s,
109+
track_type: ~x"./tt:TrackType/text()"s,
110+
description: ~x"./tt:Description/text()"s,
111+
data_from: ~x"./tt:DataFrom/text()"s,
112+
data_to: ~x"./tt:DataTo/text()"s
113+
)
114+
end)
115+
end
116+
117+
defp recording_source_changeset(module, attrs) do
118+
cast(module, attrs, [
119+
:source_id,
120+
:name,
121+
:location,
122+
:description,
123+
:address
124+
])
125+
end
126+
127+
defp track_information_changeset(module, attrs) do
128+
cast(module, attrs, [
129+
:track_token,
130+
:track_type,
131+
:description,
132+
:data_from,
133+
:data_to
134+
])
135+
end
136+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsdd="http://www.w3.org/2005/04/discovery" xmlns:chan="http://schemas.microsoft.com/ws/2005/02/duplex" xmlns:wsa5="http://www.w3.org/2005/08/addressing" xmlns:c14n="http://www.w3.org/2001/10/xml-exc-c14n#" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:wsc="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:xmime="http://tempuri.org/xmime.xsd" xmlns:xop="http://www.w3.org/2004/08/xop/include" xmlns:ns2="http://www.onvif.org/ver10/pacs" xmlns:wsrfbf="http://docs.oasis-open.org/wsrf/bf-2" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:wsrfr="http://docs.oasis-open.org/wsrf/r-2" xmlns:ns1="http://www.onvif.org/ver10/accesscontrol/wsdl" xmlns:ns3="http://www.onvif.org/ver10/actionengine/wsdl" xmlns:ns4="http://www.onvif.org/ver10/advancedsecurity/wsdl" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:tdn="http://www.onvif.org/ver10/network/wsdl" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tls="http://www.onvif.org/ver10/display/wsdl" xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" xmlns:tr2="http://www.onvif.org/ver20/media/wsdl" xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:trv="http://www.onvif.org/ver10/receiver/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:tns1="http://www.onvif.org/ver10/topics">
3+
<SOAP-ENV:Header></SOAP-ENV:Header>
4+
<SOAP-ENV:Body>
5+
<tse:GetRecordingSearchResultsResponse>
6+
<tse:ResultList>
7+
<tt:SearchState>Completed</tt:SearchState>
8+
<tt:RecordingInformation>
9+
<tt:RecordingToken>OnvifRecordingToken_1</tt:RecordingToken>
10+
<tt:Source>
11+
<tt:SourceId>SourceId_1</tt:SourceId>
12+
<tt:Name>IpCamera_1</tt:Name>
13+
<tt:Location>Location</tt:Location>
14+
<tt:Description>Description of source</tt:Description>
15+
<tt:Address>http://www.onvif.org/ver10/schema/Profile</tt:Address>
16+
</tt:Source>
17+
<tt:EarliestRecording>1970-01-01T00:03:15Z</tt:EarliestRecording>
18+
<tt:LatestRecording>2025-03-15T16:28:00Z</tt:LatestRecording>
19+
<tt:Content>RecordContent</tt:Content>
20+
<tt:Track>
21+
<tt:TrackToken>videotracktoken_1</tt:TrackToken>
22+
<tt:TrackType>Video</tt:TrackType>
23+
<tt:Description>VideoTrack</tt:Description>
24+
<tt:DataFrom>1970-01-01T00:03:15Z</tt:DataFrom>
25+
<tt:DataTo>2025-03-15T16:28:00Z</tt:DataTo>
26+
</tt:Track>
27+
<tt:Track>
28+
<tt:TrackToken>audiotracktoken_1</tt:TrackToken>
29+
<tt:TrackType>Audio</tt:TrackType>
30+
<tt:Description>AudioTrack</tt:Description>
31+
<tt:DataFrom>1970-01-01T00:03:15Z</tt:DataFrom>
32+
<tt:DataTo>2025-03-15T16:28:00Z</tt:DataTo>
33+
</tt:Track>
34+
<tt:Track>
35+
<tt:TrackToken>metadatatracktoken_1</tt:TrackToken>
36+
<tt:TrackType>Metadata</tt:TrackType>
37+
<tt:Description>MetadataTrack</tt:Description>
38+
<tt:DataFrom>1970-01-01T00:03:15Z</tt:DataFrom>
39+
<tt:DataTo>2025-03-15T16:28:00Z</tt:DataTo>
40+
</tt:Track>
41+
<tt:RecordingStatus>Stopped</tt:RecordingStatus>
42+
</tt:RecordingInformation>
43+
</tse:ResultList>
44+
</tse:GetRecordingSearchResultsResponse>
45+
</SOAP-ENV:Body>
46+
</SOAP-ENV:Envelope>

0 commit comments

Comments
 (0)