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  }