Skip to content

Commit

Permalink
ptz absolute-move cli command params better match the Onvif spec
Browse files Browse the repository at this point in the history
  • Loading branch information
faithoflifedev committed Feb 1, 2024
1 parent 4d3f1b3 commit bc743e4
Show file tree
Hide file tree
Showing 20 changed files with 420 additions and 644 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 2.2.0+2

* ptz absolute-move cli command params better match the Onvif spec

## 2.2.0+1

* new cli feature `onvif debug` generates a debug.zip with request/response for most common device features
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ To use this package in your code, first add the dependency to your project:
```yml
dependencies:
...
easy_onvif: ^2.2.0+1
easy_onvif: ^2.2.0+2
```
If you need additional help getting started with dart, check out these [guides](https://dart.dev/guides).
Expand Down
2 changes: 1 addition & 1 deletion lib/meta.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ library meta;
import 'dart:convert' show json;

final pubSpec = json.decode(
'{"name":"easy_onvif","version":"2.2.0+1","homepage":"https://github.com/faithoflifedev/easy_onvif","environment":{"sdk":">=3.2.0 <4.0.0"},"description":"A pure Dart library designed primarily for command line automation of Onvif compatible devices, but can be used anywhere Dart is used.","dependencies":{"archive":"^3.4.10","args":"^2.4.2","cli_spin":"^1.0.1","convert":"^3.1.1","crypto":"^3.0.3","dio":"^5.4.0","ffi":"^2.1.0","html_unescape":"^2.0.0","intl":"^0.19.0","json_annotation":"^4.8.1","loggy":"^2.0.3","path":"^1.8.3","shelf":"^1.4.1","shelf_router":"^1.1.4","sprintf":"^7.0.0","universal_io":"^2.2.2","uuid":"^4.3.3","xml":"^6.5.0","xml2json":"^6.2.2","yaml":"^3.1.2"},"dev_dependencies":{"build_runner":"^2.4.8","grinder":"^0.9.5","json_serializable":"^6.7.1","lints":"^3.0.0","mustache_template":"^2.0.0","process_run":"^0.14.0+1","pub_semver":"^2.1.4","publish_tools":"^0.1.0+15","pubspec":"^2.3.0","test":"^1.25.1"},"executables":{"onvif":""},"repository":"https://github.com/faithoflifedev/easy_onvif"}');
'{"name":"easy_onvif","version":"2.2.0+2","homepage":"https://github.com/faithoflifedev/easy_onvif","environment":{"sdk":">=3.2.0 <4.0.0"},"description":"A pure Dart library designed primarily for command line automation of Onvif compatible devices, but can be used anywhere Dart is used.","dependencies":{"archive":"^3.4.10","args":"^2.4.2","cli_spin":"^1.0.1","convert":"^3.1.1","crypto":"^3.0.3","dio":"^5.4.0","ffi":"^2.1.0","html_unescape":"^2.0.0","intl":"^0.19.0","json_annotation":"^4.8.1","loggy":"^2.0.3","path":"^1.8.3","shelf":"^1.4.1","shelf_router":"^1.1.4","sprintf":"^7.0.0","universal_io":"^2.2.2","uuid":"^4.3.3","xml":"^6.5.0","xml2json":"^6.2.2","yaml":"^3.1.2"},"dev_dependencies":{"build_runner":"^2.4.8","grinder":"^0.9.5","json_serializable":"^6.7.1","lints":"^3.0.0","mustache_template":"^2.0.0","process_run":"^0.14.1+3","pub_semver":"^2.1.4","publish_tools":"^0.1.0+15","pubspec":"^2.3.0","test":"^1.25.1"},"executables":{"onvif":""},"repository":"https://github.com/faithoflifedev/easy_onvif"}');
36 changes: 30 additions & 6 deletions lib/src/cmd/onvif_ptz_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,31 +66,55 @@ class OnvifAbsoluteMovePtzCommand extends OnvifHelperCommand {
help:
'The ProfileToken element indicates the media profile to use and will define the source and dimensions of the snapshot.')
..addOption('pan-tilt-x',
mandatory: true,
valueHelp: 'double',
help:
'A Position vector specifying the absolute target position x-axis.')
..addOption('pan-tilt-y',
mandatory: true,
valueHelp: 'double',
help:
'A Position vector specifying the absolute target position y-axis.')
..addOption('pan-tilt-zoom',
mandatory: true,
valueHelp: 'double',
help:
'A Position vector specifying the absolute target position zoom.');
}

@override
void run() async {
if (argResults?['pan-tilt-x'] == null &&
argResults?['pan-tilt-y'] == null &&
argResults?['pan-tilt-zoom'] == null) {
throw UsageException('API usage error:',
'Either pan-tilt (both x and y values) or pan-tilt-zoom must be specified.');
} else if ((argResults?['pan-tilt-x'] != null &&
argResults?['pan-tilt-y'] == null) ||
(argResults?['pan-tilt-x'] == null &&
argResults?['pan-tilt-y'] != null)) {
throw UsageException('API usage error:',
'When using pan-tilt, both pan-tilt-x or pan-tilt-y must be specified.');
}

PanTilt? panTilt;

Zoom? zoom;

if (argResults?['pan-tilt-x'] != null &&
argResults?['pan-tilt-y'] != null) {
panTilt = PanTilt.fromString(
x: argResults!['pan-tilt-x'], y: argResults!['pan-tilt-y']);
}

if (argResults?['pan-tilt-zoom'] != null) {
zoom = Zoom.fromString(x: argResults!['pan-tilt-zoom']);
}

await initializeOnvif();

try {
final place = PtzPosition(
panTilt: PanTilt.fromString(
x: argResults!['pan-tilt-x'], y: argResults!['pan-tilt-y']),
zoom: Zoom.fromString(x: argResults!['pan-tilt-zoom']));
panTilt: panTilt,
zoom: zoom,
);

await ptz.absoluteMove(argResults!['profile-token'], place);
} on DioException catch (err) {
Expand Down
4 changes: 2 additions & 2 deletions lib/src/model/envelope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ class Envelope {
factory Envelope.fromJson(Map<String, dynamic> json) =>
_$EnvelopeFromJson(json);

factory Envelope.fromXml(String xml) =>
factory Envelope.fromXmlString(String xml) =>
Envelope.fromJson(OnvifUtil.xmlToMap(xml));

factory Envelope.fromXmlFile(String fileNameAndPath) =>
Envelope.fromXml(File(fileNameAndPath).readAsStringSync());
Envelope.fromXmlString(File(fileNameAndPath).readAsStringSync());
}
18 changes: 18 additions & 0 deletions lib/src/model/header.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import 'dart:convert';

import 'package:easy_onvif/soap.dart';
import 'package:easy_onvif/src/model/probe/app_sequence.dart';
import 'package:easy_onvif/src/util/util.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:xml/xml.dart';

import 'security.dart';

part 'header.g.dart';

@JsonSerializable()
class Header {
@JsonKey(name: 'Security')
final Security? security;

@JsonKey(name: 'AppSequence')
final AppSequence? appSequence;

Expand All @@ -24,6 +31,7 @@ class Header {
final String? action;

Header({
this.security,
this.appSequence,
this.messageID,
this.relatesTo,
Expand All @@ -35,6 +43,16 @@ class Header {

Map<String, dynamic> toJson() => _$HeaderToJson(this);

XmlDocumentFragment toXml(XmlBuilder builder, Authorization authorization) {
builder.element('Header', namespace: Xmlns.s, nest: () {
if (security != null) {
security!.toXml(builder, authorization);
}
});

return builder.buildFragment();
}

@override
String toString() => json.encode(toJson());
}
22 changes: 22 additions & 0 deletions lib/src/model/probe/app_sequence.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:convert';

import 'package:easy_onvif/src/soap/xmlns.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:xml/xml.dart';

part 'app_sequence.g.dart';

Expand All @@ -19,6 +21,26 @@ class AppSequence {

Map<String, dynamic> toJson() => _$AppSequenceToJson(this);

XmlDocumentFragment toXml(XmlBuilder builder) {
builder.element(
'AppSequence',
namespaces: {Xmlns.ws: 'ws'},
nest: () {
builder.attribute(
'MessageNumber',
messageNumber,
);

builder.attribute(
'InstanceId',
instanceId,
);
},
);

return builder.buildFragment();
}

@override
String toString() => json.encode(toJson());
}
39 changes: 39 additions & 0 deletions lib/src/model/security.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:easy_onvif/src/soap/authorization.dart';
import 'package:easy_onvif/src/soap/xmlns.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:xml/xml.dart';

import 'username_token.dart';

class Security {
@JsonKey(name: 'UsernameToken')
final UsernameToken usernameToken;

final int mustUnderstand;

Security({
required this.usernameToken,
required this.mustUnderstand,
});

XmlDocumentFragment toXml(XmlBuilder builder, Authorization authorization) {
builder.element(
'Security',
namespaces: {Xmlns.s: 's'},
nest: () {
builder.namespace(
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd');

builder.attribute(
'mustUnderstand',
mustUnderstand,
namespace: Xmlns.s,
);

usernameToken.toXml(builder, authorization);
},
);

return builder.buildFragment();
}
}
31 changes: 31 additions & 0 deletions lib/src/model/username_token.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:easy_onvif/src/soap/authorization.dart';
import 'package:xml/xml.dart';

class UsernameToken {
Authorization? authorization;

XmlDocumentFragment toXml(XmlBuilder builder, Authorization authorization) {
builder.element('UsernameToken', nest: () {
builder.element('Username', nest: authorization.authInfo.username);
builder.element('Password',
attributes: {
'Type':
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest'
},
nest: authorization.digest);
builder.element('Nonce',
attributes: {
'EncodingType':
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary'
},
nest: authorization.nonce.toBase64());
builder.element('Created', nest: () {
builder.namespace(
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd');
builder.text(authorization.utcTimeStamp);
});
});

return builder.buildFragment();
}
}
4 changes: 2 additions & 2 deletions lib/src/multicast_probe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class MulticastProbe with UiLoggy {

loggy.debug('RESPONSE:\n$messageReceived');

var envelope = Envelope.fromXml(messageReceived);
var envelope = Envelope.fromXmlString(messageReceived);

if (envelope.body.response == null) throw Exception();

Expand Down Expand Up @@ -160,7 +160,7 @@ class MulticastProbe with UiLoggy {
final devices = _discovery(data, probeMessageData, duration.inMilliseconds);

for (var index = 0; index < devices; index++) {
var envelope = Envelope.fromXml(
var envelope = Envelope.fromXmlString(
Pointer.fromAddress(data.address + index * 8192)
.cast<Utf8>()
.toDartString());
Expand Down
13 changes: 8 additions & 5 deletions lib/src/soap/authorization.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:convert';

import 'package:crypto/crypto.dart';
import 'package:easy_onvif/onvif.dart';
import 'package:easy_onvif/soap.dart';
import 'package:intl/intl.dart';

Expand All @@ -9,23 +10,25 @@ import 'package:intl/intl.dart';
class Authorization {
late final Nonce nonce;

final Nonce? nonceOverride;

final String password;
final AuthInfo authInfo;

final DateTime timeStamp;

final Duration timeDelta;

final Nonce? nonceOverride;

String get utcTimeStamp => DateFormat('yyyy-MM-DD\'T\'HH:mm:ss\'Z\'')
.format(timeStamp.add(timeDelta));

String get digest => base64.encode(sha1
.convert(nonce.bytes + utf8.encode(utcTimeStamp) + utf8.encode(password))
.convert(nonce.bytes +
utf8.encode(utcTimeStamp) +
utf8.encode(authInfo.password))
.bytes);

Authorization({
required this.password,
required this.authInfo,
required this.timeStamp,
required this.timeDelta,
this.nonceOverride,
Expand Down
14 changes: 9 additions & 5 deletions lib/src/soap/ptz.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ class PtzRequest {
});
Transport.builder.element('Position', nest: () {
Transport.builder.namespace(Xmlns.tptz);
Transport.builder.element('PanTilt', nest: () {
Transport.builder.namespace(Xmlns.tt);
Transport.builder.attribute('x', place.panTilt!.x);
Transport.builder.attribute('y', place.panTilt!.y);
});
if (place.panTilt != null) {
Transport.builder.element('PanTilt', nest: () {
Transport.builder.namespace(Xmlns.tt);
Transport.builder.attribute('x', place.panTilt!.x);
Transport.builder.attribute('y', place.panTilt!.y);
});
}

if (place.zoom != null) {
Transport.builder.element('Zoom', nest: () {
Transport.builder.namespace(Xmlns.tt);
Expand All @@ -38,6 +41,7 @@ class PtzRequest {
Transport.builder.attribute('y', speed.panTilt!.y);
});
}

if (speed.zoom != null) {
Transport.builder.element('Zoom', nest: () {
Transport.builder.namespace(Xmlns.tt);
Expand Down
4 changes: 2 additions & 2 deletions lib/src/soap/transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Transport with UiLoggy {
throw Exception(error);
}

return Envelope.fromXml(response.data);
return Envelope.fromXmlString(response.data);
}

static XmlDocumentFragment quickTag(
Expand All @@ -69,7 +69,7 @@ class Transport with UiLoggy {
Authorization? authorization,
}) {
authorization ??= Authorization(
password: authInfo.password,
authInfo: authInfo,
timeStamp: DateTime.now(),
timeDelta: timeDelta,
);
Expand Down
2 changes: 2 additions & 0 deletions lib/src/soap/xmlns.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class Xmlns {
static const a = 'http://schemas.xmlsoap.org/ws/2004/08/addressing';
static const dn = 'http://www.onvif.org/ver10/network/wsdl';
static const s = 'http://www.w3.org/2003/05/soap-envelope';
static const tds = 'http://www.onvif.org/ver10/device/wsdl';
Expand All @@ -12,5 +13,6 @@ class Xmlns {
static const trp = 'http://www.onvif.org/ver10/replay/wsdl';
static const tse = 'http://www.onvif.org/ver10/search/wsdl';
static const tt = 'http://www.onvif.org/ver10/schema';
static const ws = 'http://schemas.xmlsoap.org/ws/2005/04/discovery';
static const xsd = 'http://www.w3.org/2001/XMLSchema';
}
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: easy_onvif
version: 2.2.0+1
version: 2.2.0+2
homepage: https://github.com/faithoflifedev/easy_onvif
description: A pure Dart library designed primarily for command line automation of Onvif compatible devices, but can be used anywhere Dart is used.
repository: https://github.com/faithoflifedev/easy_onvif
Expand Down Expand Up @@ -33,7 +33,7 @@ dev_dependencies:
json_serializable: ^6.7.1
lints: ^3.0.0
mustache_template: ^2.0.0
process_run: ^0.14.0+1
process_run: ^0.14.1+3
pub_semver: ^2.1.4
publish_tools: ^0.1.0+15
pubspec: ^2.3.0
Expand Down
2 changes: 1 addition & 1 deletion test/authentication_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void main() {
);

final authorization = Authorization(
password: authInfo.password,
authInfo: authInfo,
timeStamp: DateTime(2024, 1, 20, 16, 10, 0),
timeDelta: Duration(seconds: 0),
nonceOverride: Nonce(
Expand Down
Loading

0 comments on commit bc743e4

Please sign in to comment.