├── LICENSE
├── README.md
├── TODO.md
├── device.go
├── device_test.go
├── discovery.go
├── media.go
├── media_test.go
├── model.go
├── soap.go
└── utils.go
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Muhammad Radhi Fadlillah
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Go-ONVIF
2 |
3 | Go-ONVIF is a Go package for communicating with network camera which supports the [ONVIF](http://www.onvif.org/) specifications. ONVIF (Open Network Video Interface) is an open industry forum promoting and developing global standards for interfaces of IP-based physical security products such as network cameras. Recently, almost all network cameras support ONVIF specifications, especially network camera that made in China, which usually can bought with cheap price.
4 |
5 | ## Progress
6 |
7 | This package is still in develoment following [guide](https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_WG-APG-Application_Programmers_Guide-1.pdf) from ONVIF, with several [features](TODO.md) already available.
8 |
9 | ## License
10 |
11 | Go-ONVIF is distributed using [MIT](http://choosealicense.com/licenses/mit/) license, which means you can use it however you want as long as you preserve copyright and license notices of this package.
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | - [X] Camera discovery
2 | - [ ] OnvifServiceDevice
3 | - [X] getInformation
4 | - [ ] getSystemDateAndTime
5 | - [X] getCapabilities
6 | - [X] getDiscoveryMode
7 | - [X] getScopes
8 | - [X] getHostname
9 | - [ ] getDNS
10 | - [ ] getNetworkInterfaces
11 | - [ ] getNetworkProtocols
12 | - [ ] setScopes
13 | - [ ] addScopes
14 | - [ ] removeScopes
15 | - [ ] setHostname
16 | - [ ] setDNS
17 | - [ ] setNetworkProtocols
18 | - [ ] getNetworkDefaultGateway
19 | - [ ] setNetworkDefaultGateway
20 | - [ ] reboot
21 | - [ ] getUsers
22 | - [ ] createUsers
23 | - [ ] deleteUsers
24 | - [ ] setUser
25 | - [ ] getRelayOutputs
26 | - [ ] getNTP
27 | - [ ] setNTP
28 | - [ ] getDynamicDNS
29 | - [ ] getZeroConfiguration
30 | - [ ] getServices
31 | - [ ] getServiceCapabilities
32 | - [ ] OnvifServiceMedia
33 | - [X] getProfiles
34 | - [X] getStreamUri
35 | - [ ] getVideoEncoderConfigurations
36 | - [ ] getVideoEncoderConfiguration
37 | - [ ] getCompatibleVideoEncoderConfigurations
38 | - [ ] getVideoEncoderConfigurationOptions
39 | - [ ] getGuaranteedNumberOfVideoEncoderInstances
40 | - [ ] getProfile
41 | - [ ] createProfile
42 | - [ ] deleteProfile
43 | - [ ] getVideoSources
44 | - [ ] getVideoSourceConfiguration
45 | - [ ] getVideoSourceConfigurations
46 | - [ ] getCompatibleVideoSourceConfigurations
47 | - [ ] getVideoSourceConfigurationOptions
48 | - [ ] getMetadataConfiguration
49 | - [ ] getMetadataConfigurations
50 | - [ ] getCompatibleMetadataConfigurations
51 | - [ ] getMetadataConfigurationOptions
52 | - [ ] getAudioSources
53 | - [ ] getAudioSourceConfiguration
54 | - [ ] getAudioSourceConfigurations
55 | - [ ] getCompatibleAudioSourceConfigurations
56 | - [ ] getAudioSourceConfigurationOptions
57 | - [ ] getAudioEncoderConfiguration
58 | - [ ] getAudioEncoderConfigurations
59 | - [ ] getCompatibleAudioEncoderConfigurations
60 | - [ ] getAudioEncoderConfigurationOptions
61 | - [ ] getSnapshotUri
62 | - [ ] OnvifServicePtz
63 | - [ ] getNodes
64 | - [ ] getNode
65 | - [ ] getConfigurations
66 | - [ ] getConfiguration
67 | - [ ] getConfigurationOptions
68 | - [ ] getStatus
69 | - [ ] continuousMove
70 | - [ ] absoluteMove
71 | - [ ] relativeMove
72 | - [ ] stop
73 | - [ ] gotoHomePosition
74 | - [ ] setHomePosition
75 | - [ ] setPreset
76 | - [ ] getPresets
77 | - [ ] gotoPreset
78 | - [ ] removePreset
--------------------------------------------------------------------------------
/device.go:
--------------------------------------------------------------------------------
1 | package onvif
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | var deviceXMLNs = []string{
8 | `xmlns:tds="http://www.onvif.org/ver10/device/wsdl"`,
9 | `xmlns:tt="http://www.onvif.org/ver10/schema"`,
10 | `xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl"`,
11 | }
12 |
13 | // GetInformation fetch information of ONVIF camera
14 | func (device Device) GetInformation() (DeviceInformation, error) {
15 | // Create SOAP
16 | soap := SOAP{
17 | Body: "",
18 | XMLNs: deviceXMLNs,
19 | }
20 |
21 | // Send SOAP request
22 | response, err := soap.SendRequest(device.XAddr)
23 | if err != nil {
24 | return DeviceInformation{}, err
25 | }
26 |
27 | // Parse response to interface
28 | deviceInfo, err := response.ValueForPath("Envelope.Body.GetDeviceInformationResponse")
29 | if err != nil {
30 | return DeviceInformation{}, err
31 | }
32 |
33 | // Parse interface to struct
34 | result := DeviceInformation{}
35 | if mapInfo, ok := deviceInfo.(map[string]interface{}); ok {
36 | result.Manufacturer = interfaceToString(mapInfo["Manufacturer"])
37 | result.Model = interfaceToString(mapInfo["Model"])
38 | result.FirmwareVersion = interfaceToString(mapInfo["FirmwareVersion"])
39 | result.SerialNumber = interfaceToString(mapInfo["SerialNumber"])
40 | result.HardwareID = interfaceToString(mapInfo["HardwareId"])
41 | }
42 |
43 | return result, nil
44 | }
45 |
46 | // GetCapabilities fetch info of ONVIF camera's capabilities
47 | func (device Device) GetCapabilities() (DeviceCapabilities, error) {
48 | // Create SOAP
49 | soap := SOAP{
50 | XMLNs: deviceXMLNs,
51 | Body: `
52 | All
53 | `,
54 | }
55 |
56 | // Send SOAP request
57 | response, err := soap.SendRequest(device.XAddr)
58 | if err != nil {
59 | return DeviceCapabilities{}, err
60 | }
61 |
62 | // Get network capabilities
63 | envelopeBodyPath := "Envelope.Body.GetCapabilitiesResponse.Capabilities"
64 | ifaceNetCap, err := response.ValueForPath(envelopeBodyPath + ".Device.Network")
65 | if err != nil {
66 | return DeviceCapabilities{}, err
67 | }
68 |
69 | netCap := NetworkCapabilities{}
70 | if mapNetCap, ok := ifaceNetCap.(map[string]interface{}); ok {
71 | netCap.DynDNS = interfaceToBool(mapNetCap["DynDNS"])
72 | netCap.IPFilter = interfaceToBool(mapNetCap["IPFilter"])
73 | netCap.IPVersion6 = interfaceToBool(mapNetCap["IPVersion6"])
74 | netCap.ZeroConfig = interfaceToBool(mapNetCap["ZeroConfiguration"])
75 | }
76 |
77 | // Get events capabilities
78 | ifaceEventsCap, err := response.ValueForPath(envelopeBodyPath + ".Events")
79 | if err != nil {
80 | return DeviceCapabilities{}, err
81 | }
82 |
83 | eventsCap := make(map[string]bool)
84 | if mapEventsCap, ok := ifaceEventsCap.(map[string]interface{}); ok {
85 | for key, value := range mapEventsCap {
86 | if strings.ToLower(key) == "xaddr" {
87 | continue
88 | }
89 |
90 | key = strings.Replace(key, "WS", "", 1)
91 | eventsCap[key] = interfaceToBool(value)
92 | }
93 | }
94 |
95 | // Get streaming capabilities
96 | ifaceStreamingCap, err := response.ValueForPath(envelopeBodyPath + ".Media.StreamingCapabilities")
97 | if err != nil {
98 | return DeviceCapabilities{}, err
99 | }
100 |
101 | streamingCap := make(map[string]bool)
102 | if mapStreamingCap, ok := ifaceStreamingCap.(map[string]interface{}); ok {
103 | for key, value := range mapStreamingCap {
104 | key = strings.Replace(key, "_", " ", -1)
105 | streamingCap[key] = interfaceToBool(value)
106 | }
107 | }
108 |
109 | // Create final result
110 | deviceCapabilities := DeviceCapabilities{
111 | Network: netCap,
112 | Events: eventsCap,
113 | Streaming: streamingCap,
114 | }
115 |
116 | return deviceCapabilities, nil
117 | }
118 |
119 | // GetDiscoveryMode fetch network discovery mode of an ONVIF camera
120 | func (device Device) GetDiscoveryMode() (string, error) {
121 | // Create SOAP
122 | soap := SOAP{
123 | Body: "",
124 | XMLNs: deviceXMLNs,
125 | }
126 |
127 | // Send SOAP request
128 | response, err := soap.SendRequest(device.XAddr)
129 | if err != nil {
130 | return "", err
131 | }
132 |
133 | // Parse response
134 | discoveryMode, _ := response.ValueForPathString("Envelope.Body.GetDiscoveryModeResponse.DiscoveryMode")
135 | return discoveryMode, nil
136 | }
137 |
138 | // GetScopes fetch scopes of an ONVIF camera
139 | func (device Device) GetScopes() ([]string, error) {
140 | // Create SOAP
141 | soap := SOAP{
142 | Body: "",
143 | XMLNs: deviceXMLNs,
144 | }
145 |
146 | // Send SOAP request
147 | response, err := soap.SendRequest(device.XAddr)
148 | if err != nil {
149 | return nil, err
150 | }
151 |
152 | // Parse response to interface
153 | ifaceScopes, err := response.ValuesForPath("Envelope.Body.GetScopesResponse.Scopes")
154 | if err != nil {
155 | return nil, err
156 | }
157 |
158 | // Convert interface to array of scope
159 | scopes := []string{}
160 | for _, ifaceScope := range ifaceScopes {
161 | if mapScope, ok := ifaceScope.(map[string]interface{}); ok {
162 | scope := interfaceToString(mapScope["ScopeItem"])
163 | scopes = append(scopes, scope)
164 | }
165 | }
166 |
167 | return scopes, nil
168 | }
169 |
170 | // GetHostname fetch hostname of an ONVIF camera
171 | func (device Device) Ptz(Token, x, y, z string) error {
172 | // Create SOAP
173 | soap := SOAP{
174 | Body: `
175 | ` + Token + `
176 |
177 |
178 |
179 |
180 |
181 |
182 | `,
183 | XMLNs: deviceXMLNs,
184 | }
185 |
186 | // Send SOAP request
187 | _, err := soap.SendRequest(device.XAddr)
188 | return err
189 | }
190 | func (device Device) PtzStop(Token, x, y, z string) error {
191 | // Create SOAP
192 | soap := SOAP{
193 | Body: `
194 | ` + Token + `
195 | false
196 | false
197 | `,
198 | XMLNs: deviceXMLNs,
199 | }
200 |
201 | // Send SOAP request
202 | _, err := soap.SendRequest(device.XAddr)
203 | return err
204 | }
205 |
206 | // GetHostname fetch hostname of an ONVIF camera
207 | func (device Device) GetHostname() (HostnameInformation, error) {
208 | // Create SOAP
209 | soap := SOAP{
210 | Body: "",
211 | XMLNs: deviceXMLNs,
212 | }
213 |
214 | // Send SOAP request
215 | response, err := soap.SendRequest(device.XAddr)
216 | if err != nil {
217 | return HostnameInformation{}, err
218 | }
219 |
220 | // Parse response to interface
221 | ifaceHostInfo, err := response.ValueForPath("Envelope.Body.GetHostnameResponse.HostnameInformation")
222 | if err != nil {
223 | return HostnameInformation{}, err
224 | }
225 |
226 | // Parse interface to struct
227 | hostnameInfo := HostnameInformation{}
228 | if mapHostInfo, ok := ifaceHostInfo.(map[string]interface{}); ok {
229 | hostnameInfo.Name = interfaceToString(mapHostInfo["Name"])
230 | hostnameInfo.FromDHCP = interfaceToBool(mapHostInfo["FromDHCP"])
231 | }
232 |
233 | return hostnameInfo, nil
234 | }
235 |
--------------------------------------------------------------------------------
/device_test.go:
--------------------------------------------------------------------------------
1 | package onvif
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "testing"
7 | )
8 |
9 | func TestGetInformation(t *testing.T) {
10 | log.Println("Test GetInformation")
11 |
12 | res, err := testDevice.GetInformation()
13 | if err != nil {
14 | t.Error(err)
15 | }
16 |
17 | js := prettyJSON(&res)
18 | fmt.Println(js)
19 | }
20 |
21 | func TestGetCapabilities(t *testing.T) {
22 | log.Println("Test GetCapabilities")
23 |
24 | res, err := testDevice.GetCapabilities()
25 | if err != nil {
26 | t.Error(err)
27 | }
28 |
29 | js := prettyJSON(&res)
30 | fmt.Println(js)
31 | }
32 |
33 | func TestGetDiscoveryMode(t *testing.T) {
34 | log.Println("Test GetDiscoveryMode")
35 |
36 | res, err := testDevice.GetDiscoveryMode()
37 | if err != nil {
38 | t.Error(err)
39 | }
40 |
41 | fmt.Println(res)
42 | }
43 |
44 | func TestGetScopes(t *testing.T) {
45 | log.Println("Test GetScopes")
46 |
47 | res, err := testDevice.GetScopes()
48 | if err != nil {
49 | t.Error(err)
50 | }
51 |
52 | js := prettyJSON(&res)
53 | fmt.Println(js)
54 | }
55 |
56 | func TestGetHostname(t *testing.T) {
57 | log.Println("Test GetHostname")
58 |
59 | res, err := testDevice.GetHostname()
60 | if err != nil {
61 | t.Error(err)
62 | }
63 |
64 | js := prettyJSON(&res)
65 | fmt.Println(js)
66 | }
67 |
--------------------------------------------------------------------------------
/discovery.go:
--------------------------------------------------------------------------------
1 | package onvif
2 |
3 | import (
4 | "errors"
5 | "net"
6 | "regexp"
7 | "strings"
8 | "time"
9 |
10 | "github.com/clbanning/mxj"
11 | "github.com/satori/go.uuid"
12 | )
13 |
14 | var errWrongDiscoveryResponse = errors.New("Response is not related to discovery request")
15 |
16 | // StartDiscovery send a WS-Discovery message and wait for all matching device to respond
17 | func StartDiscovery(duration time.Duration) ([]Device, error) {
18 | // Get list of interface address
19 | addrs, err := net.InterfaceAddrs()
20 | if err != nil {
21 | return []Device{}, err
22 | }
23 |
24 | // Fetch IPv4 address
25 | ipAddrs := []string{}
26 | for _, addr := range addrs {
27 | ipAddr, ok := addr.(*net.IPNet)
28 | if ok && !ipAddr.IP.IsLoopback() && ipAddr.IP.To4() != nil {
29 | ipAddrs = append(ipAddrs, ipAddr.IP.String())
30 | }
31 | }
32 |
33 | // Create initial discovery results
34 | discoveryResults := []Device{}
35 |
36 | // Discover device on each interface's network
37 | for _, ipAddr := range ipAddrs {
38 | devices, err := discoverDevices(ipAddr, duration)
39 | if err != nil {
40 | return []Device{}, err
41 | }
42 |
43 | discoveryResults = append(discoveryResults, devices...)
44 | }
45 |
46 | return discoveryResults, nil
47 | }
48 |
49 | func discoverDevices(ipAddr string, duration time.Duration) ([]Device, error) {
50 | // Create WS-Discovery request
51 | requestID := "uuid:" + uuid.NewV4().String()
52 | request := `
53 |
54 |
59 |
60 | ` + requestID + `
61 | urn:schemas-xmlsoap-org:ws:2005:04:discovery
62 | http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe
63 |
64 |
65 |
66 |
67 | dn:NetworkVideoTransmitter
68 |
69 |
70 | `
71 |
72 | // Clean WS-Discovery message
73 | request = regexp.MustCompile(`\>\s+\<`).ReplaceAllString(request, "><")
74 | request = regexp.MustCompile(`\s+`).ReplaceAllString(request, " ")
75 |
76 | // Create UDP address for local and multicast address
77 | localAddress, err := net.ResolveUDPAddr("udp4", ipAddr+":0")
78 | if err != nil {
79 | return []Device{}, err
80 | }
81 |
82 | multicastAddress, err := net.ResolveUDPAddr("udp4", "239.255.255.250:3702")
83 | if err != nil {
84 | return []Device{}, err
85 | }
86 |
87 | // Create UDP connection to listen for respond from matching device
88 | conn, err := net.ListenUDP("udp", localAddress)
89 | if err != nil {
90 | return []Device{}, err
91 | }
92 | defer conn.Close()
93 |
94 | // Set connection's timeout
95 | err = conn.SetDeadline(time.Now().Add(duration))
96 | if err != nil {
97 | return []Device{}, err
98 | }
99 |
100 | // Send WS-Discovery request to multicast address
101 | _, err = conn.WriteToUDP([]byte(request), multicastAddress)
102 | if err != nil {
103 | return []Device{}, err
104 | }
105 |
106 | // Create initial discovery results
107 | discoveryResults := []Device{}
108 |
109 | // Keep reading UDP message until timeout
110 | for {
111 | // Create buffer and receive UDP response
112 | buffer := make([]byte, 10*1024)
113 | _, _, err = conn.ReadFromUDP(buffer)
114 |
115 | // Check if connection timeout
116 | if err != nil {
117 | if udpErr, ok := err.(net.Error); ok && udpErr.Timeout() {
118 | break
119 | } else {
120 | return discoveryResults, err
121 | }
122 | }
123 |
124 | // Read and parse WS-Discovery response
125 | device, err := readDiscoveryResponse(requestID, buffer)
126 | if err != nil && err != errWrongDiscoveryResponse {
127 | return discoveryResults, err
128 | }
129 |
130 | // Push device to results
131 | discoveryResults = append(discoveryResults, device)
132 | }
133 |
134 | return discoveryResults, nil
135 | }
136 |
137 | // readDiscoveryResponse reads and parses WS-Discovery response
138 | func readDiscoveryResponse(messageID string, buffer []byte) (Device, error) {
139 | // Inital result
140 | result := Device{}
141 |
142 | // Parse XML to map
143 | mapXML, err := mxj.NewMapXml(buffer)
144 | if err != nil {
145 | return result, err
146 | }
147 |
148 | // Check if this response is for our request
149 | responseMessageID, _ := mapXML.ValueForPathString("Envelope.Header.RelatesTo")
150 | if responseMessageID != messageID {
151 | return result, errWrongDiscoveryResponse
152 | }
153 |
154 | // Get device's ID and clean it
155 | deviceID, _ := mapXML.ValueForPathString("Envelope.Body.ProbeMatches.ProbeMatch.EndpointReference.Address")
156 | deviceID = strings.Replace(deviceID, "urn:uuid:", "", 1)
157 |
158 | // Get device's name
159 | deviceName := ""
160 | scopes, _ := mapXML.ValueForPathString("Envelope.Body.ProbeMatches.ProbeMatch.Scopes")
161 | for _, scope := range strings.Split(scopes, " ") {
162 | if strings.HasPrefix(scope, "onvif://www.onvif.org/name/") {
163 | deviceName = strings.Replace(scope, "onvif://www.onvif.org/name/", "", 1)
164 | deviceName = strings.Replace(deviceName, "_", " ", -1)
165 | break
166 | }
167 | }
168 |
169 | // Get device's xAddrs
170 | xAddrs, _ := mapXML.ValueForPathString("Envelope.Body.ProbeMatches.ProbeMatch.XAddrs")
171 | listXAddr := strings.Split(xAddrs, " ")
172 | if len(listXAddr) == 0 {
173 | return result, errors.New("Device does not have any xAddr")
174 | }
175 |
176 | // Finalize result
177 | result.ID = deviceID
178 | result.Name = deviceName
179 | result.XAddr = listXAddr[0]
180 |
181 | return result, nil
182 | }
183 |
--------------------------------------------------------------------------------
/media.go:
--------------------------------------------------------------------------------
1 | package onvif
2 |
3 | var mediaXMLNs = []string{
4 | `xmlns:trt="http://www.onvif.org/ver10/media/wsdl"`,
5 | `xmlns:tt="http://www.onvif.org/ver10/schema"`,
6 | `xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl"`,
7 | }
8 |
9 | // GetProfiles fetch available media profiles of ONVIF camera
10 | func (device Device) GetProfiles() ([]MediaProfile, error) {
11 | // Create SOAP
12 | soap := SOAP{
13 | Body: "",
14 | XMLNs: mediaXMLNs,
15 | }
16 |
17 | // Send SOAP request
18 | response, err := soap.SendRequest(device.XAddr)
19 | if err != nil {
20 | return []MediaProfile{}, err
21 | }
22 |
23 | // Get and parse list of profile to interface
24 | ifaceProfiles, err := response.ValuesForPath("Envelope.Body.GetProfilesResponse.Profiles")
25 | if err != nil {
26 | return []MediaProfile{}, err
27 | }
28 |
29 | // Create initial result
30 | result := []MediaProfile{}
31 |
32 | // Parse each available profile
33 | for _, ifaceProfile := range ifaceProfiles {
34 | if mapProfile, ok := ifaceProfile.(map[string]interface{}); ok {
35 | // Parse name and token
36 | profile := MediaProfile{}
37 | profile.Name = interfaceToString(mapProfile["Name"])
38 | profile.Token = interfaceToString(mapProfile["-token"])
39 |
40 | // Parse video source configuration
41 | videoSource := MediaSourceConfig{}
42 | if mapVideoSource, ok := mapProfile["VideoSourceConfiguration"].(map[string]interface{}); ok {
43 | videoSource.Name = interfaceToString(mapVideoSource["Name"])
44 | videoSource.Token = interfaceToString(mapVideoSource["-token"])
45 | videoSource.SourceToken = interfaceToString(mapVideoSource["SourceToken"])
46 |
47 | // Parse video bounds
48 | bounds := MediaBounds{}
49 | if mapVideoBounds, ok := mapVideoSource["Bounds"].(map[string]interface{}); ok {
50 | bounds.Height = interfaceToInt(mapVideoBounds["-height"])
51 | bounds.Width = interfaceToInt(mapVideoBounds["-width"])
52 | }
53 | videoSource.Bounds = bounds
54 | }
55 | profile.VideoSourceConfig = videoSource
56 |
57 | // Parse video encoder configuration
58 | videoEncoder := VideoEncoderConfig{}
59 | if mapVideoEncoder, ok := mapProfile["VideoEncoderConfiguration"].(map[string]interface{}); ok {
60 | videoEncoder.Name = interfaceToString(mapVideoEncoder["Name"])
61 | videoEncoder.Token = interfaceToString(mapVideoEncoder["-token"])
62 | videoEncoder.Encoding = interfaceToString(mapVideoEncoder["Encoding"])
63 | videoEncoder.Quality = interfaceToInt(mapVideoEncoder["Quality"])
64 | videoEncoder.SessionTimeout = interfaceToString(mapVideoEncoder["SessionTimeout"])
65 |
66 | // Parse video rate control
67 | rateControl := VideoRateControl{}
68 | if mapVideoRate, ok := mapVideoEncoder["RateControl"].(map[string]interface{}); ok {
69 | rateControl.BitrateLimit = interfaceToInt(mapVideoRate["BitrateLimit"])
70 | rateControl.EncodingInterval = interfaceToInt(mapVideoRate["EncodingInterval"])
71 | rateControl.FrameRateLimit = interfaceToInt(mapVideoRate["FrameRateLimit"])
72 | }
73 | videoEncoder.RateControl = rateControl
74 |
75 | // Parse video resolution
76 | resolution := MediaBounds{}
77 | if mapVideoRes, ok := mapVideoEncoder["Resolution"].(map[string]interface{}); ok {
78 | resolution.Height = interfaceToInt(mapVideoRes["Height"])
79 | resolution.Width = interfaceToInt(mapVideoRes["Width"])
80 | }
81 | videoEncoder.Resolution = resolution
82 | }
83 | profile.VideoEncoderConfig = videoEncoder
84 |
85 | // Parse audio source configuration
86 | audioSource := MediaSourceConfig{}
87 | if mapAudioSource, ok := mapProfile["AudioSourceConfiguration"].(map[string]interface{}); ok {
88 | audioSource.Name = interfaceToString(mapAudioSource["Name"])
89 | audioSource.Token = interfaceToString(mapAudioSource["-token"])
90 | audioSource.SourceToken = interfaceToString(mapAudioSource["SourceToken"])
91 | }
92 | profile.AudioSourceConfig = audioSource
93 |
94 | // Parse audio encoder configuration
95 | audioEncoder := AudioEncoderConfig{}
96 | if mapAudioEncoder, ok := mapProfile["AudioEncoderConfiguration"].(map[string]interface{}); ok {
97 | audioEncoder.Name = interfaceToString(mapAudioEncoder["Name"])
98 | audioEncoder.Token = interfaceToString(mapAudioEncoder["-token"])
99 | audioEncoder.Encoding = interfaceToString(mapAudioEncoder["Encoding"])
100 | audioEncoder.Bitrate = interfaceToInt(mapAudioEncoder["Bitrate"])
101 | audioEncoder.SampleRate = interfaceToInt(mapAudioEncoder["SampleRate"])
102 | audioEncoder.SessionTimeout = interfaceToString(mapAudioEncoder["SessionTimeout"])
103 | }
104 | profile.AudioEncoderConfig = audioEncoder
105 |
106 | // Parse PTZ configuration
107 | ptzConfig := PTZConfig{}
108 | if mapPTZ, ok := mapProfile["PTZConfiguration"].(map[string]interface{}); ok {
109 | ptzConfig.Name = interfaceToString(mapPTZ["Name"])
110 | ptzConfig.Token = interfaceToString(mapPTZ["-token"])
111 | ptzConfig.NodeToken = interfaceToString(mapPTZ["NodeToken"])
112 | }
113 | profile.PTZConfig = ptzConfig
114 |
115 | // Push profile to result
116 | result = append(result, profile)
117 | }
118 | }
119 |
120 | return result, nil
121 | }
122 |
123 | // GetStreamURI fetch stream URI of a media profile.
124 | // Possible protocol is UDP, HTTP or RTSP
125 | func (device Device) GetStreamURI(profileToken, protocol string) (MediaURI, error) {
126 | // Create SOAP
127 | soap := SOAP{
128 | XMLNs: mediaXMLNs,
129 | Body: `
130 |
131 | RTP-Unicast
132 | ` + protocol + `
133 |
134 | ` + profileToken + `
135 | `,
136 | }
137 |
138 | // Send SOAP request
139 | response, err := soap.SendRequest(device.XAddr)
140 | if err != nil {
141 | return MediaURI{}, err
142 | }
143 |
144 | // Parse response to interface
145 | ifaceURI, err := response.ValueForPath("Envelope.Body.GetStreamUriResponse.MediaUri")
146 | if err != nil {
147 | return MediaURI{}, err
148 | }
149 |
150 | // Parse interface to struct
151 | streamURI := MediaURI{}
152 | if mapURI, ok := ifaceURI.(map[string]interface{}); ok {
153 | streamURI.URI = interfaceToString(mapURI["Uri"])
154 | streamURI.Timeout = interfaceToString(mapURI["Timeout"])
155 | streamURI.InvalidAfterConnect = interfaceToBool(mapURI["InvalidAfterConnect"])
156 | streamURI.InvalidAfterReboot = interfaceToBool(mapURI["InvalidAfterReboot"])
157 | }
158 |
159 | return streamURI, nil
160 | }
161 |
--------------------------------------------------------------------------------
/media_test.go:
--------------------------------------------------------------------------------
1 | package onvif
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "testing"
7 | )
8 |
9 | func TestGetProfiles(t *testing.T) {
10 | log.Println("Test GetProfiles")
11 |
12 | res, err := testDevice.GetProfiles()
13 | if err != nil {
14 | t.Error(err)
15 | }
16 |
17 | js := prettyJSON(&res)
18 | fmt.Println(js)
19 | }
20 |
21 | func TestGetStreamURI(t *testing.T) {
22 | log.Println("Test GetStreamURI")
23 |
24 | res, err := testDevice.GetStreamURI("IPCProfilesToken0", "UDP")
25 | if err != nil {
26 | t.Error(err)
27 | }
28 |
29 | js := prettyJSON(&res)
30 | fmt.Println(js)
31 | }
32 |
--------------------------------------------------------------------------------
/model.go:
--------------------------------------------------------------------------------
1 | package onvif
2 |
3 | // Device contains data of ONVIF camera
4 | type Device struct {
5 | ID string
6 | Name string
7 | XAddr string
8 | User string
9 | Password string
10 | }
11 |
12 | // DeviceInformation contains information of ONVIF camera
13 | type DeviceInformation struct {
14 | FirmwareVersion string
15 | HardwareID string
16 | Manufacturer string
17 | Model string
18 | SerialNumber string
19 | }
20 |
21 | // NetworkCapabilities contains networking capabilities of ONVIF camera
22 | type NetworkCapabilities struct {
23 | DynDNS bool
24 | IPFilter bool
25 | IPVersion6 bool
26 | ZeroConfig bool
27 | }
28 |
29 | // DeviceCapabilities contains capabilities of an ONVIF camera
30 | type DeviceCapabilities struct {
31 | Network NetworkCapabilities
32 | Events map[string]bool
33 | Streaming map[string]bool
34 | }
35 |
36 | // HostnameInformation contains hostname info of an ONVIF camera
37 | type HostnameInformation struct {
38 | Name string
39 | FromDHCP bool
40 | }
41 |
42 | // MediaBounds contains resolution of a video media
43 | type MediaBounds struct {
44 | Height int
45 | Width int
46 | }
47 |
48 | // MediaSourceConfig contains configuration of a media source
49 | type MediaSourceConfig struct {
50 | Name string
51 | Token string
52 | SourceToken string
53 | Bounds MediaBounds
54 | }
55 |
56 | // VideoRateControl contains rate control of a video
57 | type VideoRateControl struct {
58 | BitrateLimit int
59 | EncodingInterval int
60 | FrameRateLimit int
61 | }
62 |
63 | // VideoEncoderConfig contains configuration of a video encoder
64 | type VideoEncoderConfig struct {
65 | Name string
66 | Token string
67 | Encoding string
68 | Quality int
69 | RateControl VideoRateControl
70 | Resolution MediaBounds
71 | SessionTimeout string
72 | }
73 |
74 | // AudioEncoderConfig contains configuration of an audio encoder
75 | type AudioEncoderConfig struct {
76 | Name string
77 | Token string
78 | Encoding string
79 | Bitrate int
80 | SampleRate int
81 | SessionTimeout string
82 | }
83 |
84 | // PTZConfig contains configuration of a PTZ control in camera
85 | type PTZConfig struct {
86 | Name string
87 | Token string
88 | NodeToken string
89 | }
90 |
91 | // MediaProfile contains media profile of an ONVIF camera
92 | type MediaProfile struct {
93 | Name string
94 | Token string
95 | VideoSourceConfig MediaSourceConfig
96 | VideoEncoderConfig VideoEncoderConfig
97 | AudioSourceConfig MediaSourceConfig
98 | AudioEncoderConfig AudioEncoderConfig
99 | PTZConfig PTZConfig
100 | }
101 |
102 | // MediaURI contains streaming URI of an ONVIF camera
103 | type MediaURI struct {
104 | URI string
105 | Timeout string
106 | InvalidAfterConnect bool
107 | InvalidAfterReboot bool
108 | }
109 |
--------------------------------------------------------------------------------
/soap.go:
--------------------------------------------------------------------------------
1 | package onvif
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha1"
6 | "encoding/base64"
7 | "errors"
8 | "io/ioutil"
9 | "net/http"
10 | "net/url"
11 | "regexp"
12 | "time"
13 |
14 | "github.com/clbanning/mxj"
15 | "github.com/satori/go.uuid"
16 | )
17 |
18 | var httpClient = &http.Client{Timeout: time.Second * 4}
19 |
20 | // SOAP contains data for SOAP request
21 | type SOAP struct {
22 | Body string
23 | XMLNs []string
24 | User string
25 | Password string
26 | TokenAge time.Duration
27 | }
28 |
29 | // SendRequest sends SOAP request to xAddr
30 | func (soap SOAP) SendRequest(xaddr string) (mxj.Map, error) {
31 | // Create SOAP request
32 |
33 | // Make sure URL valid and add authentication in xAddr
34 | urlXAddr, err := url.Parse(xaddr)
35 |
36 | if urlXAddr.User != nil && urlXAddr.User.Username() != "" {
37 | soap.User = urlXAddr.User.Username()
38 | soap.Password, _ = urlXAddr.User.Password()
39 | }
40 | if err != nil {
41 | return nil, err
42 | }
43 | request := soap.createRequest()
44 |
45 | // Make sure URL valid and add authentication in xAddr
46 | //urlXAddr, err := url.Parse(xaddr)
47 |
48 | if soap.User != "" {
49 | urlXAddr.User = url.UserPassword(soap.User, soap.Password)
50 | }
51 |
52 | // Create HTTP request
53 | buffer := bytes.NewBuffer([]byte(request))
54 | req, err := http.NewRequest("POST", urlXAddr.String(), buffer)
55 | req.Header.Set("Content-Type", "application/soap+xml")
56 | req.Header.Set("Charset", "utf-8")
57 |
58 | // Send request
59 | resp, err := httpClient.Do(req)
60 | if err != nil {
61 | return nil, err
62 | }
63 | defer resp.Body.Close()
64 |
65 | // Read response body
66 | responseBody, err := ioutil.ReadAll(resp.Body)
67 | if err != nil {
68 | return nil, err
69 | }
70 |
71 | // Parse XML to map
72 | mapXML, err := mxj.NewMapXml(responseBody)
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | // Check if SOAP returns fault
78 | fault, _ := mapXML.ValueForPathString("Envelope.Body.Fault.Reason.Text.#text")
79 | if fault != "" {
80 | return nil, errors.New(fault)
81 | }
82 |
83 | return mapXML, nil
84 | }
85 |
86 | func (soap SOAP) createRequest() string {
87 | // Create request envelope
88 | request := ``
89 | request += `"
96 |
97 | // Set request header
98 | if soap.User != "" {
99 | request += "" + soap.createUserToken() + ""
100 | }
101 |
102 | // Set request body
103 | request += "" + soap.Body + ""
104 |
105 | // Close request envelope
106 | request += ""
107 |
108 | // Clean request
109 | request = regexp.MustCompile(`\>\s+\<`).ReplaceAllString(request, "><")
110 | request = regexp.MustCompile(`\s+`).ReplaceAllString(request, " ")
111 |
112 | return request
113 | }
114 |
115 | func (soap SOAP) createUserToken() string {
116 | nonce := uuid.NewV4().Bytes()
117 | nonce64 := base64.StdEncoding.EncodeToString(nonce)
118 | timestamp := time.Now().Add(soap.TokenAge).UTC().Format(time.RFC3339)
119 | token := string(nonce) + timestamp + soap.Password
120 |
121 | sha := sha1.New()
122 | sha.Write([]byte(token))
123 | shaToken := sha.Sum(nil)
124 | shaDigest64 := base64.StdEncoding.EncodeToString(shaToken)
125 |
126 | return `
127 |
128 | ` + soap.User + `
129 | ` + shaDigest64 + `
130 | ` + nonce64 + `
131 | ` + timestamp + `
132 |
133 | `
134 | }
135 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package onvif
2 |
3 | import (
4 | "encoding/json"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | var testDevice = Device{
10 | XAddr: "http://192.168.1.75:5000/onvif/device_service",
11 | }
12 |
13 | func interfaceToString(src interface{}) string {
14 | str, _ := src.(string)
15 | return str
16 | }
17 |
18 | func interfaceToBool(src interface{}) bool {
19 | strBool := interfaceToString(src)
20 | return strings.ToLower(strBool) == "true"
21 | }
22 |
23 | func interfaceToInt(src interface{}) int {
24 | strNumber := interfaceToString(src)
25 | number, _ := strconv.Atoi(strNumber)
26 | return number
27 | }
28 |
29 | func prettyJSON(src interface{}) string {
30 | result, _ := json.MarshalIndent(&src, "", " ")
31 | return string(result)
32 | }
33 |
--------------------------------------------------------------------------------