diff --git a/TODO.md b/TODO.md index 66ba03b..d9505f9 100644 --- a/TODO.md +++ b/TODO.md @@ -30,13 +30,13 @@ - [ ] getServices - [ ] getServiceCapabilities - [ ] OnvifServiceMedia - - [ ] getStreamUri + - [X] getProfiles + - [X] getStreamUri - [ ] getVideoEncoderConfigurations - [ ] getVideoEncoderConfiguration - [ ] getCompatibleVideoEncoderConfigurations - [ ] getVideoEncoderConfigurationOptions - [ ] getGuaranteedNumberOfVideoEncoderInstances - - [ ] getProfiles - [ ] getProfile - [ ] createProfile - [ ] deleteProfile diff --git a/media.go b/media.go new file mode 100644 index 0000000..975b581 --- /dev/null +++ b/media.go @@ -0,0 +1,159 @@ +package onvif + +var mediaXMLNs = []string{ + `xmlns:trt="http://www.onvif.org/ver10/media/wsdl"`, + `xmlns:tt="http://www.onvif.org/ver10/schema"`, +} + +// GetProfiles fetch available media profiles of ONVIF camera +func (device Device) GetProfiles() ([]MediaProfile, error) { + // Create SOAP + soap := SOAP{ + Body: "", + XMLNs: mediaXMLNs, + } + + // Send SOAP request + response, err := soap.SendRequest(device.XAddr) + if err != nil { + return []MediaProfile{}, err + } + + // Get and parse list of profile to interface + ifaceProfiles, err := response.ValuesForPath("Envelope.Body.GetProfilesResponse.Profiles") + if err != nil { + return []MediaProfile{}, err + } + + // Create initial result + result := []MediaProfile{} + + // Parse each available profile + for _, ifaceProfile := range ifaceProfiles { + if mapProfile, ok := ifaceProfile.(map[string]interface{}); ok { + // Parse name and token + profile := MediaProfile{} + profile.Name = interfaceToString(mapProfile["Name"]) + profile.Token = interfaceToString(mapProfile["-token"]) + + // Parse video source configuration + videoSource := MediaSourceConfig{} + if mapVideoSource, ok := mapProfile["VideoSourceConfiguration"].(map[string]interface{}); ok { + videoSource.Name = interfaceToString(mapVideoSource["Name"]) + videoSource.Token = interfaceToString(mapVideoSource["-token"]) + videoSource.SourceToken = interfaceToString(mapVideoSource["SourceToken"]) + + // Parse video bounds + bounds := MediaBounds{} + if mapVideoBounds, ok := mapVideoSource["Bounds"].(map[string]interface{}); ok { + bounds.Height = interfaceToInt(mapVideoBounds["-height"]) + bounds.Width = interfaceToInt(mapVideoBounds["-width"]) + } + videoSource.Bounds = bounds + } + profile.VideoSourceConfig = videoSource + + // Parse video encoder configuration + videoEncoder := VideoEncoderConfig{} + if mapVideoEncoder, ok := mapProfile["VideoEncoderConfiguration"].(map[string]interface{}); ok { + videoEncoder.Name = interfaceToString(mapVideoEncoder["Name"]) + videoEncoder.Token = interfaceToString(mapVideoEncoder["-token"]) + videoEncoder.Encoding = interfaceToString(mapVideoEncoder["Encoding"]) + videoEncoder.Quality = interfaceToInt(mapVideoEncoder["Quality"]) + videoEncoder.SessionTimeout = interfaceToString(mapVideoEncoder["SessionTimeout"]) + + // Parse video rate control + rateControl := VideoRateControl{} + if mapVideoRate, ok := mapVideoEncoder["RateControl"].(map[string]interface{}); ok { + rateControl.BitrateLimit = interfaceToInt(mapVideoRate["BitrateLimit"]) + rateControl.EncodingInterval = interfaceToInt(mapVideoRate["EncodingInterval"]) + rateControl.FrameRateLimit = interfaceToInt(mapVideoRate["FrameRateLimit"]) + } + videoEncoder.RateControl = rateControl + + // Parse video resolution + resolution := MediaBounds{} + if mapVideoRes, ok := mapVideoEncoder["Resolution"].(map[string]interface{}); ok { + resolution.Height = interfaceToInt(mapVideoRes["Height"]) + resolution.Width = interfaceToInt(mapVideoRes["Width"]) + } + videoEncoder.Resolution = resolution + } + profile.VideoEncoderConfig = videoEncoder + + // Parse audio source configuration + audioSource := MediaSourceConfig{} + if mapAudioSource, ok := mapProfile["AudioSourceConfiguration"].(map[string]interface{}); ok { + audioSource.Name = interfaceToString(mapAudioSource["Name"]) + audioSource.Token = interfaceToString(mapAudioSource["-token"]) + audioSource.SourceToken = interfaceToString(mapAudioSource["SourceToken"]) + } + profile.AudioSourceConfig = audioSource + + // Parse audio encoder configuration + audioEncoder := AudioEncoderConfig{} + if mapAudioEncoder, ok := mapProfile["AudioEncoderConfiguration"].(map[string]interface{}); ok { + audioEncoder.Name = interfaceToString(mapAudioEncoder["Name"]) + audioEncoder.Token = interfaceToString(mapAudioEncoder["-token"]) + audioEncoder.Encoding = interfaceToString(mapAudioEncoder["Encoding"]) + audioEncoder.Bitrate = interfaceToInt(mapAudioEncoder["Bitrate"]) + audioEncoder.SampleRate = interfaceToInt(mapAudioEncoder["SampleRate"]) + audioEncoder.SessionTimeout = interfaceToString(mapAudioEncoder["SessionTimeout"]) + } + profile.AudioEncoderConfig = audioEncoder + + // Parse PTZ configuration + ptzConfig := PTZConfig{} + if mapPTZ, ok := mapProfile["PTZConfiguration"].(map[string]interface{}); ok { + ptzConfig.Name = interfaceToString(mapPTZ["Name"]) + ptzConfig.Token = interfaceToString(mapPTZ["-token"]) + ptzConfig.NodeToken = interfaceToString(mapPTZ["NodeToken"]) + } + profile.PTZConfig = ptzConfig + + // Push profile to result + result = append(result, profile) + } + } + + return result, nil +} + +// GetStreamURI fetch stream URI of a media profile. +// Possible protocol is UDP, HTTP or RTSP +func (device Device) GetStreamURI(profileToken, protocol string) (MediaURI, error) { + // Create SOAP + soap := SOAP{ + XMLNs: mediaXMLNs, + Body: ` + + RTP-Unicast + ` + protocol + ` + + ` + profileToken + ` + `, + } + + // Send SOAP request + response, err := soap.SendRequest(device.XAddr) + if err != nil { + return MediaURI{}, err + } + + // Parse response to interface + ifaceURI, err := response.ValueForPath("Envelope.Body.GetStreamUriResponse.MediaUri") + if err != nil { + return MediaURI{}, err + } + + // Parse interface to struct + streamURI := MediaURI{} + if mapURI, ok := ifaceURI.(map[string]interface{}); ok { + streamURI.URI = interfaceToString(mapURI["Uri"]) + streamURI.Timeout = interfaceToString(mapURI["Timeout"]) + streamURI.InvalidAfterConnect = interfaceToBool(mapURI["InvalidAfterConnect"]) + streamURI.InvalidAfterReboot = interfaceToBool(mapURI["InvalidAfterReboot"]) + } + + return streamURI, nil +} diff --git a/media_test.go b/media_test.go new file mode 100644 index 0000000..5fa655f --- /dev/null +++ b/media_test.go @@ -0,0 +1,31 @@ +package onvif + +import ( + "fmt" + "log" + "testing" +) + +func TestGetProfiles(t *testing.T) { + log.Println("Test GetProfiles") + + res, err := testDevice.GetProfiles() + if err != nil { + t.Error(err) + } + + js := prettyJSON(&res) + fmt.Println(js) +} + +func TestGetStreamURI(t *testing.T) { + log.Println("Test GetStreamURI") + + res, err := testDevice.GetStreamURI("IPCProfilesToken0", "UDP") + if err != nil { + t.Error(err) + } + + js := prettyJSON(&res) + fmt.Println(js) +} diff --git a/model.go b/model.go index bd704c5..47c0129 100644 --- a/model.go +++ b/model.go @@ -38,3 +38,71 @@ type HostnameInformation struct { Name string FromDHCP bool } + +// MediaBounds contains resolution of a video media +type MediaBounds struct { + Height int + Width int +} + +// MediaSourceConfig contains configuration of a media source +type MediaSourceConfig struct { + Name string + Token string + SourceToken string + Bounds MediaBounds +} + +// VideoRateControl contains rate control of a video +type VideoRateControl struct { + BitrateLimit int + EncodingInterval int + FrameRateLimit int +} + +// VideoEncoderConfig contains configuration of a video encoder +type VideoEncoderConfig struct { + Name string + Token string + Encoding string + Quality int + RateControl VideoRateControl + Resolution MediaBounds + SessionTimeout string +} + +// AudioEncoderConfig contains configuration of an audio encoder +type AudioEncoderConfig struct { + Name string + Token string + Encoding string + Bitrate int + SampleRate int + SessionTimeout string +} + +// PTZConfig contains configuration of a PTZ control in camera +type PTZConfig struct { + Name string + Token string + NodeToken string +} + +// MediaProfile contains media profile of an ONVIF camera +type MediaProfile struct { + Name string + Token string + VideoSourceConfig MediaSourceConfig + VideoEncoderConfig VideoEncoderConfig + AudioSourceConfig MediaSourceConfig + AudioEncoderConfig AudioEncoderConfig + PTZConfig PTZConfig +} + +// MediaURI contains streaming URI of an ONVIF camera +type MediaURI struct { + URI string + Timeout string + InvalidAfterConnect bool + InvalidAfterReboot bool +} diff --git a/utils.go b/utils.go index e6677ea..895c7be 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,7 @@ package onvif import ( "encoding/json" + "strconv" "strings" ) @@ -19,6 +20,12 @@ func interfaceToBool(src interface{}) bool { return strings.ToLower(strBool) == "true" } +func interfaceToInt(src interface{}) int { + strNumber := interfaceToString(src) + number, _ := strconv.Atoi(strNumber) + return number +} + func prettyJSON(src interface{}) string { result, _ := json.MarshalIndent(&src, "", " ") return string(result)