github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/plugins/onvif/client.go (about) 1 // This file is part of the Smart Home 2 // Program complex distribution https://github.com/e154/smart-home 3 // Copyright (C) 2023, Filippov Alex 4 // 5 // This library is free software: you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 3 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Library General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library. If not, see 17 // <https://www.gnu.org/licenses/>. 18 19 package onvif 20 21 import ( 22 "context" 23 "fmt" 24 "regexp" 25 "strings" 26 "sync" 27 "sync/atomic" 28 "time" 29 30 wsnt "github.com/eyetowers/gonvif/pkg/generated/onvif/docs_oasisopen_org/wsn/b2" 31 deviceWsdl "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver10/device/wsdl" 32 eventsWsdl "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver10/events/wsdl" 33 media1Wsdl "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver10/media/wsdl" 34 "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver10/schema" 35 media2Wsdl "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver20/media/wsdl" 36 ptzWsdl "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver20/ptz/wsdl" 37 "github.com/eyetowers/gonvif/pkg/gonvif" 38 39 "github.com/e154/smart-home/common" 40 ) 41 42 const ( 43 unsubscribeTimeout = 2 * time.Second 44 profileIndex = 0 45 pollTimeout = "PT60S" 46 ) 47 48 var ( 49 subscriptionTimeout wsnt.AbsoluteOrRelativeTimeType = "PT120S" 50 ) 51 52 type Client struct { 53 username, password, address string 54 port int64 55 requireAuthorization bool 56 cli gonvif.Client 57 mediaProfiles []*schema.Profile 58 media2Profiles []*media2Wsdl.MediaProfile 59 capabilities *schema.Capabilities 60 pTZConfigurationOptions *schema.PTZConfigurationOptions 61 isStarted atomic.Bool 62 quit chan struct{} 63 wg sync.WaitGroup 64 actorHandler func(interface{}) 65 } 66 67 func NewClient(handler func(interface{})) *Client { 68 return &Client{ 69 actorHandler: handler, 70 } 71 } 72 73 func (s *Client) Start(username, password, address string, port int64, requireAuthorization bool) (err error) { 74 if s.isStarted.Load() { 75 return 76 } 77 s.isStarted.Store(true) 78 79 s.username = username 80 s.password = password 81 s.address = address 82 s.port = port 83 s.requireAuthorization = requireAuthorization 84 85 s.quit = make(chan struct{}) 86 87 s.wg.Add(1) 88 89 go func() { 90 defer func() { 91 s.wg.Done() 92 }() 93 94 var counter int 95 96 for { 97 98 counter++ 99 if counter >= 3 { 100 counter = 0 101 go s.actorHandler(&ConnectionStatus{false}) 102 } 103 104 select { 105 case <-s.quit: 106 return 107 default: 108 } 109 110 if err != nil { 111 time.Sleep(time.Second * 5) 112 } 113 114 // Connect to the Onvif device. 115 s.cli, err = gonvif.New(fmt.Sprintf("http://%s:%d", address, port), username, password, false) 116 if err != nil { 117 continue 118 } 119 120 if err = s.GetCapabilities(); err != nil { 121 continue 122 } 123 124 if err = s.getOptions(); err != nil { 125 continue 126 } 127 128 if len(s.mediaProfiles) == 0 { 129 continue 130 } 131 132 var streamList []string 133 if streamList, err = s.GetStreamList(); err != nil { 134 continue 135 } 136 137 snapshotURI := s.GetSnapshotURI() 138 139 go s.actorHandler(&StreamList{List: streamList, SnapshotUri: snapshotURI}) 140 141 err = s.eventServiceSubscribe() 142 } 143 }() 144 145 return 146 } 147 148 func (s *Client) Shutdown() (err error) { 149 if !s.isStarted.Load() { 150 return 151 } 152 close(s.quit) 153 s.wg.Wait() 154 s.isStarted.Store(false) 155 return 156 } 157 158 func (s *Client) GetCapabilities() error { 159 device, err := s.cli.Device() 160 if err != nil { 161 return err 162 } 163 164 resp, err := device.GetCapabilities(&deviceWsdl.GetCapabilities{}) 165 if err != nil { 166 return err 167 } 168 s.capabilities = resp.Capabilities 169 return nil 170 } 171 172 func (s *Client) GetStreamList() ([]string, error) { 173 174 var list = make([]string, 0) 175 176 var protocol schema.TransportProtocol 177 protocol = schema.TransportProtocolTCP 178 if s.capabilities.Media.StreamingCapabilities.RTP_RTSP_TCP { 179 protocol = schema.TransportProtocolRTSP 180 } 181 var stream = schema.StreamTypeRTPUnicast 182 if media, err := s.cli.Media(); err == nil { 183 for _, profile := range s.mediaProfiles { 184 resp, _ := media.GetStreamUri(&media1Wsdl.GetStreamUri{ 185 StreamSetup: &schema.StreamSetup{ 186 Transport: &schema.Transport{ 187 Protocol: &protocol, 188 }, 189 Stream: &stream, 190 }, 191 ProfileToken: profile.Token, 192 }) 193 if resp != nil && resp.MediaUri != nil { 194 list = append(list, s.prepareUri(resp.MediaUri.Uri)) 195 } 196 } 197 } 198 199 if media, err := s.cli.Media2(); err == nil { 200 for _, profile := range s.media2Profiles { 201 resp, _ := media.GetStreamUri(&media2Wsdl.GetStreamUri{ 202 Protocol: string(protocol), 203 ProfileToken: profile.Token, 204 }) 205 if resp != nil { 206 list = append(list, s.prepareUri(resp.Uri)) 207 } 208 } 209 } 210 211 return list, nil 212 } 213 214 func (s *Client) GetSnapshotURI() *string { 215 var uri string 216 if media, err := s.cli.Media(); err == nil { 217 resp, _ := media.GetSnapshotUri(&media1Wsdl.GetSnapshotUri{ 218 ProfileToken: s.mediaProfiles[profileIndex].Token, 219 }) 220 if resp != nil && resp.MediaUri != nil { 221 uri = resp.MediaUri.Uri 222 } 223 } 224 if media, err := s.cli.Media2(); err == nil { 225 resp, _ := media.GetSnapshotUri(&media2Wsdl.GetSnapshotUri{ 226 ProfileToken: s.mediaProfiles[profileIndex].Token, 227 }) 228 if resp != nil { 229 uri = resp.Uri 230 } 231 } 232 if uri == "" { 233 return nil 234 } 235 return common.String(s.prepareUri(uri)) 236 } 237 238 func (s *Client) ContinuousMove(X, Y float32) error { 239 240 if X == 0 && Y == 0 { 241 return nil 242 } 243 244 ptz, err := s.cli.PTZ() 245 if err != nil { 246 return err 247 } 248 249 options := s.pTZConfigurationOptions.Spaces.ContinuousPanTiltVelocitySpace[profileIndex] 250 if Y > options.YRange.Max { 251 Y = options.YRange.Max 252 } 253 if Y < options.YRange.Min { 254 Y = options.YRange.Min 255 } 256 257 if X > options.XRange.Max { 258 X = options.XRange.Max 259 } 260 if X < options.XRange.Min { 261 X = options.XRange.Min 262 } 263 264 var profileToken *schema.ReferenceToken 265 if s.mediaProfiles != nil { 266 profileToken = s.mediaProfiles[profileIndex].Token 267 } 268 if s.media2Profiles != nil { 269 profileToken = s.media2Profiles[profileIndex].Token 270 } 271 _, err = ptz.ContinuousMove(&ptzWsdl.ContinuousMove{ 272 ProfileToken: profileToken, 273 Velocity: &schema.PTZSpeed{ 274 PanTilt: &schema.Vector2D{ 275 X: X, 276 Y: Y, 277 }, 278 }, 279 }) 280 if err != nil { 281 log.Warn(err.Error()) 282 } 283 return err 284 } 285 286 func (s *Client) StopContinuousMove() error { 287 288 ptz, err := s.cli.PTZ() 289 if err != nil { 290 return err 291 } 292 293 var profileToken *schema.ReferenceToken 294 if s.mediaProfiles != nil { 295 profileToken = s.mediaProfiles[profileIndex].Token 296 } 297 if s.media2Profiles != nil { 298 profileToken = s.media2Profiles[profileIndex].Token 299 } 300 _, err = ptz.Stop(&ptzWsdl.Stop{ 301 ProfileToken: profileToken, 302 }) 303 if err != nil { 304 log.Warn(err.Error()) 305 } 306 return err 307 } 308 309 func (s *Client) getOptions() error { 310 311 // MEDIA PROFILES 312 if media, err := s.cli.Media(); err == nil { 313 var resp *media1Wsdl.GetProfilesResponse 314 resp, err = media.GetProfiles(&media1Wsdl.GetProfiles{}) 315 if err == nil { 316 s.mediaProfiles = resp.Profiles 317 } 318 } 319 320 if media, err := s.cli.Media2(); err == nil { 321 var resp *media2Wsdl.GetProfilesResponse 322 resp, err = media.GetProfiles(&media2Wsdl.GetProfiles{ 323 Type: []string{"All"}, 324 }) 325 if err == nil { 326 s.media2Profiles = resp.Profiles 327 } 328 } 329 330 // PTZ 331 ptz, err := s.cli.PTZ() 332 if err == nil { 333 var configurationToken *schema.ReferenceToken 334 if s.mediaProfiles != nil { 335 configurationToken = s.mediaProfiles[profileIndex].PTZConfiguration.Token 336 } 337 if s.media2Profiles != nil { 338 configurationToken = s.media2Profiles[profileIndex].Configurations.PTZ.Token 339 } 340 configurationOptions, err := ptz.GetConfigurationOptions(&ptzWsdl.GetConfigurationOptions{ 341 ConfigurationToken: configurationToken, 342 }) 343 if err == nil { 344 s.pTZConfigurationOptions = configurationOptions.PTZConfigurationOptions 345 } 346 } 347 348 return nil 349 } 350 351 func (s *Client) eventServiceSubscribe() error { 352 events, err := s.cli.Events() 353 if err != nil { 354 return err 355 } 356 resp, err := events.CreatePullPointSubscription(&eventsWsdl.CreatePullPointSubscription{ 357 InitialTerminationTime: &subscriptionTimeout, 358 }) 359 if err != nil { 360 return err 361 } 362 headers := gonvif.ComposeHeaders(resp.SubscriptionReference) 363 subscription, err := s.cli.Subscription(string(*resp.SubscriptionReference.Address), headers...) 364 if err != nil { 365 return err 366 } 367 return s.processEvents(subscription) 368 } 369 370 func (s *Client) processEvents(subscription eventsWsdl.PullPointSubscription) error { 371 defer func() { _ = s.unsubscribe(subscription) }() 372 ch := make(chan *eventsWsdl.PullMessagesResponse) 373 chErr := make(chan error) 374 defer func() { 375 close(ch) 376 close(chErr) 377 }() 378 379 for { 380 381 go func() { 382 resp, err := subscription.PullMessages(&eventsWsdl.PullMessages{MessageLimit: 100, Timeout: pollTimeout}) 383 select { 384 case <-s.quit: 385 return 386 default: 387 } 388 if err != nil { 389 chErr <- err 390 return 391 } 392 ch <- resp 393 }() 394 395 select { 396 case <-s.quit: 397 return nil 398 case v := <-ch: 399 s.eventHandler(v.NotificationMessage) 400 if _, err := subscription.Renew(&wsnt.Renew{TerminationTime: &subscriptionTimeout}); err != nil { 401 return err 402 } 403 case err := <-chErr: 404 return err 405 } 406 } 407 } 408 409 func (s *Client) unsubscribe(subscription eventsWsdl.PullPointSubscription) error { 410 ctx, cancel := context.WithTimeout(context.Background(), unsubscribeTimeout) 411 defer cancel() 412 413 var empty eventsWsdl.EmptyString 414 _, err := subscription.UnsubscribeContext(ctx, &empty) 415 return err 416 } 417 418 func (s *Client) eventHandler(messages []*wsnt.NotificationMessage) { 419 for _, msg := range messages { 420 switch msg.Topic.Value { 421 case "tns1:VideoSource/MotionAlarm": 422 s.prepareMotionAlarm(msg) 423 case "tns1:VideoSource/GlobalSceneChange/ImagingService": 424 s.prepareImagingService(msg) 425 default: 426 log.Debugf("unknown message topic: \"%s\"", msg.Topic.Value) 427 } 428 } 429 } 430 431 func (s *Client) prepareMotionAlarm(msg *wsnt.NotificationMessage) { 432 if msg.Message.Message == nil || msg.Message.Message.PropertyOperation != "Changed" { 433 return 434 } 435 var state = false 436 var t time.Time 437 if msg.Message.Message != nil && msg.Message.Message.Data != nil && 438 msg.Message.Message.Data.SimpleItem != nil && len(msg.Message.Message.Data.SimpleItem) > 0 { 439 state = msg.Message.Message.Data.SimpleItem[profileIndex].Value == "true" 440 } 441 if msg.Message.Message != nil && msg.Message.Message.UTCTime != nil { 442 t = msg.Message.Message.UTCTime.Time 443 } 444 go s.actorHandler(&MotionAlarm{State: state, Time: t}) 445 } 446 447 func (s *Client) prepareImagingService(msg *wsnt.NotificationMessage) { 448 449 } 450 451 var re = regexp.MustCompile(`(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`) 452 453 func (s *Client) prepareUri(uri string) string { 454 if !s.requireAuthorization || !re.MatchString(uri) { 455 return uri 456 } 457 ip := re.FindString(uri) 458 return strings.ReplaceAll(uri, ip, fmt.Sprintf("%s:%s@%s", s.username, s.password, ip)) 459 }