Skip to content

Commit ca4789f

Browse files
authored
Media20: Get video encoder configuration options (#106)
1 parent 8fdfb65 commit ca4789f

5 files changed

+369
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
defmodule Onvif.Media.Ver20.GetVideoEncoderConfigurationOptions do
2+
import SweetXml
3+
import XmlBuilder
4+
5+
require Logger
6+
7+
alias Onvif.Device
8+
alias Onvif.Media.Ver20.VideoEncoderConfigurationOption
9+
10+
@spec soap_action :: String.t()
11+
def soap_action, do: "http://www.onvif.org/ver20/media/wsdl/GetVideoEncoderConfigurationOptions"
12+
13+
@spec request(Device.t(), list) :: {:ok, any} | {:error, map()}
14+
def request(device, args \\ []),
15+
do: Onvif.Media.Ver20.Media.request(device, args, __MODULE__)
16+
17+
def request_body(configuration_token \\ nil, profile_token \\ nil) do
18+
config =
19+
with_configuration_token([], configuration_token) |> with_profile_token(profile_token)
20+
21+
element(:"s:Body", [
22+
element(:"tr2:GetVideoEncoderConfigurationOptions", config)
23+
])
24+
end
25+
26+
@spec response(any) :: {:error, Ecto.Changeset.t()} | {:ok, struct()}
27+
def response(xml_response_body) do
28+
response =
29+
xml_response_body
30+
|> parse(namespace_conformant: true, quiet: true)
31+
|> xpath(
32+
~x"//s:Envelope/s:Body/tr2:GetVideoEncoderConfigurationOptionsResponse/tr2:Options"el
33+
|> add_namespace("s", "http://www.w3.org/2003/05/soap-envelope")
34+
|> add_namespace("tr2", "http://www.onvif.org/ver20/media/wsdl")
35+
|> add_namespace("tt", "http://www.onvif.org/ver10/schema")
36+
)
37+
|> Enum.map(&VideoEncoderConfigurationOption.parse/1)
38+
|> Enum.reduce([], fn raw_config, acc ->
39+
case VideoEncoderConfigurationOption.to_struct(raw_config) do
40+
{:ok, config} ->
41+
[config | acc]
42+
43+
{:error, changeset} ->
44+
Logger.error("Discarding invalid video encoder option config: #{inspect(changeset)}")
45+
acc
46+
end
47+
end)
48+
49+
{:ok, Enum.reverse(response)}
50+
end
51+
52+
defp with_configuration_token(config, nil), do: config
53+
54+
defp with_configuration_token(config, configuration_token) do
55+
[element(:"tr2:ConfigurationToken", configuration_token) | config]
56+
end
57+
58+
defp with_profile_token(config, nil), do: config
59+
60+
defp with_profile_token(config, profile_token) do
61+
[element(:"tr2:ProfileToken", profile_token) | config]
62+
end
63+
end
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
defmodule Onvif.Media.Ver20.VideoEncoderConfigurationOption do
2+
@moduledoc """
3+
Available options for video encoder configuration
4+
"""
5+
6+
use Ecto.Schema
7+
8+
import Ecto.Changeset
9+
import SweetXml
10+
11+
@primary_key false
12+
@derive Jason.Encoder
13+
embedded_schema do
14+
field(:gov_length_range, {:array, :integer})
15+
field(:max_anchor_frame_distance, :integer)
16+
field(:frame_rates_supported, {:array, :float})
17+
field(:profiles_supported, {:array, :string})
18+
field(:constant_bit_rate_supported, :boolean)
19+
field(:guaranteed_frame_rate_supported, :boolean)
20+
field(:encoding, Ecto.Enum, values: [h264: "H264", h265: "H265", jpeg: "JPEG"])
21+
22+
embeds_one :quality_range, QualityRange, primary_key: false, on_replace: :update do
23+
@derive Jason.Encoder
24+
field(:min, :float)
25+
field(:max, :float)
26+
end
27+
28+
embeds_many :resolutions_available, ResolutionsAvailable,
29+
primary_key: false,
30+
on_replace: :delete do
31+
@derive Jason.Encoder
32+
field(:width, :integer)
33+
field(:height, :integer)
34+
end
35+
36+
embeds_one :bitrate_range, BitrateRange, primary_key: false, on_replace: :update do
37+
@derive Jason.Encoder
38+
field(:min, :integer)
39+
field(:max, :integer)
40+
end
41+
end
42+
43+
def parse(doc) do
44+
xmap(
45+
doc,
46+
gov_length_range: ~x"./@GovLengthRange"s |> transform_by(&String.split(&1, " ")),
47+
max_anchor_frame_distance: ~x"./@MaxAnchorFrameDistance"I,
48+
frame_rates_supported: ~x"./@FrameRatesSupported"s |> transform_by(&String.split(&1, " ")),
49+
profiles_supported: ~x"./@ProfilesSupported"s |> transform_by(&String.split(&1, " ")),
50+
constant_bit_rate_supported: ~x"./@ConstantBitRateSupported"s,
51+
guaranteed_frame_rate_supported: ~x"./@GuaranteedFrameRateSupported"s,
52+
encoding: ~x"./tt:Encoding/text()"s,
53+
quality_range: ~x"./tt:QualityRange"e |> transform_by(&parse_float_range/1),
54+
resolutions_available:
55+
~x"./tt:ResolutionsAvailable"el |> transform_by(&parse_resolutions_available/1),
56+
bitrate_range: ~x"./tt:BitrateRange"eo |> transform_by(&parse_int_range/1)
57+
)
58+
end
59+
60+
def changeset(module, attrs) do
61+
module
62+
|> cast(attrs, [
63+
:gov_length_range,
64+
:max_anchor_frame_distance,
65+
:frame_rates_supported,
66+
:profiles_supported,
67+
:constant_bit_rate_supported,
68+
:guaranteed_frame_rate_supported,
69+
:encoding
70+
])
71+
|> cast_embed(:quality_range, with: &range_changeset/2)
72+
|> cast_embed(:resolutions_available, with: &resolutions_available_changeset/2)
73+
|> cast_embed(:bitrate_range, with: &range_changeset/2)
74+
end
75+
76+
def to_struct(parsed) do
77+
%__MODULE__{}
78+
|> changeset(parsed)
79+
|> apply_action(:validate)
80+
end
81+
82+
defp range_changeset(module, attrs) do
83+
cast(module, attrs, [:min, :max])
84+
end
85+
86+
defp resolutions_available_changeset(module, attrs) do
87+
cast(module, attrs, [:width, :height])
88+
end
89+
90+
defp parse_int_range([]) do
91+
nil
92+
end
93+
94+
defp parse_int_range(nil) do
95+
nil
96+
end
97+
98+
defp parse_int_range(doc) do
99+
xmap(
100+
doc,
101+
min: ~x"./tt:Min/text()"i,
102+
max: ~x"./tt:Max/text()"i
103+
)
104+
end
105+
106+
defp parse_float_range(doc) do
107+
xmap(
108+
doc,
109+
min: ~x"./tt:Min/text()"f,
110+
max: ~x"./tt:Max/text()"f
111+
)
112+
end
113+
114+
defp parse_resolutions_available(resolutions) do
115+
Enum.map(resolutions, fn resolution ->
116+
xmap(
117+
resolution,
118+
width: ~x"./tt:Width/text()"i,
119+
height: ~x"./tt:Height/text()"i
120+
)
121+
end)
122+
end
123+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:soapenc="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:tst="http://www.onvif.org/ver10/storage/wsdl" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:dn="http://www.onvif.org/ver10/network/wsdl" xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl" xmlns:wsoap12="http://schemas.xmlsoap.org/wsdl/soap12" xmlns:http="http://schemas.xmlsoap.org/wsdl/http" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:wsadis="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1" xmlns:wsrf-bf="http://docs.oasis-open.org/wsrf/bf-2" xmlns:wsntw="http://docs.oasis-open.org/wsn/bw-2" xmlns:wsrf-rw="http://docs.oasis-open.org/wsrf/rw-2" xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2" xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl" xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:tnshik="http://www.hikvision.com/2011/event/topics" xmlns:hikwsd="http://www.onvifext.com/onvif/ext/ver10/wsdl" xmlns:hikxsd="http://www.onvifext.com/onvif/ext/ver10/schema" xmlns:tas="http://www.onvif.org/ver10/advancedsecurity/wsdl" xmlns:tr2="http://www.onvif.org/ver20/media/wsdl" xmlns:axt="http://www.onvif.org/ver20/analytics">
3+
<env:Body>
4+
<tr2:GetVideoEncoderConfigurationOptionsResponse>
5+
<tr2:Options GovLengthRange="1 250" FrameRatesSupported="12.5 12 10 8 6 4 2 1 0.5 0.25 0.125 0.0625" ProfilesSupported="Main High" ConstantBitRateSupported="true" GuaranteedInstances="1">
6+
<tt:Encoding>H264</tt:Encoding>
7+
<tt:QualityRange>
8+
<tt:Min>0</tt:Min>
9+
<tt:Max>5</tt:Max>
10+
</tt:QualityRange>
11+
<tt:ResolutionsAvailable>
12+
<tt:Width>1280</tt:Width>
13+
<tt:Height>720</tt:Height>
14+
</tt:ResolutionsAvailable>
15+
<tt:ResolutionsAvailable>
16+
<tt:Width>3840</tt:Width>
17+
<tt:Height>2160</tt:Height>
18+
</tt:ResolutionsAvailable>
19+
<tt:BitrateRange>
20+
<tt:Min>32</tt:Min>
21+
<tt:Max>16384</tt:Max>
22+
</tt:BitrateRange>
23+
</tr2:Options>
24+
<tr2:Options GovLengthRange="1 250" FrameRatesSupported="12.5 12 10 8 6 4 2 1 0.5 0.25 0.125 0.0625" ProfilesSupported="Main" ConstantBitRateSupported="true" GuaranteedInstances="1">
25+
<tt:Encoding>H265</tt:Encoding>
26+
<tt:QualityRange>
27+
<tt:Min>0</tt:Min>
28+
<tt:Max>5</tt:Max>
29+
</tt:QualityRange>
30+
<tt:ResolutionsAvailable>
31+
<tt:Width>1920</tt:Width>
32+
<tt:Height>1080</tt:Height>
33+
</tt:ResolutionsAvailable>
34+
<tt:ResolutionsAvailable>
35+
<tt:Width>2560</tt:Width>
36+
<tt:Height>1440</tt:Height>
37+
</tt:ResolutionsAvailable>
38+
<tt:ResolutionsAvailable>
39+
<tt:Width>3072</tt:Width>
40+
<tt:Height>1728</tt:Height>
41+
</tt:ResolutionsAvailable>
42+
<tt:BitrateRange>
43+
<tt:Min>32</tt:Min>
44+
<tt:Max>16384</tt:Max>
45+
</tt:BitrateRange>
46+
</tr2:Options>
47+
</tr2:GetVideoEncoderConfigurationOptionsResponse>
48+
</env:Body>
49+
</env:Envelope>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
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: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: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:wsa5="http://www.w3.org/2005/08/addressing" xmlns:xmime="http://tempuri.org/xmime.xsd" xmlns:xop="http://www.w3.org/2004/08/xop/include" xmlns:ns1="http://www.onvif.org/ver20/analytics/humanface" xmlns:ns2="http://www.onvif.org/ver20/analytics/humanbody" 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:acert="http://www.axis.com/vapix/ws/cert" xmlns:wsrfr="http://docs.oasis-open.org/wsrf/r-2" xmlns:aa="http://www.axis.com/vapix/ws/action1" xmlns:acertificates="http://www.axis.com/vapix/ws/certificates" xmlns:aentry="http://www.axis.com/vapix/ws/entry" xmlns:aev="http://www.axis.com/vapix/ws/event1" xmlns:aeva="http://www.axis.com/vapix/ws/embeddedvideoanalytics1" xmlns:ali1="http://www.axis.com/vapix/ws/light/CommonBinding" xmlns:ali2="http://www.axis.com/vapix/ws/light/IntensityBinding" xmlns:ali3="http://www.axis.com/vapix/ws/light/AngleOfIlluminationBinding" xmlns:ali4="http://www.axis.com/vapix/ws/light/DayNightSynchronizeBinding" xmlns:ali="http://www.axis.com/vapix/ws/light" xmlns:apc="http://www.axis.com/vapix/ws/panopsiscalibration1" xmlns:arth="http://www.axis.com/vapix/ws/recordedtour1" xmlns:asd="http://www.axis.com/vapix/ws/shockdetection" xmlns:aweb="http://www.axis.com/vapix/ws/webserver" xmlns:tan1="http://www.onvif.org/ver20/analytics/wsdl/RuleEngineBinding" xmlns:tan2="http://www.onvif.org/ver20/analytics/wsdl/AnalyticsEngineBinding" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tev1="http://www.onvif.org/ver10/events/wsdl/NotificationProducerBinding" xmlns:tev2="http://www.onvif.org/ver10/events/wsdl/EventBinding" xmlns:tev3="http://www.onvif.org/ver10/events/wsdl/SubscriptionManagerBinding" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:tev4="http://www.onvif.org/ver10/events/wsdl/PullPointSubscriptionBinding" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:timg="http://www.onvif.org/ver20/imaging/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:tse="http://www.onvif.org/ver10/search/wsdl" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tnsaxis="http://www.axis.com/2009/event/topics"><SOAP-ENV:Header></SOAP-ENV:Header><SOAP-ENV:Body><SOAP-ENV:Fault><SOAP-ENV:Code><SOAP-ENV:Value>SOAP-ENV:Receiver</SOAP-ENV:Value><SOAP-ENV:Subcode><SOAP-ENV:Value>ter:ActionNotSupported</SOAP-ENV:Value></SOAP-ENV:Subcode></SOAP-ENV:Code><SOAP-ENV:Reason><SOAP-ENV:Text xml:lang="en">Optional action not implemented</SOAP-ENV:Text></SOAP-ENV:Reason><SOAP-ENV:Detail><SOAP-ENV:Text>The requested action is optional and is not implemented</SOAP-ENV:Text></SOAP-ENV:Detail></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
defmodule Onvif.Media.Ver20.GetVideoEncoderConfigurationOptionsTest do
2+
use ExUnit.Case, async: true
3+
4+
@moduletag capture_log: true
5+
6+
alias Onvif.Media.Ver20.VideoEncoderConfigurationOption
7+
8+
describe "GetVideoEncoderConfigurationOptions/1" do
9+
test "should parse with correct values" do
10+
xml_response =
11+
File.read!("test/media/ver20/fixtures/get_video_encoder_configuration_options.xml")
12+
13+
device = Onvif.Factory.device()
14+
15+
Mimic.expect(Tesla, :request, fn _client, _opts ->
16+
{:ok, %{status: 200, body: xml_response}}
17+
end)
18+
19+
{:ok, response} = Onvif.Media.Ver20.GetVideoEncoderConfigurationOptions.request(device, [])
20+
21+
assert [
22+
%VideoEncoderConfigurationOption{
23+
bitrate_range: %VideoEncoderConfigurationOption.BitrateRange{
24+
max: 16384,
25+
min: 32
26+
},
27+
constant_bit_rate_supported: true,
28+
encoding: :h264,
29+
frame_rates_supported: [
30+
12.5,
31+
12.0,
32+
10.0,
33+
8.0,
34+
6.0,
35+
4.0,
36+
2.0,
37+
1.0,
38+
0.5,
39+
0.25,
40+
0.125,
41+
0.0625
42+
],
43+
gov_length_range: [1, 250],
44+
guaranteed_frame_rate_supported: nil,
45+
max_anchor_frame_distance: 0,
46+
profiles_supported: ["Main", "High"],
47+
quality_range: %VideoEncoderConfigurationOption.QualityRange{
48+
max: 5,
49+
min: 0
50+
},
51+
resolutions_available: [
52+
%VideoEncoderConfigurationOption.ResolutionsAvailable{
53+
height: 720,
54+
width: 1280
55+
},
56+
%VideoEncoderConfigurationOption.ResolutionsAvailable{
57+
height: 2160,
58+
width: 3840
59+
}
60+
]
61+
},
62+
%VideoEncoderConfigurationOption{
63+
bitrate_range: %VideoEncoderConfigurationOption.BitrateRange{
64+
max: 16384,
65+
min: 32
66+
},
67+
constant_bit_rate_supported: true,
68+
encoding: :h265,
69+
frame_rates_supported: [
70+
12.5,
71+
12.0,
72+
10.0,
73+
8.0,
74+
6.0,
75+
4.0,
76+
2.0,
77+
1.0,
78+
0.5,
79+
0.25,
80+
0.125,
81+
0.0625
82+
],
83+
gov_length_range: [1, 250],
84+
guaranteed_frame_rate_supported: nil,
85+
max_anchor_frame_distance: 0,
86+
profiles_supported: ["Main"],
87+
quality_range: %VideoEncoderConfigurationOption.QualityRange{
88+
max: 5,
89+
min: 0
90+
},
91+
resolutions_available: [
92+
%VideoEncoderConfigurationOption.ResolutionsAvailable{
93+
height: 1080,
94+
width: 1920
95+
},
96+
%VideoEncoderConfigurationOption.ResolutionsAvailable{
97+
height: 1440,
98+
width: 2560
99+
},
100+
%VideoEncoderConfigurationOption.ResolutionsAvailable{
101+
height: 1728,
102+
width: 3072
103+
}
104+
]
105+
}
106+
] == response
107+
end
108+
109+
test "should return error when response is invalid" do
110+
xml_response =
111+
File.read!(
112+
"test/media/ver20/fixtures/invalid_get_video_encoder_configuration_options.xml"
113+
)
114+
115+
device = Onvif.Factory.device()
116+
117+
Mimic.expect(Tesla, :request, fn _client, _opts ->
118+
{:ok, %{status: 400, body: xml_response}}
119+
end)
120+
121+
{:error, %{status: status, reason: reason, response: response}} =
122+
Onvif.Media.Ver20.GetVideoEncoderConfigurationOptions.request(device)
123+
124+
assert status == 400
125+
126+
assert reason ==
127+
"Received 400 from Elixir.Onvif.Media.Ver20.GetVideoEncoderConfigurationOptions"
128+
129+
assert response == xml_response
130+
end
131+
end
132+
end

0 commit comments

Comments
 (0)