├── 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 += `" 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 | --------------------------------------------------------------------------------