diff --git a/examples/go2rtc_mjpeg/main.go b/examples/go2rtc_mjpeg/main.go
new file mode 100644
index 00000000..3c915b3c
--- /dev/null
+++ b/examples/go2rtc_mjpeg/main.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "github.com/AlexxIT/go2rtc/internal/api"
+ "github.com/AlexxIT/go2rtc/internal/api/ws"
+ "github.com/AlexxIT/go2rtc/internal/app"
+ "github.com/AlexxIT/go2rtc/internal/ffmpeg"
+ "github.com/AlexxIT/go2rtc/internal/mjpeg"
+ "github.com/AlexxIT/go2rtc/internal/streams"
+ "github.com/AlexxIT/go2rtc/internal/v4l2"
+ "github.com/AlexxIT/go2rtc/pkg/shell"
+)
+
+func main() {
+ app.Init()
+ streams.Init()
+
+ api.Init()
+ ws.Init()
+
+ ffmpeg.Init()
+ mjpeg.Init()
+ v4l2.Init()
+
+ shell.RunUntilSignal()
+}
diff --git a/examples/onvif_client/main.go b/examples/onvif_client/main.go
new file mode 100644
index 00000000..03dd12ba
--- /dev/null
+++ b/examples/onvif_client/main.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+ "log"
+ "net"
+ "net/url"
+ "os"
+
+ "github.com/AlexxIT/go2rtc/pkg/onvif"
+)
+
+func main() {
+ var rawURL = os.Args[1]
+ var operation = os.Args[2]
+ var token string
+ if len(os.Args) > 3 {
+ token = os.Args[3]
+ }
+
+ client, err := onvif.NewClient(rawURL)
+ if err != nil {
+ log.Panic(err)
+ }
+
+ var b []byte
+
+ switch operation {
+ case onvif.ServiceGetServiceCapabilities:
+ b, err = client.MediaRequest(operation)
+ case onvif.DeviceGetCapabilities,
+ onvif.DeviceGetDeviceInformation,
+ onvif.DeviceGetDiscoveryMode,
+ onvif.DeviceGetDNS,
+ onvif.DeviceGetHostname,
+ onvif.DeviceGetNetworkDefaultGateway,
+ onvif.DeviceGetNetworkInterfaces,
+ onvif.DeviceGetNetworkProtocols,
+ onvif.DeviceGetNTP,
+ onvif.DeviceGetScopes,
+ onvif.DeviceGetServices,
+ onvif.DeviceGetSystemDateAndTime,
+ onvif.DeviceSystemReboot:
+ b, err = client.DeviceRequest(operation)
+ case onvif.MediaGetProfiles, onvif.MediaGetVideoSources:
+ b, err = client.MediaRequest(operation)
+ case onvif.MediaGetProfile:
+ b, err = client.GetProfile(token)
+ case onvif.MediaGetVideoSourceConfiguration:
+ b, err = client.GetVideoSourceConfiguration(token)
+ case onvif.MediaGetStreamUri:
+ b, err = client.GetStreamUri(token)
+ case onvif.MediaGetSnapshotUri:
+ b, err = client.GetSnapshotUri(token)
+ default:
+ log.Printf("unknown action\n")
+ }
+
+ if err != nil {
+ log.Printf("%s\n", err)
+ }
+
+ u, err := url.Parse(rawURL)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ host, _, _ := net.SplitHostPort(u.Host)
+
+ if err = os.WriteFile(host+"_"+operation+".xml", b, 0644); err != nil {
+ log.Printf("%s\n", err)
+ }
+}
diff --git a/go.mod b/go.mod
index ecd32f3a..5f0a193b 100644
--- a/go.mod
+++ b/go.mod
@@ -8,20 +8,20 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-isatty v0.0.20
github.com/miekg/dns v1.1.62
- github.com/pion/ice/v2 v2.3.36
+ github.com/pion/ice/v2 v2.3.37
github.com/pion/interceptor v0.1.37
- github.com/pion/rtcp v1.2.14
- github.com/pion/rtp v1.8.9
+ github.com/pion/rtcp v1.2.15
+ github.com/pion/rtp v1.8.10
github.com/pion/sdp/v3 v3.0.9
github.com/pion/srtp/v2 v2.0.20
github.com/pion/stun v0.6.1
- github.com/pion/webrtc/v3 v3.3.4
+ github.com/pion/webrtc/v3 v3.3.5
github.com/rs/zerolog v1.33.0
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
- github.com/stretchr/testify v1.9.0
+ github.com/stretchr/testify v1.10.0
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
- golang.org/x/crypto v0.28.0
+ golang.org/x/crypto v0.31.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -31,19 +31,20 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/pion/datachannel v1.5.9 // indirect
+ github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.12 // indirect
github.com/pion/randutil v0.1.0 // indirect
- github.com/pion/sctp v1.8.33 // indirect
+ github.com/pion/sctp v1.8.35 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
+ github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
- golang.org/x/mod v0.18.0 // indirect
- golang.org/x/net v0.27.0 // indirect
- golang.org/x/sync v0.7.0 // indirect
- golang.org/x/sys v0.26.0 // indirect
- golang.org/x/tools v0.22.0 // indirect
+ golang.org/x/mod v0.20.0 // indirect
+ golang.org/x/net v0.33.0 // indirect
+ golang.org/x/sync v0.10.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+ golang.org/x/tools v0.24.0 // indirect
)
diff --git a/go.sum b/go.sum
index 804ecc43..c75ffced 100644
--- a/go.sum
+++ b/go.sum
@@ -31,13 +31,13 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
-github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
-github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
+github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
+github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
-github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc=
-github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
+github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0=
+github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
@@ -47,13 +47,13 @@ github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYF
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
-github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
-github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
+github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
+github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
-github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
-github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
-github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
-github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
+github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU=
+github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
+github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA=
+github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
@@ -67,11 +67,12 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
+github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
-github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk=
-github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
+github.com/pion/webrtc/v3 v3.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg=
+github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
@@ -95,8 +96,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
@@ -108,12 +110,13 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
-golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
-golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
+golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -122,13 +125,13 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
-golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -143,8 +146,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
-golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -165,6 +168,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
+golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
+golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
+golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
+golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
+golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
+golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
diff --git a/internal/onvif/README.md b/internal/onvif/README.md
new file mode 100644
index 00000000..ee922fbf
--- /dev/null
+++ b/internal/onvif/README.md
@@ -0,0 +1,25 @@
+# ONVIF
+
+A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`).
+
+Go2rtc has one video source and one profile per stream.
+
+## Tested clients
+
+Go2rtc works as ONVIF server:
+
+- Happytime onvif client (windows)
+- Home Assistant ONVIF integration (linux)
+- Onvier (android)
+- ONVIF Device Manager (windows)
+
+PS. Support only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet.
+
+## Tested cameras
+
+Go2rtc works as ONVIF client:
+
+- Dahua IPC-K42
+- OpenIPC
+- Reolink RLC-520A
+- TP-Link Tapo TC60
diff --git a/internal/onvif/init.go b/internal/onvif/onvif.go
similarity index 64%
rename from internal/onvif/init.go
rename to internal/onvif/onvif.go
index 014c5e18..d332ca38 100644
--- a/internal/onvif/init.go
+++ b/internal/onvif/onvif.go
@@ -55,49 +55,65 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
return
}
- action := onvif.GetRequestAction(b)
- if action == "" {
+ operation := onvif.GetRequestAction(b)
+ if operation == "" {
http.Error(w, "malformed request body", http.StatusBadRequest)
return
}
- log.Trace().Msgf("[onvif] %s", action)
+ log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
- var res string
+ switch operation {
+ case onvif.DeviceGetNetworkInterfaces, // important for Hass
+ onvif.DeviceGetSystemDateAndTime, // important for Hass
+ onvif.DeviceGetDiscoveryMode,
+ onvif.DeviceGetDNS,
+ onvif.DeviceGetHostname,
+ onvif.DeviceGetNetworkDefaultGateway,
+ onvif.DeviceGetNetworkProtocols,
+ onvif.DeviceGetNTP,
+ onvif.DeviceGetScopes:
+ b = onvif.StaticResponse(operation)
- switch action {
- case onvif.ActionGetCapabilities:
+ case onvif.DeviceGetCapabilities:
// important for Hass: Media section
- res = onvif.GetCapabilitiesResponse(r.Host)
+ b = onvif.GetCapabilitiesResponse(r.Host)
- case onvif.ActionGetSystemDateAndTime:
- // important for Hass
- res = onvif.GetSystemDateAndTimeResponse()
+ case onvif.DeviceGetServices:
+ b = onvif.GetServicesResponse(r.Host)
- case onvif.ActionGetNetworkInterfaces:
- // important for Hass: none
- res = onvif.GetNetworkInterfacesResponse()
-
- case onvif.ActionGetDeviceInformation:
+ case onvif.DeviceGetDeviceInformation:
// important for Hass: SerialNumber (unique server ID)
- res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
+ b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
- case onvif.ActionGetServiceCapabilities:
+ case onvif.ServiceGetServiceCapabilities:
// important for Hass
- res = onvif.GetServiceCapabilitiesResponse()
+ // TODO: check path links to media
+ b = onvif.GetMediaServiceCapabilitiesResponse()
- case onvif.ActionSystemReboot:
- res = onvif.SystemRebootResponse()
+ case onvif.DeviceSystemReboot:
+ b = onvif.StaticResponse(operation)
time.AfterFunc(time.Second, func() {
os.Exit(0)
})
- case onvif.ActionGetProfiles:
- // important for Hass: H264 codec, width, height
- res = onvif.GetProfilesResponse(streams.GetAll())
+ case onvif.MediaGetVideoSources:
+ b = onvif.GetVideoSourcesResponse(streams.GetAll())
- case onvif.ActionGetStreamUri:
+ case onvif.MediaGetProfiles:
+ // important for Hass: H264 codec, width, height
+ b = onvif.GetProfilesResponse(streams.GetAll())
+
+ case onvif.MediaGetProfile:
+ token := onvif.FindTagValue(b, "ProfileToken")
+ b = onvif.GetProfileResponse(token)
+
+ case onvif.MediaGetVideoSourceConfiguration:
+ token := onvif.FindTagValue(b, "ConfigurationToken")
+ b = onvif.GetVideoSourceConfigurationResponse(token)
+
+ case onvif.MediaGetStreamUri:
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -105,16 +121,22 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
}
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
- res = onvif.GetStreamUriResponse(uri)
+ b = onvif.GetStreamUriResponse(uri)
+
+ case onvif.MediaGetSnapshotUri:
+ uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken")
+ b = onvif.GetSnapshotUriResponse(uri)
default:
- http.Error(w, "unsupported action", http.StatusBadRequest)
+ http.Error(w, "unsupported operation", http.StatusBadRequest)
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
return
}
+ log.Trace().Msgf("[onvif] server response:\n%s", b)
+
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
- if _, err = w.Write([]byte(res)); err != nil {
+ if _, err = w.Write(b); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -160,7 +182,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
}
if l := log.Trace(); l.Enabled() {
- b, _ := client.GetProfiles()
+ b, _ := client.MediaRequest(onvif.MediaGetProfiles)
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
}
diff --git a/internal/tapo/tapo.go b/internal/tapo/tapo.go
index 724c9e86..88eff5c4 100644
--- a/internal/tapo/tapo.go
+++ b/internal/tapo/tapo.go
@@ -15,4 +15,8 @@ func Init() {
streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
return tapo.Dial(source)
})
+
+ streams.HandleFunc("vigi", func(source string) (core.Producer, error) {
+ return tapo.Dial(source)
+ })
}
diff --git a/internal/v4l2/v4l2.go b/internal/v4l2/v4l2.go
new file mode 100644
index 00000000..9cef99a5
--- /dev/null
+++ b/internal/v4l2/v4l2.go
@@ -0,0 +1,7 @@
+//go:build !linux
+
+package v4l2
+
+func Init() {
+ // not supported
+}
diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go
new file mode 100644
index 00000000..2cd60692
--- /dev/null
+++ b/internal/v4l2/v4l2_linux.go
@@ -0,0 +1,89 @@
+package v4l2
+
+import (
+ "encoding/binary"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+
+ "github.com/AlexxIT/go2rtc/internal/api"
+ "github.com/AlexxIT/go2rtc/internal/streams"
+ "github.com/AlexxIT/go2rtc/pkg/core"
+ "github.com/AlexxIT/go2rtc/pkg/v4l2"
+ "github.com/AlexxIT/go2rtc/pkg/v4l2/device"
+)
+
+func Init() {
+ streams.HandleFunc("v4l2", func(source string) (core.Producer, error) {
+ return v4l2.Open(source)
+ })
+
+ api.HandleFunc("api/v4l2", apiV4L2)
+}
+
+func apiV4L2(w http.ResponseWriter, r *http.Request) {
+ files, err := os.ReadDir("/dev")
+ if err != nil {
+ return
+ }
+
+ var sources []*api.Source
+
+ for _, file := range files {
+ if !strings.HasPrefix(file.Name(), core.KindVideo) {
+ continue
+ }
+
+ path := "/dev/" + file.Name()
+
+ dev, err := device.Open(path)
+ if err != nil {
+ continue
+ }
+
+ formats, _ := dev.ListFormats()
+ for _, fourCC := range formats {
+ name, ffmpeg := findFormat(fourCC)
+ source := &api.Source{Name: name}
+
+ sizes, _ := dev.ListSizes(fourCC)
+ for _, wh := range sizes {
+ if source.Info != "" {
+ source.Info += " "
+ }
+
+ source.Info += fmt.Sprintf("%dx%d", wh[0], wh[1])
+
+ frameRates, _ := dev.ListFrameRates(fourCC, wh[0], wh[1])
+ for _, fr := range frameRates {
+ source.Info += fmt.Sprintf("@%d", fr)
+
+ if source.URL == "" && ffmpeg != "" {
+ source.URL = fmt.Sprintf(
+ "v4l2:device?video=%s&input_format=%s&video_size=%dx%d&framerate=%d",
+ path, ffmpeg, wh[0], wh[1], fr,
+ )
+ }
+ }
+ }
+
+ if source.Info != "" {
+ sources = append(sources, source)
+ }
+ }
+
+ _ = dev.Close()
+ }
+
+ api.ResponseSources(w, sources)
+}
+
+func findFormat(fourCC uint32) (name, ffmpeg string) {
+ for _, format := range device.Formats {
+ if format.FourCC == fourCC {
+ return format.Name, format.FFmpeg
+ }
+ }
+ return string(binary.LittleEndian.AppendUint32(nil, fourCC)), ""
+}
diff --git a/main.go b/main.go
index d5c59ffc..db8de9f4 100644
--- a/main.go
+++ b/main.go
@@ -31,13 +31,14 @@ import (
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/tapo"
+ "github.com/AlexxIT/go2rtc/internal/v4l2"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/internal/webtorrent"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
- app.Version = "1.9.7"
+ app.Version = "1.9.8"
// 1. Core modules: app, api/ws, streams
@@ -84,6 +85,7 @@ func main() {
expr.Init() // expr source
gopro.Init() // gopro source
doorbird.Init() // doorbird source
+ v4l2.Init() // v4l2 source
// 6. Helper modules
diff --git a/pkg/flv/producer.go b/pkg/flv/producer.go
index 66755217..7535a8a4 100644
--- a/pkg/flv/producer.go
+++ b/pkg/flv/producer.go
@@ -140,23 +140,29 @@ func (c *Producer) probe() error {
// 1. Empty video/audio flag
// 2. MedaData without stereo key for AAC
// 3. Audio header after Video keyframe tag
- waitType := []byte{TagData}
- timeout := time.Now().Add(core.ProbeTimeout)
- for len(waitType) != 0 && time.Now().Before(timeout) {
+ // OpenIPC camera sends:
+ // 1. Empty video/audio flag
+ // 2. No MetaData packet
+ // 3. Sends a video packet in more than 3 seconds
+ waitVideo := true
+ waitAudio := true
+ timeout := time.Now().Add(time.Second * 5)
+
+ for (waitVideo || waitAudio) && time.Now().Before(timeout) {
pkt, err := c.readPacket()
if err != nil {
return err
}
- if i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 {
- continue
- } else {
- waitType = append(waitType[:i], waitType[i+1:]...)
- }
+ //log.Printf("%d %0.20s", pkt.PayloadType, pkt.Payload)
switch pkt.PayloadType {
case TagAudio:
+ if !waitAudio {
+ continue
+ }
+
_ = pkt.Payload[1] // bounds
codecID := pkt.Payload[0] >> 4 // SoundFormat
@@ -179,8 +185,13 @@ func (c *Producer) probe() error {
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
+ waitAudio = false
case TagVideo:
+ if !waitVideo {
+ continue
+ }
+
var codec *core.Codec
if isExHeader(pkt.Payload) {
@@ -213,19 +224,20 @@ func (c *Producer) probe() error {
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
+ waitVideo = false
case TagData:
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
- waitType = append(waitType, TagData)
+ continue
}
// Dahua cameras doesn't send videocodecid
- if bytes.Contains(pkt.Payload, []byte("videocodecid")) ||
- bytes.Contains(pkt.Payload, []byte("width")) ||
- bytes.Contains(pkt.Payload, []byte("framerate")) {
- waitType = append(waitType, TagVideo)
+ if !bytes.Contains(pkt.Payload, []byte("videocodecid")) &&
+ !bytes.Contains(pkt.Payload, []byte("width")) &&
+ !bytes.Contains(pkt.Payload, []byte("framerate")) {
+ waitVideo = false
}
- if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
- waitType = append(waitType, TagAudio)
+ if !bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
+ waitAudio = false
}
}
}
diff --git a/pkg/magic/keyframe.go b/pkg/magic/keyframe.go
index 8f70eec6..9b6ef562 100644
--- a/pkg/magic/keyframe.go
+++ b/pkg/magic/keyframe.go
@@ -24,6 +24,7 @@ func NewKeyframe() *Keyframe {
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecJPEG},
+ {Name: core.CodecRAW},
{Name: core.CodecH264},
{Name: core.CodecH265},
},
@@ -87,6 +88,15 @@ func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
if track.Codec.IsRTP() {
sender.Handler = mjpeg.RTPDepay(sender.Handler)
}
+
+ case core.CodecRAW:
+ sender.Handler = func(packet *rtp.Packet) {
+ if n, err := k.wr.Write(packet.Payload); err == nil {
+ k.Send += n
+ }
+ }
+
+ sender.Handler = mjpeg.Encoder(track.Codec, 5, sender.Handler)
}
sender.HandleRTP(track)
diff --git a/pkg/mjpeg/consumer.go b/pkg/mjpeg/consumer.go
index 16edc895..819c558a 100644
--- a/pkg/mjpeg/consumer.go
+++ b/pkg/mjpeg/consumer.go
@@ -46,7 +46,7 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
if track.Codec.IsRTP() {
sender.Handler = RTPDepay(sender.Handler)
} else if track.Codec.Name == core.CodecRAW {
- sender.Handler = Encoder(track.Codec, sender.Handler)
+ sender.Handler = Encoder(track.Codec, 0, sender.Handler)
}
sender.HandleRTP(track)
diff --git a/pkg/mjpeg/helpers.go b/pkg/mjpeg/helpers.go
index 08b4408b..87f59e07 100644
--- a/pkg/mjpeg/helpers.go
+++ b/pkg/mjpeg/helpers.go
@@ -9,24 +9,38 @@ import (
"github.com/pion/rtp"
)
-// FixJPEG - reencode JPEG if it has wrong header
-//
-// for example, this app produce "bad" images:
-// https://github.com/jacksonliam/mjpg-streamer
-//
-// and they can't be uploaded to the Telegram servers:
-// {"ok":false,"error_code":400,"description":"Bad Request: IMAGE_PROCESS_FAILED"}
func FixJPEG(b []byte) []byte {
// skip non-JPEG
- if len(b) < 10 || b[0] != 0xFF || b[1] != 0xD8 {
- return b
- }
- // skip if header OK for imghdr library
- // https://docs.python.org/3/library/imghdr.html
- if string(b[2:4]) == "\xFF\xDB" || string(b[6:10]) == "JFIF" || string(b[6:10]) == "Exif" {
+ if len(b) < 10 || b[0] != 0xFF || b[1] != markerSOI {
return b
}
+ // skip JPEG without app marker
+ if b[2] == 0xFF && b[3] == markerDQT {
+ return b
+ }
+
+ switch string(b[6:10]) {
+ case "JFIF", "Exif":
+ // skip if header OK for imghdr library
+ // - https://docs.python.org/3/library/imghdr.html
+ return b
+ case "AVI1":
+ // adds DHT tables to JPEG file before SOS marker
+ // useful when you want to save a JPEG frame from an MJPEG stream
+ // - https://github.com/image-rs/jpeg-decoder/issues/76
+ // - https://github.com/pion/mediadevices/pull/493
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=963907#c18
+ return InjectDHT(b)
+ }
+
+ // reencode JPEG if it has wrong header
+ //
+ // for example, this app produce "bad" images:
+ // https://github.com/jacksonliam/mjpg-streamer
+ //
+ // and they can't be uploaded to the Telegram servers:
+ // {"ok":false,"error_code":400,"description":"Bad Request: IMAGE_PROCESS_FAILED"}
img, err := jpeg.Decode(bytes.NewReader(b))
if err != nil {
return b
@@ -38,12 +52,19 @@ func FixJPEG(b []byte) []byte {
return buf.Bytes()
}
-func Encoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
+// Encoder convert YUV frame to Img.
+// Support skipping empty frames, for example if USB cam needs time to start.
+func Encoder(codec *core.Codec, skipEmpty int, handler core.HandlerFunc) core.HandlerFunc {
newImage := y4m.NewImage(codec.FmtpLine)
return func(packet *rtp.Packet) {
img := newImage(packet.Payload)
+ if skipEmpty != 0 && y4m.HasSameColor(img) {
+ skipEmpty--
+ return
+ }
+
buf := bytes.NewBuffer(nil)
if err := jpeg.Encode(buf, img, nil); err != nil {
return
@@ -54,3 +75,26 @@ func Encoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
handler(&clone)
}
}
+
+const dhtSize = 432 // known size for 4 default tables
+
+func InjectDHT(b []byte) []byte {
+ if bytes.Index(b, []byte{0xFF, markerDHT}) > 0 {
+ return b // already exist
+ }
+
+ i := bytes.Index(b, []byte{0xFF, markerSOS})
+ if i < 0 {
+ return b
+ }
+
+ dht := make([]byte, 0, dhtSize)
+ dht = MakeHuffmanHeaders(dht)
+
+ tmp := make([]byte, len(b)+dhtSize)
+ copy(tmp, b[:i])
+ copy(tmp[i:], dht)
+ copy(tmp[i+dhtSize:], b[i:])
+
+ return tmp
+}
diff --git a/pkg/mjpeg/jpeg.go b/pkg/mjpeg/jpeg.go
new file mode 100644
index 00000000..8d6d13d1
--- /dev/null
+++ b/pkg/mjpeg/jpeg.go
@@ -0,0 +1,10 @@
+package mjpeg
+
+const (
+ markerSOF = 0xC0 // Start Of Frame (Baseline Sequential)
+ markerSOI = 0xD8 // Start Of Image
+ markerEOI = 0xD9 // End Of Image
+ markerSOS = 0xDA // Start Of Scan
+ markerDQT = 0xDB // Define Quantization Table
+ markerDHT = 0xC4 // Define Huffman Table
+)
diff --git a/pkg/mjpeg/rfc2435.go b/pkg/mjpeg/rfc2435.go
index 44307896..aa34c2f1 100644
--- a/pkg/mjpeg/rfc2435.go
+++ b/pkg/mjpeg/rfc2435.go
@@ -143,9 +143,7 @@ var chm_ac_symbols = []byte{
func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
- p = append(p, 0xFF,
- 0xD8, // SOI
- )
+ p = append(p, 0xFF, markerSOI)
p = MakeQuantHeader(p, lqt, 0)
p = MakeQuantHeader(p, cqt, 1)
@@ -156,8 +154,7 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {
t = 0x22 // hsamp = 2, vsamp = 2
}
- p = append(p, 0xFF,
- 0xC0, // SOF
+ p = append(p, 0xFF, markerSOF,
0, 17, // size
8, // bits per component
byte(h>>8), byte(h&0xFF),
@@ -174,13 +171,9 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {
1, // quant table 1
)
- p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0)
- p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1)
- p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0)
- p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1)
+ p = MakeHuffmanHeaders(p)
- return append(p, 0xFF,
- 0xDA, // SOS
+ return append(p, 0xFF, markerSOS,
0, 12, // size
3, // 3 components
0, // comp 0
@@ -196,16 +189,23 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {
}
func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte {
- p = append(p, 0xFF, 0xDB, 0, 67, tableNo)
+ p = append(p, 0xFF, markerDQT, 0, 67, tableNo)
return append(p, qt...)
}
func MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte {
- p = append(p,
- 0xFF, 0xC4, 0,
- byte(3+len(codelens)+len(symbols)),
+ p = append(p, 0xFF, markerDHT,
+ 0, byte(3+len(codelens)+len(symbols)), // size
(tableClass<<4)|tableNo,
)
p = append(p, codelens...)
return append(p, symbols...)
}
+
+func MakeHuffmanHeaders(p []byte) []byte {
+ p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0)
+ p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1)
+ p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0)
+ p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1)
+ return p
+}
diff --git a/pkg/onvif/README.md b/pkg/onvif/README.md
new file mode 100644
index 00000000..73267379
--- /dev/null
+++ b/pkg/onvif/README.md
@@ -0,0 +1,38 @@
+## Profiles
+
+- Profile A - For access control configuration
+- Profile C - For door control and event management
+- Profile S - For basic video streaming
+ - Video streaming and configuration
+- Profile T - For advanced video streaming
+ - H.264 / H.265 video compression
+ - Imaging settings
+ - Motion alarm and tampering events
+ - Metadata streaming
+ - Bi-directional audio
+
+## Services
+
+https://www.onvif.org/profiles/specifications/
+
+- https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl
+- https://www.onvif.org/ver20/imaging/wsdl/imaging.wsdl
+- https://www.onvif.org/ver10/media/wsdl/media.wsdl
+
+## TMP
+
+| | Dahua | Reolink | TP-Link |
+|------------------------|---------|---------|---------|
+| GetCapabilities | no auth | no auth | no auth |
+| GetServices | no auth | no auth | no auth |
+| GetServiceCapabilities | no auth | no auth | auth |
+| GetSystemDateAndTime | no auth | no auth | no auth |
+| GetNetworkInterfaces | auth | auth | auth |
+| GetDeviceInformation | auth | auth | auth |
+| GetProfiles | auth | auth | auth |
+| GetScopes | auth | auth | auth |
+
+- Dahua - onvif://192.168.10.90:80
+- Reolink - onvif://192.168.10.92:8000
+- TP-Link - onvif://192.168.10.91:2020/onvif/device_service
+-
\ No newline at end of file
diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go
index 97bfd8dc..cb6221e1 100644
--- a/pkg/onvif/client.go
+++ b/pkg/onvif/client.go
@@ -2,8 +2,6 @@ package onvif
import (
"bytes"
- "crypto/sha1"
- "encoding/base64"
"errors"
"html"
"io"
@@ -12,8 +10,6 @@ import (
"regexp"
"strings"
"time"
-
- "github.com/AlexxIT/go2rtc/pkg/core"
)
const PathDevice = "/onvif/device_service"
@@ -41,7 +37,7 @@ func NewClient(rawURL string) (*Client, error) {
client.deviceURL = baseURL + u.Path
}
- b, err := client.GetCapabilities()
+ b, err := client.DeviceRequest(DeviceGetCapabilities)
if err != nil {
return nil, err
}
@@ -95,7 +91,7 @@ func (c *Client) GetURI() (string, error) {
}
func (c *Client) GetName() (string, error) {
- b, err := c.GetDeviceInformation()
+ b, err := c.DeviceRequest(DeviceGetDeviceInformation)
if err != nil {
return "", err
}
@@ -104,7 +100,7 @@ func (c *Client) GetName() (string, error) {
}
func (c *Client) GetProfilesTokens() ([]string, error) {
- b, err := c.GetProfiles()
+ b, err := c.MediaRequest(MediaGetProfiles)
if err != nil {
return nil, err
}
@@ -127,86 +123,53 @@ func (c *Client) HasSnapshots() bool {
return strings.Contains(string(b), `SnapshotUri="true"`)
}
-func (c *Client) GetCapabilities() ([]byte, error) {
+func (c *Client) GetProfile(token string) ([]byte, error) {
return c.Request(
- c.deviceURL,
- `
- All
-`,
+ c.mediaURL, ``+token+``,
)
}
-func (c *Client) GetNetworkInterfaces() ([]byte, error) {
- return c.Request(
- c.deviceURL, ``,
- )
-}
-
-func (c *Client) GetDeviceInformation() ([]byte, error) {
- return c.Request(
- c.deviceURL, ``,
- )
-}
-
-func (c *Client) GetProfiles() ([]byte, error) {
- return c.Request(
- c.mediaURL, ``,
- )
+func (c *Client) GetVideoSourceConfiguration(token string) ([]byte, error) {
+ return c.Request(c.mediaURL, `
+ `+token+`
+`)
}
func (c *Client) GetStreamUri(token string) ([]byte, error) {
- return c.Request(
- c.mediaURL,
- `
+ return c.Request(c.mediaURL, `
RTP-Unicast
RTSP
`+token+`
-`,
- )
+`)
}
func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
return c.Request(
- c.imaginURL,
- `
- `+token+`
-`,
- )
-}
-
-func (c *Client) GetSystemDateAndTime() ([]byte, error) {
- return c.Request(
- c.deviceURL, ``,
+ c.imaginURL, ``+token+``,
)
}
func (c *Client) GetServiceCapabilities() ([]byte, error) {
// some cameras answer GetServiceCapabilities for media only for path = "/onvif/media"
return c.Request(
- c.mediaURL, ``,
+ c.mediaURL, ``,
)
}
-func (c *Client) SystemReboot() ([]byte, error) {
- return c.Request(
- c.deviceURL, ``,
- )
+func (c *Client) DeviceRequest(operation string) ([]byte, error) {
+ if operation == DeviceGetServices {
+ operation = `true`
+ } else {
+ operation = ``
+ }
+ return c.Request(c.deviceURL, operation)
}
-func (c *Client) GetServices() ([]byte, error) {
- return c.Request(
- c.deviceURL, `
- true
-`,
- )
-}
-
-func (c *Client) GetScopes() ([]byte, error) {
- return c.Request(
- c.deviceURL, ``,
- )
+func (c *Client) MediaRequest(operation string) ([]byte, error) {
+ operation = ``
+ return c.Request(c.mediaURL, operation)
}
func (c *Client) Request(url, body string) ([]byte, error) {
@@ -214,35 +177,11 @@ func (c *Client) Request(url, body string) ([]byte, error) {
return nil, errors.New("onvif: unsupported service")
}
- buf := bytes.NewBuffer(nil)
- buf.WriteString(
- ``,
- )
-
- if user := c.url.User; user != nil {
- nonce := core.RandString(16, 36)
- created := time.Now().UTC().Format(time.RFC3339Nano)
- pass, _ := user.Password()
-
- h := sha1.New()
- h.Write([]byte(nonce + created + pass))
-
- buf.WriteString(`
-
-
-` + user.Username() + `
-` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + `
-` + base64.StdEncoding.EncodeToString([]byte(nonce)) + `
-` + created + `
-
-
-`)
- }
-
- buf.WriteString(`` + body + ``)
+ e := NewEnvelopeWithUser(c.url.User)
+ e.Append(body)
client := &http.Client{Timeout: time.Second * 5000}
- res, err := client.Post(url, `application/soap+xml;charset=utf-8`, buf)
+ res, err := client.Post(url, `application/soap+xml;charset=utf-8`, bytes.NewReader(e.Bytes()))
if err != nil {
return nil, err
}
diff --git a/pkg/onvif/envelope.go b/pkg/onvif/envelope.go
new file mode 100644
index 00000000..f0e1b29c
--- /dev/null
+++ b/pkg/onvif/envelope.go
@@ -0,0 +1,79 @@
+package onvif
+
+import (
+ "crypto/sha1"
+ "encoding/base64"
+ "fmt"
+ "net/url"
+ "time"
+
+ "github.com/AlexxIT/go2rtc/pkg/core"
+)
+
+type Envelope struct {
+ buf []byte
+}
+
+const (
+ prefix1 = `
+
+`
+ prefix2 = `
+`
+ suffix = `
+
+`
+)
+
+func NewEnvelope() *Envelope {
+ e := &Envelope{buf: make([]byte, 0, 1024)}
+ e.Append(prefix1, prefix2)
+ return e
+}
+
+func NewEnvelopeWithUser(user *url.Userinfo) *Envelope {
+ if user == nil {
+ return NewEnvelope()
+ }
+
+ nonce := core.RandString(16, 36)
+ created := time.Now().UTC().Format(time.RFC3339Nano)
+ pass, _ := user.Password()
+
+ h := sha1.New()
+ h.Write([]byte(nonce + created + pass))
+
+ e := &Envelope{buf: make([]byte, 0, 1024)}
+ e.Append(prefix1)
+ e.Appendf(`
+
+
+ %s
+ %s
+ %s
+ %s
+
+
+
+`,
+ user.Username(),
+ base64.StdEncoding.EncodeToString(h.Sum(nil)),
+ base64.StdEncoding.EncodeToString([]byte(nonce)),
+ created)
+ e.Append(prefix2)
+ return e
+}
+
+func (e *Envelope) Append(args ...string) {
+ for _, s := range args {
+ e.buf = append(e.buf, s...)
+ }
+}
+
+func (e *Envelope) Appendf(format string, args ...any) {
+ e.buf = fmt.Appendf(e.buf, format, args...)
+}
+
+func (e *Envelope) Bytes() []byte {
+ return append(e.buf, suffix...)
+}
diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go
index fc9c8392..f240f2ec 100644
--- a/pkg/onvif/helpers.go
+++ b/pkg/onvif/helpers.go
@@ -1,6 +1,7 @@
package onvif
import (
+ "fmt"
"net"
"regexp"
"strconv"
@@ -11,7 +12,7 @@ import (
)
func FindTagValue(b []byte, tag string) string {
- re := regexp.MustCompile(`(?s)[:<]` + tag + `>([^<]+)`)
+ re := regexp.MustCompile(`(?s)<(?:\w+:)?` + tag + `\b[^>]*>([^<]+)`)
m := re.FindSubmatch(b)
if len(m) != 2 {
return ""
@@ -106,3 +107,25 @@ func atoi(s string) int {
}
return i
}
+
+func GetPosixTZ(current time.Time) string {
+ // Thanks to https://github.com/Path-Variable/go-posix-time
+ _, offset := current.Zone()
+
+ if current.IsDST() {
+ _, end := current.ZoneBounds()
+ endPlus1 := end.Add(time.Hour * 25)
+ _, offset = endPlus1.Zone()
+ }
+
+ var prefix string
+ if offset < 0 {
+ prefix = "GMT+"
+ offset = -offset / 60
+ } else {
+ prefix = "GMT-"
+ offset = offset / 60
+ }
+
+ return prefix + fmt.Sprintf("%02d:%02d", offset/60, offset%60)
+}
diff --git a/pkg/onvif/onvif_test.go b/pkg/onvif/onvif_test.go
index cd57d60b..e9ffab04 100644
--- a/pkg/onvif/onvif_test.go
+++ b/pkg/onvif/onvif_test.go
@@ -84,6 +84,34 @@ func TestGetStreamUri(t *testing.T) {
`,
url: "rtsp://192.168.5.53:8090/profile1=r",
},
+ {
+ name: "go2rtc 1.9.4",
+ xml: `
+
+
+
+ rtsp://192.168.1.123:8554/rtsp-dahua1
+
+
+
+`,
+ url: "rtsp://192.168.1.123:8554/rtsp-dahua1",
+ },
+ {
+ name: "go2rtc 1.9.8",
+ xml: `
+
+
+
+
+ rtsp://192.168.1.123:8554/rtsp-dahua2
+
+
+
+
+`,
+ url: "rtsp://192.168.1.123:8554/rtsp-dahua2",
+ },
}
for _, test := range tests {
diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go
index f8f2883c..db0bb2fb 100644
--- a/pkg/onvif/server.go
+++ b/pkg/onvif/server.go
@@ -2,30 +2,40 @@ package onvif
import (
"bytes"
- "fmt"
"regexp"
- "strconv"
"time"
)
-const (
- ActionGetCapabilities = "GetCapabilities"
- ActionGetSystemDateAndTime = "GetSystemDateAndTime"
- ActionGetNetworkInterfaces = "GetNetworkInterfaces"
- ActionGetDeviceInformation = "GetDeviceInformation"
- ActionGetServiceCapabilities = "GetServiceCapabilities"
- ActionGetProfiles = "GetProfiles"
- ActionGetStreamUri = "GetStreamUri"
- ActionSystemReboot = "SystemReboot"
+const ServiceGetServiceCapabilities = "GetServiceCapabilities"
- ActionGetServices = "GetServices"
- ActionGetScopes = "GetScopes"
- ActionGetVideoSources = "GetVideoSources"
- ActionGetAudioSources = "GetAudioSources"
- ActionGetVideoSourceConfigurations = "GetVideoSourceConfigurations"
- ActionGetAudioSourceConfigurations = "GetAudioSourceConfigurations"
- ActionGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations"
- ActionGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations"
+const (
+ DeviceGetCapabilities = "GetCapabilities"
+ DeviceGetDeviceInformation = "GetDeviceInformation"
+ DeviceGetDiscoveryMode = "GetDiscoveryMode"
+ DeviceGetDNS = "GetDNS"
+ DeviceGetHostname = "GetHostname"
+ DeviceGetNetworkDefaultGateway = "GetNetworkDefaultGateway"
+ DeviceGetNetworkInterfaces = "GetNetworkInterfaces"
+ DeviceGetNetworkProtocols = "GetNetworkProtocols"
+ DeviceGetNTP = "GetNTP"
+ DeviceGetScopes = "GetScopes"
+ DeviceGetServices = "GetServices"
+ DeviceGetSystemDateAndTime = "GetSystemDateAndTime"
+ DeviceSystemReboot = "SystemReboot"
+)
+
+const (
+ MediaGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations"
+ MediaGetAudioSources = "GetAudioSources"
+ MediaGetAudioSourceConfigurations = "GetAudioSourceConfigurations"
+ MediaGetProfile = "GetProfile"
+ MediaGetProfiles = "GetProfiles"
+ MediaGetSnapshotUri = "GetSnapshotUri"
+ MediaGetStreamUri = "GetStreamUri"
+ MediaGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations"
+ MediaGetVideoSources = "GetVideoSources"
+ MediaGetVideoSourceConfiguration = "GetVideoSourceConfiguration"
+ MediaGetVideoSourceConfigurations = "GetVideoSourceConfigurations"
)
func GetRequestAction(b []byte) string {
@@ -42,163 +52,201 @@ func GetRequestAction(b []byte) string {
return string(m[1])
}
-func GetCapabilitiesResponse(host string) string {
- return `
-
-
-
-
-
- http://` + host + `/onvif/device_service
-
-
- http://` + host + `/onvif/media_service
-
- false
- false
- true
-
-
-
-
-
-`
+func GetCapabilitiesResponse(host string) []byte {
+ e := NewEnvelope()
+ e.Append(`
+
+
+ http://`, host, `/onvif/device_service
+
+
+ http://`, host, `/onvif/media_service
+
+ false
+ false
+ true
+
+
+
+`)
+ return e.Bytes()
}
-func GetSystemDateAndTimeResponse() string {
+func GetServicesResponse(host string) []byte {
+ e := NewEnvelope()
+ e.Append(`
+
+ http://www.onvif.org/ver10/device/wsdl
+ http://`, host, `/onvif/device_service
+ 25
+
+
+ http://www.onvif.org/ver10/media/wsdl
+ http://`, host, `/onvif/media_service
+ 25
+
+`)
+ return e.Bytes()
+}
+
+func GetSystemDateAndTimeResponse() []byte {
loc := time.Now()
utc := loc.UTC()
- return fmt.Sprintf(`
-
-
-
-
- NTP
- false
-
- GMT%s
-
-
-
- %d
- %d
- %d
-
-
- %d
- %d
- %d
-
-
-
-
- %d
- %d
- %d
-
-
- %d
- %d
- %d
-
-
-
-
-
-`,
- loc.Format("-07:00"),
+ e := NewEnvelope()
+ e.Appendf(`
+
+ NTP
+ true
+
+ %s
+
+
+ %d%d%d
+ %d%d%d
+
+
+ %d%d%d
+ %d%d%d
+
+
+`,
+ GetPosixTZ(loc),
utc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(),
loc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(),
)
+ return e.Bytes()
}
-func GetNetworkInterfacesResponse() string {
- return `
-
-
-
-
-`
+func GetDeviceInformationResponse(manuf, model, firmware, serial string) []byte {
+ e := NewEnvelope()
+ e.Append(`
+ `, manuf, `
+ `, model, `
+ `, firmware, `
+ `, serial, `
+ 1.00
+`)
+ return e.Bytes()
}
-func GetDeviceInformationResponse(manuf, model, firmware, serial string) string {
- return `
-
-
-
- ` + manuf + `
- ` + model + `
- ` + firmware + `
- ` + serial + `
- 1.00
-
-
-`
+func GetMediaServiceCapabilitiesResponse() []byte {
+ e := NewEnvelope()
+ e.Append(`
+
+
+
+`)
+ return e.Bytes()
}
-func GetServiceCapabilitiesResponse() string {
- return `
-
-
-
-
-
-
-
-
-`
+func GetProfilesResponse(names []string) []byte {
+ e := NewEnvelope()
+ e.Append(`
+`)
+ for _, name := range names {
+ appendProfile(e, "Profiles", name)
+ }
+ e.Append(``)
+ return e.Bytes()
}
-func SystemRebootResponse() string {
- return `
-
-
-
- system reboot in 1 second...
-
-
-`
+func GetProfileResponse(name string) []byte {
+ e := NewEnvelope()
+ e.Append(`
+`)
+ appendProfile(e, "Profile", name)
+ e.Append(``)
+ return e.Bytes()
}
-func GetProfilesResponse(names []string) string {
- buf := bytes.NewBuffer(nil)
- buf.WriteString(`
-
-
- `)
+func appendProfile(e *Envelope, tag, name string) {
+ // empty `RateControl` important for UniFi Protect
+ e.Append(`
+ `, name, `
+
+ VSC
+ `, name, `
+
+
+
+ VEC
+ H264
+ 19201080
+
+
+
+`)
+}
- for i, name := range names {
- buf.WriteString(`
-
- ` + name + `
-
- H264
-
- 1920
- 1080
-
-
- `)
+func GetVideoSourceConfigurationResponse(name string) []byte {
+ e := NewEnvelope()
+ e.Append(`
+
+ VSC
+ `, name, `
+
+
+`)
+ return e.Bytes()
+}
+
+func GetVideoSourcesResponse(names []string) []byte {
+ e := NewEnvelope()
+ e.Append(`
+`)
+ for _, name := range names {
+ e.Append(`
+ 30.000000
+ 19201080
+
+`)
+ }
+ e.Append(``)
+ return e.Bytes()
+}
+
+func GetStreamUriResponse(uri string) []byte {
+ e := NewEnvelope()
+ e.Append(``, uri, ``)
+ return e.Bytes()
+}
+
+func GetSnapshotUriResponse(uri string) []byte {
+ e := NewEnvelope()
+ e.Append(``, uri, ``)
+ return e.Bytes()
+}
+
+func StaticResponse(operation string) []byte {
+ switch operation {
+ case DeviceGetSystemDateAndTime:
+ return GetSystemDateAndTimeResponse()
}
- buf.WriteString(`
-
-
-`)
-
- return buf.String()
+ e := NewEnvelope()
+ e.Append(responses[operation])
+ b := e.Bytes()
+ if operation == DeviceGetNetworkInterfaces {
+ println()
+ }
+ return b
}
-func GetStreamUriResponse(uri string) string {
- return `
-
-
-
-
- ` + uri + `
-
-
-
-`
+var responses = map[string]string{
+ DeviceGetDiscoveryMode: `Discoverable`,
+ DeviceGetDNS: ``,
+ DeviceGetHostname: ``,
+ DeviceGetNetworkDefaultGateway: ``,
+ DeviceGetNTP: ``,
+ DeviceSystemReboot: `OK`,
+
+ DeviceGetNetworkInterfaces: ``,
+ DeviceGetNetworkProtocols: ``,
+ DeviceGetScopes: `
+ Fixedonvif://www.onvif.org/name/go2rtc
+ Fixedonvif://www.onvif.org/location/github
+ Fixedonvif://www.onvif.org/Profile/Streaming
+ Fixedonvif://www.onvif.org/type/Network_Video_Transmitter
+`,
}
diff --git a/pkg/rtmp/server.go b/pkg/rtmp/server.go
index ed727b98..3dcd4048 100644
--- a/pkg/rtmp/server.go
+++ b/pkg/rtmp/server.go
@@ -117,10 +117,6 @@ func (c *Conn) acceptCommand(b []byte) error {
}
}
- if c.App == "" {
- return fmt.Errorf("rtmp: read command %x", b)
- }
-
payload := amf.EncodeItems(
"_result", tID,
map[string]any{"fmsVer": "FMS/3,0,1,123"},
@@ -129,9 +125,16 @@ func (c *Conn) acceptCommand(b []byte) error {
return c.writeMessage(3, TypeCommand, 0, payload)
case CommandReleaseStream:
+ // if app is empty - will use key as app
+ if c.App == "" && len(items) == 4 {
+ c.App, _ = items[3].(string)
+ }
+
payload := amf.EncodeItems("_result", tID, nil)
return c.writeMessage(3, TypeCommand, 0, payload)
+ case CommandFCPublish: // no response
+
case CommandCreateStream:
payload := amf.EncodeItems("_result", tID, nil, 1)
return c.writeMessage(3, TypeCommand, 0, payload)
@@ -140,8 +143,6 @@ func (c *Conn) acceptCommand(b []byte) error {
c.Intent = cmd
c.streamID = 1
- case CommandFCPublish: // no response
-
default:
println("rtmp: unknown command: " + cmd)
}
diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go
index 6b07342d..346ecf73 100644
--- a/pkg/rtsp/helpers.go
+++ b/pkg/rtsp/helpers.go
@@ -70,8 +70,15 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
// Check buggy SDP with fmtp for H264 on another track
// https://github.com/AlexxIT/WebRTC/issues/419
for _, codec := range media.Codecs {
- if codec.Name == core.CodecH264 && codec.FmtpLine == "" {
- codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
+ switch codec.Name {
+ case core.CodecH264:
+ if codec.FmtpLine == "" {
+ codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
+ }
+ case core.CodecOpus:
+ // fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587
+ codec.ClockRate = 48000
+ codec.Channels = 2
}
}
diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go
index 3585011c..6ccafe4e 100644
--- a/pkg/tapo/client.go
+++ b/pkg/tapo/client.go
@@ -27,7 +27,7 @@ import (
type Client struct {
core.Listener
- url string
+ url *url.URL
medias []*core.Media
receivers []*core.Receiver
@@ -52,17 +52,15 @@ type cbcMode interface {
SetIV([]byte)
}
-func Dial(url string) (*Client, error) {
- var err error
- c := &Client{url: url}
- if c.conn1, err = c.newConn(); err != nil {
- return nil, err
- }
- return c, nil
-}
-
-func (c *Client) newConn() (net.Conn, error) {
- u, err := url.Parse(c.url)
+// Dial support different urls:
+// - tapo://{cloud-password}@192.168.1.123 - auth to Tapo cameras
+// with cloud password (autodetect hash method)
+// - tapo://admin:{hashed-cloud-password}@192.168.1.123 - auth to Tapo cameras
+// with pre-hashed cloud password
+// - vigi://admin:{password}@192.168.1.123 - auth to Vigi cameras with password
+// for admin account (other not supported)
+func Dial(rawURL string) (*Client, error) {
+ u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
@@ -71,21 +69,31 @@ func (c *Client) newConn() (net.Conn, error) {
u.Host += ":8800"
}
- req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil)
+ c := &Client{url: u}
+ if c.conn1, err = c.newConn(); err != nil {
+ return nil, err
+ }
+ return c, nil
+}
+
+func (c *Client) newConn() (net.Conn, error) {
+ req, err := http.NewRequest("POST", "http://"+c.url.Host+"/stream", nil)
if err != nil {
return nil, err
}
- query := u.Query()
+ query := c.url.Query()
if deviceId := query.Get("deviceId"); deviceId != "" {
req.URL.RawQuery = "deviceId=" + deviceId
}
- req.URL.User = u.User
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
- conn, res, err := dial(req)
+ username := c.url.User.Username()
+ password, _ := c.url.User.Password()
+
+ conn, res, err := dial(req, c.url.Scheme, username, password)
if err != nil {
return nil, err
}
@@ -95,7 +103,7 @@ func (c *Client) newConn() (net.Conn, error) {
}
if c.decrypt == nil {
- c.newDectypter(res)
+ c.newDectypter(res, c.url.Scheme, username, password)
}
channel := query.Get("channel")
@@ -119,14 +127,18 @@ func (c *Client) newConn() (net.Conn, error) {
return conn, nil
}
-func (c *Client) newDectypter(res *http.Response) {
- username := res.Request.URL.User.Username()
- password, _ := res.Request.URL.User.Password()
+func (c *Client) newDectypter(res *http.Response, brand, username, password string) {
+ exchange := res.Header.Get("Key-Exchange")
+ nonce := core.Between(exchange, `nonce="`, `"`)
- // extract nonce from response
- // cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***"
- nonce := res.Header.Get("Key-Exchange")
- nonce = core.Between(nonce, `nonce="`, `"`)
+ if brand == "tapo" && password == "" {
+ if strings.Contains(exchange, `encrypt_type="3"`) {
+ password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
+ } else {
+ password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
+ }
+ username = "admin"
+ }
key := md5.Sum([]byte(nonce + ":" + password))
iv := md5.Sum([]byte(username + ":" + nonce))
@@ -263,16 +275,12 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
}
}
-func dial(req *http.Request) (net.Conn, *http.Response, error) {
+func dial(req *http.Request, brand, username, password string) (net.Conn, *http.Response, error) {
conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
if err != nil {
return nil, nil, err
}
- username := req.URL.User.Username()
- password, _ := req.URL.User.Password()
- req.URL.User = nil
-
if err = req.Write(conn); err != nil {
return nil, nil, err
}
@@ -291,7 +299,7 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode)
}
- if password == "" {
+ if brand == "tapo" && password == "" {
// support cloud password in place of username
if strings.Contains(auth, `encrypt_type="3"`) {
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
@@ -299,6 +307,8 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
}
username = "admin"
+ } else if brand == "vigi" && username == "admin" {
+ password = securityEncode(password)
}
realm := tcp.Between(auth, `realm="`, `"`)
@@ -331,7 +341,39 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
return nil, nil, err
}
- req.URL.User = url.UserPassword(username, password)
-
return conn, res, nil
}
+
+const (
+ keyShort = "RDpbLfCPsJZ7fiv"
+ keyLong = "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW"
+)
+
+func securityEncode(s string) string {
+ size := len(s)
+
+ var n int // max
+ if size > len(keyShort) {
+ n = size
+ } else {
+ n = len(keyShort)
+ }
+
+ b := make([]byte, n)
+
+ for i := 0; i < n; i++ {
+ c1 := 187
+ c2 := 187
+ if i >= size {
+ c1 = int(keyShort[i])
+ } else if i >= len(keyShort) {
+ c2 = int(s[i])
+ } else {
+ c1 = int(keyShort[i])
+ c2 = int(s[i])
+ }
+ b[i] = keyLong[(c1^c2)%len(keyLong)]
+ }
+
+ return string(b)
+}
diff --git a/pkg/tapo/producer.go b/pkg/tapo/producer.go
index 7d66d907..87a91ff5 100644
--- a/pkg/tapo/producer.go
+++ b/pkg/tapo/producer.go
@@ -77,7 +77,7 @@ func (c *Client) Stop() error {
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Connection{
ID: core.ID(c),
- FormatName: "tapo",
+ FormatName: c.url.Scheme,
Protocol: "http",
Medias: c.medias,
Recv: c.recv,
diff --git a/pkg/v4l2/device/README.md b/pkg/v4l2/device/README.md
new file mode 100644
index 00000000..de686ea0
--- /dev/null
+++ b/pkg/v4l2/device/README.md
@@ -0,0 +1,21 @@
+# Video For Linux Two
+
+Build on Ubuntu
+
+```bash
+sudo apt install gcc-x86-64-linux-gnu
+sudo apt install gcc-i686-linux-gnu
+sudo apt install gcc-aarch64-linux-gnu binutils
+sudo apt install gcc-arm-linux-gnueabihf
+sudo apt install gcc-mipsel-linux-gnu
+
+x86_64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_x86_64
+i686-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_i686
+aarch64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_aarch64
+arm-linux-gnueabihf-gcc -w -static videodev2_arch.c -o videodev2_armhf
+mipsel-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_mipsel -D_TIME_BITS=32
+```
+
+## Useful links
+
+- https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h
diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go
new file mode 100644
index 00000000..7f16fd23
--- /dev/null
+++ b/pkg/v4l2/device/device.go
@@ -0,0 +1,244 @@
+//go:build linux
+
+package device
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "syscall"
+ "unsafe"
+)
+
+type Device struct {
+ fd int
+ bufs [][]byte
+}
+
+func Open(path string) (*Device, error) {
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CLOEXEC, 0)
+ if err != nil {
+ return nil, err
+ }
+ return &Device{fd: fd}, nil
+}
+
+const buffersCount = 2
+
+type Capability struct {
+ Driver string
+ Card string
+ BusInfo string
+ Version string
+}
+
+func (d *Device) Capability() (*Capability, error) {
+ c := v4l2_capability{}
+ if err := ioctl(d.fd, VIDIOC_QUERYCAP, unsafe.Pointer(&c)); err != nil {
+ return nil, err
+ }
+ return &Capability{
+ Driver: str(c.driver[:]),
+ Card: str(c.card[:]),
+ BusInfo: str(c.bus_info[:]),
+ Version: fmt.Sprintf("%d.%d.%d", byte(c.version>>16), byte(c.version>>8), byte(c.version)),
+ }, nil
+}
+
+func (d *Device) ListFormats() ([]uint32, error) {
+ var items []uint32
+
+ for i := uint32(0); ; i++ {
+ fd := v4l2_fmtdesc{
+ index: i,
+ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ }
+ if err := ioctl(d.fd, VIDIOC_ENUM_FMT, unsafe.Pointer(&fd)); err != nil {
+ if !errors.Is(err, syscall.EINVAL) {
+ return nil, err
+ }
+ break
+ }
+
+ items = append(items, fd.pixelformat)
+ }
+
+ return items, nil
+}
+
+func (d *Device) ListSizes(pixFmt uint32) ([][2]uint32, error) {
+ var items [][2]uint32
+
+ for i := uint32(0); ; i++ {
+ fs := v4l2_frmsizeenum{
+ index: i,
+ pixel_format: pixFmt,
+ }
+ if err := ioctl(d.fd, VIDIOC_ENUM_FRAMESIZES, unsafe.Pointer(&fs)); err != nil {
+ if !errors.Is(err, syscall.EINVAL) {
+ return nil, err
+ }
+ break
+ }
+
+ if fs.typ != V4L2_FRMSIZE_TYPE_DISCRETE {
+ continue
+ }
+
+ items = append(items, [2]uint32{fs.discrete.width, fs.discrete.height})
+ }
+
+ return items, nil
+}
+
+func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) {
+ var items []uint32
+
+ for i := uint32(0); ; i++ {
+ fi := v4l2_frmivalenum{
+ index: i,
+ pixel_format: pixFmt,
+ width: width,
+ height: height,
+ }
+ if err := ioctl(d.fd, VIDIOC_ENUM_FRAMEINTERVALS, unsafe.Pointer(&fi)); err != nil {
+ if !errors.Is(err, syscall.EINVAL) {
+ return nil, err
+ }
+ break
+ }
+
+ if fi.typ != V4L2_FRMIVAL_TYPE_DISCRETE || fi.discrete.numerator != 1 {
+ continue
+ }
+
+ items = append(items, fi.discrete.denominator)
+ }
+
+ return items, nil
+}
+
+func (d *Device) SetFormat(width, height, pixFmt uint32) error {
+ f := v4l2_format{
+ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ pix: v4l2_pix_format{
+ width: width,
+ height: height,
+ pixelformat: pixFmt,
+ field: V4L2_FIELD_NONE,
+ colorspace: V4L2_COLORSPACE_DEFAULT,
+ },
+ }
+ return ioctl(d.fd, VIDIOC_S_FMT, unsafe.Pointer(&f))
+}
+
+func (d *Device) SetParam(fps uint32) error {
+ p := v4l2_streamparm{
+ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ capture: v4l2_captureparm{
+ timeperframe: v4l2_fract{numerator: 1, denominator: fps},
+ },
+ }
+ return ioctl(d.fd, VIDIOC_S_PARM, unsafe.Pointer(&p))
+}
+
+func (d *Device) StreamOn() (err error) {
+ rb := v4l2_requestbuffers{
+ count: buffersCount,
+ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ memory: V4L2_MEMORY_MMAP,
+ }
+ if err = ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)); err != nil {
+ return err
+ }
+
+ d.bufs = make([][]byte, buffersCount)
+ for i := uint32(0); i < buffersCount; i++ {
+ qb := v4l2_buffer{
+ index: i,
+ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ memory: V4L2_MEMORY_MMAP,
+ }
+ if err = ioctl(d.fd, VIDIOC_QUERYBUF, unsafe.Pointer(&qb)); err != nil {
+ return err
+ }
+
+ if d.bufs[i], err = syscall.Mmap(
+ d.fd, int64(qb.offset), int(qb.length), syscall.PROT_READ, syscall.MAP_SHARED,
+ ); nil != err {
+ return err
+ }
+
+ if err = ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&qb)); err != nil {
+ return err
+ }
+ }
+
+ typ := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return ioctl(d.fd, VIDIOC_STREAMON, unsafe.Pointer(&typ))
+}
+
+func (d *Device) StreamOff() (err error) {
+ typ := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ if err = ioctl(d.fd, VIDIOC_STREAMOFF, unsafe.Pointer(&typ)); err != nil {
+ return err
+ }
+
+ for i := range d.bufs {
+ _ = syscall.Munmap(d.bufs[i])
+ }
+
+ rb := v4l2_requestbuffers{
+ count: 0,
+ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ memory: V4L2_MEMORY_MMAP,
+ }
+ return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb))
+}
+
+func (d *Device) Capture(planarYUV bool) ([]byte, error) {
+ dec := v4l2_buffer{
+ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ memory: V4L2_MEMORY_MMAP,
+ }
+ if err := ioctl(d.fd, VIDIOC_DQBUF, unsafe.Pointer(&dec)); err != nil {
+ return nil, err
+ }
+
+ buf := make([]byte, dec.bytesused)
+ if planarYUV {
+ YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused])
+ } else {
+ copy(buf, d.bufs[dec.index][:dec.bytesused])
+ }
+
+ enc := v4l2_buffer{
+ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ memory: V4L2_MEMORY_MMAP,
+ index: dec.index,
+ }
+ if err := ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&enc)); err != nil {
+ return nil, err
+ }
+
+ return buf, nil
+}
+
+func (d *Device) Close() error {
+ return syscall.Close(d.fd)
+}
+
+func ioctl(fd int, req uint, arg unsafe.Pointer) error {
+ _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
+ if err != 0 {
+ return err
+ }
+ return nil
+}
+
+func str(b []byte) string {
+ if i := bytes.IndexByte(b, 0); i >= 0 {
+ return string(b[:i])
+ }
+ return string(b)
+}
diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go
new file mode 100644
index 00000000..fb54bbd1
--- /dev/null
+++ b/pkg/v4l2/device/formats.go
@@ -0,0 +1,40 @@
+package device
+
+const (
+ V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24
+ V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24
+)
+
+type Format struct {
+ FourCC uint32
+ Name string
+ FFmpeg string
+}
+
+var Formats = []Format{
+ {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"},
+ {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"},
+}
+
+// YUYV2YUV convert packed YUV to planar YUV
+func YUYV2YUV(dst, src []byte) {
+ n := len(src)
+ i0 := 0
+ iy := 0
+ iu := n / 2
+ iv := n / 4 * 3
+ for i0 < n {
+ dst[iy] = src[i0]
+ i0++
+ iy++
+ dst[iu] = src[i0]
+ i0++
+ iu++
+ dst[iy] = src[i0]
+ i0++
+ iy++
+ dst[iv] = src[i0]
+ i0++
+ iv++
+ }
+}
diff --git a/pkg/v4l2/device/videodev2_386.go b/pkg/v4l2/device/videodev2_386.go
new file mode 100644
index 00000000..8737ca9d
--- /dev/null
+++ b/pkg/v4l2/device/videodev2_386.go
@@ -0,0 +1,149 @@
+package device
+
+const (
+ VIDIOC_QUERYCAP = 0x80685600
+ VIDIOC_ENUM_FMT = 0xc0405602
+ VIDIOC_G_FMT = 0xc0cc5604
+ VIDIOC_S_FMT = 0xc0cc5605
+ VIDIOC_REQBUFS = 0xc0145608
+ VIDIOC_QUERYBUF = 0xc0445609
+
+ VIDIOC_QBUF = 0xc044560f
+ VIDIOC_DQBUF = 0xc0445611
+ VIDIOC_STREAMON = 0x40045612
+ VIDIOC_STREAMOFF = 0x40045613
+ VIDIOC_G_PARM = 0xc0cc5615
+ VIDIOC_S_PARM = 0xc0cc5616
+
+ VIDIOC_ENUM_FRAMESIZES = 0xc02c564a
+ VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b
+)
+
+const (
+ V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
+ V4L2_COLORSPACE_DEFAULT = 0
+ V4L2_FIELD_NONE = 1
+ V4L2_FRMIVAL_TYPE_DISCRETE = 1
+ V4L2_FRMSIZE_TYPE_DISCRETE = 1
+ V4L2_MEMORY_MMAP = 1
+)
+
+type v4l2_capability struct { // size 104
+ driver [16]byte // offset 0, size 16
+ card [32]byte // offset 16, size 32
+ bus_info [32]byte // offset 48, size 32
+ version uint32 // offset 80, size 4
+ capabilities uint32 // offset 84, size 4
+ device_caps uint32 // offset 88, size 4
+ reserved [3]uint32 // offset 92, size 12
+}
+
+type v4l2_format struct { // size 204
+ typ uint32 // offset 0, size 4
+ _ [0]byte // align
+ pix v4l2_pix_format // offset 4, size 48
+ _ [152]byte // filler
+}
+
+type v4l2_pix_format struct { // size 48
+ width uint32 // offset 0, size 4
+ height uint32 // offset 4, size 4
+ pixelformat uint32 // offset 8, size 4
+ field uint32 // offset 12, size 4
+ bytesperline uint32 // offset 16, size 4
+ sizeimage uint32 // offset 20, size 4
+ colorspace uint32 // offset 24, size 4
+ priv uint32 // offset 28, size 4
+ flags uint32 // offset 32, size 4
+ ycbcr_enc uint32 // offset 36, size 4
+ quantization uint32 // offset 40, size 4
+ xfer_func uint32 // offset 44, size 4
+}
+
+type v4l2_streamparm struct { // size 204
+ typ uint32 // offset 0, size 4
+ capture v4l2_captureparm // offset 4, size 40
+ _ [160]byte // filler
+}
+
+type v4l2_captureparm struct { // size 40
+ capability uint32 // offset 0, size 4
+ capturemode uint32 // offset 4, size 4
+ timeperframe v4l2_fract // offset 8, size 8
+ extendedmode uint32 // offset 16, size 4
+ readbuffers uint32 // offset 20, size 4
+ reserved [4]uint32 // offset 24, size 16
+}
+
+type v4l2_fract struct { // size 8
+ numerator uint32 // offset 0, size 4
+ denominator uint32 // offset 4, size 4
+}
+
+type v4l2_requestbuffers struct { // size 20
+ count uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ memory uint32 // offset 8, size 4
+ capabilities uint32 // offset 12, size 4
+ flags uint8 // offset 16, size 1
+ reserved [3]uint8 // offset 17, size 3
+}
+
+type v4l2_buffer struct { // size 68
+ index uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ bytesused uint32 // offset 8, size 4
+ flags uint32 // offset 12, size 4
+ field uint32 // offset 16, size 4
+ _ [8]byte // align
+ timecode v4l2_timecode // offset 28, size 16
+ sequence uint32 // offset 44, size 4
+ memory uint32 // offset 48, size 4
+ offset uint32 // offset 52, size 4
+ _ [0]byte // align
+ length uint32 // offset 56, size 4
+ _ [8]byte // filler
+}
+
+type v4l2_timecode struct { // size 16
+ typ uint32 // offset 0, size 4
+ flags uint32 // offset 4, size 4
+ frames uint8 // offset 8, size 1
+ seconds uint8 // offset 9, size 1
+ minutes uint8 // offset 10, size 1
+ hours uint8 // offset 11, size 1
+ userbits [4]uint8 // offset 12, size 4
+}
+
+type v4l2_fmtdesc struct { // size 64
+ index uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ flags uint32 // offset 8, size 4
+ description [32]byte // offset 12, size 32
+ pixelformat uint32 // offset 44, size 4
+ mbus_code uint32 // offset 48, size 4
+ reserved [3]uint32 // offset 52, size 12
+}
+
+type v4l2_frmsizeenum struct { // size 44
+ index uint32 // offset 0, size 4
+ pixel_format uint32 // offset 4, size 4
+ typ uint32 // offset 8, size 4
+ discrete v4l2_frmsize_discrete // offset 12, size 8
+ _ [24]byte // filler
+}
+
+type v4l2_frmsize_discrete struct { // size 8
+ width uint32 // offset 0, size 4
+ height uint32 // offset 4, size 4
+}
+
+type v4l2_frmivalenum struct { // size 52
+ index uint32 // offset 0, size 4
+ pixel_format uint32 // offset 4, size 4
+ width uint32 // offset 8, size 4
+ height uint32 // offset 12, size 4
+ typ uint32 // offset 16, size 4
+ discrete v4l2_fract // offset 20, size 8
+ _ [24]byte // filler
+}
diff --git a/pkg/v4l2/device/videodev2_arch.c b/pkg/v4l2/device/videodev2_arch.c
new file mode 100644
index 00000000..1053a088
--- /dev/null
+++ b/pkg/v4l2/device/videodev2_arch.c
@@ -0,0 +1,163 @@
+#include
+#include
+#include
+
+#define printconst1(con) printf("\t%s = 0x%08lx\n", #con, con)
+#define printconst2(con) printf("\t%s = %d\n", #con, con)
+#define printstruct(str) printf("type %s struct { // size %lu\n", #str, sizeof(struct str))
+#define printmember(str, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem == "type" ? "typ" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem))
+#define printunimem(str, uni, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem, typ, offsetof(struct str, uni.mem), sizeof((struct str){0}.uni.mem))
+#define printalign1(str, mem2, mem1) printf("\t_ [%lu]byte // align\n", offsetof(struct str, mem2) - offsetof(struct str, mem1) - sizeof((struct str){0}.mem1))
+#define printfiller(str, mem) printf("\t_ [%lu]byte // filler\n", sizeof(struct str) - offsetof(struct str, mem) - sizeof((struct str){0}.mem))
+
+int main() {
+ printf("const (\n");
+ printconst1(VIDIOC_QUERYCAP);
+ printconst1(VIDIOC_ENUM_FMT);
+ printconst1(VIDIOC_G_FMT);
+ printconst1(VIDIOC_S_FMT);
+ printconst1(VIDIOC_REQBUFS);
+ printconst1(VIDIOC_QUERYBUF);
+ printf("\n");
+ printconst1(VIDIOC_QBUF);
+ printconst1(VIDIOC_DQBUF);
+ printconst1(VIDIOC_STREAMON);
+ printconst1(VIDIOC_STREAMOFF);
+ printconst1(VIDIOC_G_PARM);
+ printconst1(VIDIOC_S_PARM);
+ printf("\n");
+ printconst1(VIDIOC_ENUM_FRAMESIZES);
+ printconst1(VIDIOC_ENUM_FRAMEINTERVALS);
+ printf(")\n\n");
+
+ printf("const (\n");
+ printconst2(V4L2_BUF_TYPE_VIDEO_CAPTURE);
+ printconst2(V4L2_COLORSPACE_DEFAULT);
+ printconst2(V4L2_FIELD_NONE);
+ printconst2(V4L2_FRMIVAL_TYPE_DISCRETE);
+ printconst2(V4L2_FRMSIZE_TYPE_DISCRETE);
+ printconst2(V4L2_MEMORY_MMAP);
+ printf(")\n\n");
+
+ printstruct(v4l2_capability);
+ printmember(v4l2_capability, driver, "[16]byte");
+ printmember(v4l2_capability, card, "[32]byte");
+ printmember(v4l2_capability, bus_info, "[32]byte");
+ printmember(v4l2_capability, version, "uint32");
+ printmember(v4l2_capability, capabilities, "uint32");
+ printmember(v4l2_capability, device_caps, "uint32");
+ printmember(v4l2_capability, reserved, "[3]uint32");
+ printf("}\n\n");
+
+ printstruct(v4l2_format);
+ printmember(v4l2_format, type, "uint32");
+ printalign1(v4l2_format, fmt, type);
+ printunimem(v4l2_format, fmt, pix, "v4l2_pix_format");
+ printfiller(v4l2_format, fmt.pix);
+ printf("}\n\n");
+
+ printstruct(v4l2_pix_format);
+ printmember(v4l2_pix_format, width, "uint32");
+ printmember(v4l2_pix_format, height, "uint32");
+ printmember(v4l2_pix_format, pixelformat, "uint32");
+ printmember(v4l2_pix_format, field, "uint32");
+ printmember(v4l2_pix_format, bytesperline, "uint32");
+ printmember(v4l2_pix_format, sizeimage, "uint32");
+ printmember(v4l2_pix_format, colorspace, "uint32");
+ printmember(v4l2_pix_format, priv, "uint32");
+ printmember(v4l2_pix_format, flags, "uint32");
+ printmember(v4l2_pix_format, ycbcr_enc, "uint32");
+ printmember(v4l2_pix_format, quantization, "uint32");
+ printmember(v4l2_pix_format, xfer_func, "uint32");
+ printf("}\n\n");
+
+ printstruct(v4l2_streamparm);
+ printmember(v4l2_streamparm, type, "uint32");
+ printunimem(v4l2_streamparm, parm, capture, "v4l2_captureparm");
+ printfiller(v4l2_streamparm, parm.capture);
+ printf("}\n\n");
+
+ printstruct(v4l2_captureparm);
+ printmember(v4l2_captureparm, capability, "uint32");
+ printmember(v4l2_captureparm, capturemode, "uint32");
+ printmember(v4l2_captureparm, timeperframe, "v4l2_fract");
+ printmember(v4l2_captureparm, extendedmode, "uint32");
+ printmember(v4l2_captureparm, readbuffers, "uint32");
+ printmember(v4l2_captureparm, reserved, "[4]uint32");
+ printf("}\n\n");
+
+ printstruct(v4l2_fract);
+ printmember(v4l2_fract, numerator, "uint32");
+ printmember(v4l2_fract, denominator, "uint32");
+ printf("}\n\n");
+
+ printstruct(v4l2_requestbuffers);
+ printmember(v4l2_requestbuffers, count, "uint32");
+ printmember(v4l2_requestbuffers, type, "uint32");
+ printmember(v4l2_requestbuffers, memory, "uint32");
+ printmember(v4l2_requestbuffers, capabilities, "uint32");
+ printmember(v4l2_requestbuffers, flags, "uint8");
+ printmember(v4l2_requestbuffers, reserved, "[3]uint8");
+ printf("}\n\n");
+
+ printstruct(v4l2_buffer);
+ printmember(v4l2_buffer, index, "uint32");
+ printmember(v4l2_buffer, type, "uint32");
+ printmember(v4l2_buffer, bytesused, "uint32");
+ printmember(v4l2_buffer, flags, "uint32");
+ printmember(v4l2_buffer, field, "uint32");
+ printalign1(v4l2_buffer, timecode, field);
+ printmember(v4l2_buffer, timecode, "v4l2_timecode");
+ printmember(v4l2_buffer, sequence, "uint32");
+ printmember(v4l2_buffer, memory, "uint32");
+ printunimem(v4l2_buffer, m, offset, "uint32");
+ printalign1(v4l2_buffer, length, m.offset);
+ printmember(v4l2_buffer, length, "uint32");
+ printfiller(v4l2_buffer, length);
+ printf("}\n\n");
+
+ printstruct(v4l2_timecode);
+ printmember(v4l2_timecode, type, "uint32");
+ printmember(v4l2_timecode, flags, "uint32");
+ printmember(v4l2_timecode, frames, "uint8");
+ printmember(v4l2_timecode, seconds, "uint8");
+ printmember(v4l2_timecode, minutes, "uint8");
+ printmember(v4l2_timecode, hours, "uint8");
+ printmember(v4l2_timecode, userbits, "[4]uint8");
+ printf("}\n\n");
+
+ printstruct(v4l2_fmtdesc);
+ printmember(v4l2_fmtdesc, index, "uint32");
+ printmember(v4l2_fmtdesc, type, "uint32");
+ printmember(v4l2_fmtdesc, flags, "uint32");
+ printmember(v4l2_fmtdesc, description, "[32]byte");
+ printmember(v4l2_fmtdesc, pixelformat, "uint32");
+ printmember(v4l2_fmtdesc, mbus_code, "uint32");
+ printmember(v4l2_fmtdesc, reserved, "[3]uint32");
+ printf("}\n\n");
+
+ printstruct(v4l2_frmsizeenum);
+ printmember(v4l2_frmsizeenum, index, "uint32");
+ printmember(v4l2_frmsizeenum, pixel_format, "uint32");
+ printmember(v4l2_frmsizeenum, type, "uint32");
+ printmember(v4l2_frmsizeenum, discrete, "v4l2_frmsize_discrete");
+ printfiller(v4l2_frmsizeenum, discrete);
+ printf("}\n\n");
+
+ printstruct(v4l2_frmsize_discrete);
+ printmember(v4l2_frmsize_discrete, width, "uint32");
+ printmember(v4l2_frmsize_discrete, height, "uint32");
+ printf("}\n\n");
+
+ printstruct(v4l2_frmivalenum);
+ printmember(v4l2_frmivalenum, index, "uint32");
+ printmember(v4l2_frmivalenum, pixel_format, "uint32");
+ printmember(v4l2_frmivalenum, width, "uint32");
+ printmember(v4l2_frmivalenum, height, "uint32");
+ printmember(v4l2_frmivalenum, type, "uint32");
+ printmember(v4l2_frmivalenum, discrete, "v4l2_fract");
+ printfiller(v4l2_frmivalenum, discrete);
+ printf("}\n\n");
+
+ return 0;
+}
\ No newline at end of file
diff --git a/pkg/v4l2/device/videodev2_arm.go b/pkg/v4l2/device/videodev2_arm.go
new file mode 100644
index 00000000..098ca5a3
--- /dev/null
+++ b/pkg/v4l2/device/videodev2_arm.go
@@ -0,0 +1,149 @@
+package device
+
+const (
+ VIDIOC_QUERYCAP = 0x80685600
+ VIDIOC_ENUM_FMT = 0xc0405602
+ VIDIOC_G_FMT = 0xc0cc5604
+ VIDIOC_S_FMT = 0xc0cc5605
+ VIDIOC_REQBUFS = 0xc0145608
+ VIDIOC_QUERYBUF = 0xc0505609
+
+ VIDIOC_QBUF = 0xc050560f
+ VIDIOC_DQBUF = 0xc0505611
+ VIDIOC_STREAMON = 0x40045612
+ VIDIOC_STREAMOFF = 0x40045613
+ VIDIOC_G_PARM = 0xc0cc5615
+ VIDIOC_S_PARM = 0xc0cc5616
+
+ VIDIOC_ENUM_FRAMESIZES = 0xc02c564a
+ VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b
+)
+
+const (
+ V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
+ V4L2_COLORSPACE_DEFAULT = 0
+ V4L2_FIELD_NONE = 1
+ V4L2_FRMIVAL_TYPE_DISCRETE = 1
+ V4L2_FRMSIZE_TYPE_DISCRETE = 1
+ V4L2_MEMORY_MMAP = 1
+)
+
+type v4l2_capability struct { // size 104
+ driver [16]byte // offset 0, size 16
+ card [32]byte // offset 16, size 32
+ bus_info [32]byte // offset 48, size 32
+ version uint32 // offset 80, size 4
+ capabilities uint32 // offset 84, size 4
+ device_caps uint32 // offset 88, size 4
+ reserved [3]uint32 // offset 92, size 12
+}
+
+type v4l2_format struct { // size 204
+ typ uint32 // offset 0, size 4
+ _ [0]byte // align
+ pix v4l2_pix_format // offset 4, size 48
+ _ [152]byte // filler
+}
+
+type v4l2_pix_format struct { // size 48
+ width uint32 // offset 0, size 4
+ height uint32 // offset 4, size 4
+ pixelformat uint32 // offset 8, size 4
+ field uint32 // offset 12, size 4
+ bytesperline uint32 // offset 16, size 4
+ sizeimage uint32 // offset 20, size 4
+ colorspace uint32 // offset 24, size 4
+ priv uint32 // offset 28, size 4
+ flags uint32 // offset 32, size 4
+ ycbcr_enc uint32 // offset 36, size 4
+ quantization uint32 // offset 40, size 4
+ xfer_func uint32 // offset 44, size 4
+}
+
+type v4l2_streamparm struct { // size 204
+ typ uint32 // offset 0, size 4
+ capture v4l2_captureparm // offset 4, size 40
+ _ [160]byte // filler
+}
+
+type v4l2_captureparm struct { // size 40
+ capability uint32 // offset 0, size 4
+ capturemode uint32 // offset 4, size 4
+ timeperframe v4l2_fract // offset 8, size 8
+ extendedmode uint32 // offset 16, size 4
+ readbuffers uint32 // offset 20, size 4
+ reserved [4]uint32 // offset 24, size 16
+}
+
+type v4l2_fract struct { // size 8
+ numerator uint32 // offset 0, size 4
+ denominator uint32 // offset 4, size 4
+}
+
+type v4l2_requestbuffers struct { // size 20
+ count uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ memory uint32 // offset 8, size 4
+ capabilities uint32 // offset 12, size 4
+ flags uint8 // offset 16, size 1
+ reserved [3]uint8 // offset 17, size 3
+}
+
+type v4l2_buffer struct { // size 80
+ index uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ bytesused uint32 // offset 8, size 4
+ flags uint32 // offset 12, size 4
+ field uint32 // offset 16, size 4
+ _ [20]byte // align
+ timecode v4l2_timecode // offset 40, size 16
+ sequence uint32 // offset 56, size 4
+ memory uint32 // offset 60, size 4
+ offset uint32 // offset 64, size 4
+ _ [0]byte // align
+ length uint32 // offset 68, size 4
+ _ [8]byte // filler
+}
+
+type v4l2_timecode struct { // size 16
+ typ uint32 // offset 0, size 4
+ flags uint32 // offset 4, size 4
+ frames uint8 // offset 8, size 1
+ seconds uint8 // offset 9, size 1
+ minutes uint8 // offset 10, size 1
+ hours uint8 // offset 11, size 1
+ userbits [4]uint8 // offset 12, size 4
+}
+
+type v4l2_fmtdesc struct { // size 64
+ index uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ flags uint32 // offset 8, size 4
+ description [32]byte // offset 12, size 32
+ pixelformat uint32 // offset 44, size 4
+ mbus_code uint32 // offset 48, size 4
+ reserved [3]uint32 // offset 52, size 12
+}
+
+type v4l2_frmsizeenum struct { // size 44
+ index uint32 // offset 0, size 4
+ pixel_format uint32 // offset 4, size 4
+ typ uint32 // offset 8, size 4
+ discrete v4l2_frmsize_discrete // offset 12, size 8
+ _ [24]byte // filler
+}
+
+type v4l2_frmsize_discrete struct { // size 8
+ width uint32 // offset 0, size 4
+ height uint32 // offset 4, size 4
+}
+
+type v4l2_frmivalenum struct { // size 52
+ index uint32 // offset 0, size 4
+ pixel_format uint32 // offset 4, size 4
+ width uint32 // offset 8, size 4
+ height uint32 // offset 12, size 4
+ typ uint32 // offset 16, size 4
+ discrete v4l2_fract // offset 20, size 8
+ _ [24]byte // filler
+}
diff --git a/pkg/v4l2/device/videodev2_mipsle.go b/pkg/v4l2/device/videodev2_mipsle.go
new file mode 100644
index 00000000..cecc54c4
--- /dev/null
+++ b/pkg/v4l2/device/videodev2_mipsle.go
@@ -0,0 +1,149 @@
+package device
+
+const (
+ VIDIOC_QUERYCAP = 0x40685600
+ VIDIOC_ENUM_FMT = 0xc0405602
+ VIDIOC_G_FMT = 0xc0cc5604
+ VIDIOC_S_FMT = 0xc0cc5605
+ VIDIOC_REQBUFS = 0xc0145608
+ VIDIOC_QUERYBUF = 0xc0445609
+
+ VIDIOC_QBUF = 0xc044560f
+ VIDIOC_DQBUF = 0xc0445611
+ VIDIOC_STREAMON = 0x80045612
+ VIDIOC_STREAMOFF = 0x80045613
+ VIDIOC_G_PARM = 0xc0cc5615
+ VIDIOC_S_PARM = 0xc0cc5616
+
+ VIDIOC_ENUM_FRAMESIZES = 0xc02c564a
+ VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b
+)
+
+const (
+ V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
+ V4L2_COLORSPACE_DEFAULT = 0
+ V4L2_FIELD_NONE = 1
+ V4L2_FRMIVAL_TYPE_DISCRETE = 1
+ V4L2_FRMSIZE_TYPE_DISCRETE = 1
+ V4L2_MEMORY_MMAP = 1
+)
+
+type v4l2_capability struct { // size 104
+ driver [16]byte // offset 0, size 16
+ card [32]byte // offset 16, size 32
+ bus_info [32]byte // offset 48, size 32
+ version uint32 // offset 80, size 4
+ capabilities uint32 // offset 84, size 4
+ device_caps uint32 // offset 88, size 4
+ reserved [3]uint32 // offset 92, size 12
+}
+
+type v4l2_format struct { // size 204
+ typ uint32 // offset 0, size 4
+ _ [0]byte // align
+ pix v4l2_pix_format // offset 4, size 48
+ _ [152]byte // filler
+}
+
+type v4l2_pix_format struct { // size 48
+ width uint32 // offset 0, size 4
+ height uint32 // offset 4, size 4
+ pixelformat uint32 // offset 8, size 4
+ field uint32 // offset 12, size 4
+ bytesperline uint32 // offset 16, size 4
+ sizeimage uint32 // offset 20, size 4
+ colorspace uint32 // offset 24, size 4
+ priv uint32 // offset 28, size 4
+ flags uint32 // offset 32, size 4
+ ycbcr_enc uint32 // offset 36, size 4
+ quantization uint32 // offset 40, size 4
+ xfer_func uint32 // offset 44, size 4
+}
+
+type v4l2_streamparm struct { // size 204
+ typ uint32 // offset 0, size 4
+ capture v4l2_captureparm // offset 4, size 40
+ _ [160]byte // filler
+}
+
+type v4l2_captureparm struct { // size 40
+ capability uint32 // offset 0, size 4
+ capturemode uint32 // offset 4, size 4
+ timeperframe v4l2_fract // offset 8, size 8
+ extendedmode uint32 // offset 16, size 4
+ readbuffers uint32 // offset 20, size 4
+ reserved [4]uint32 // offset 24, size 16
+}
+
+type v4l2_fract struct { // size 8
+ numerator uint32 // offset 0, size 4
+ denominator uint32 // offset 4, size 4
+}
+
+type v4l2_requestbuffers struct { // size 20
+ count uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ memory uint32 // offset 8, size 4
+ capabilities uint32 // offset 12, size 4
+ flags uint8 // offset 16, size 1
+ reserved [3]uint8 // offset 17, size 3
+}
+
+type v4l2_buffer struct { // size 68
+ index uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ bytesused uint32 // offset 8, size 4
+ flags uint32 // offset 12, size 4
+ field uint32 // offset 16, size 4
+ _ [8]byte // align
+ timecode v4l2_timecode // offset 28, size 16
+ sequence uint32 // offset 44, size 4
+ memory uint32 // offset 48, size 4
+ offset uint32 // offset 52, size 4
+ _ [0]byte // align
+ length uint32 // offset 56, size 4
+ _ [8]byte // filler
+}
+
+type v4l2_timecode struct { // size 16
+ typ uint32 // offset 0, size 4
+ flags uint32 // offset 4, size 4
+ frames uint8 // offset 8, size 1
+ seconds uint8 // offset 9, size 1
+ minutes uint8 // offset 10, size 1
+ hours uint8 // offset 11, size 1
+ userbits [4]uint8 // offset 12, size 4
+}
+
+type v4l2_fmtdesc struct { // size 64
+ index uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ flags uint32 // offset 8, size 4
+ description [32]byte // offset 12, size 32
+ pixelformat uint32 // offset 44, size 4
+ mbus_code uint32 // offset 48, size 4
+ reserved [3]uint32 // offset 52, size 12
+}
+
+type v4l2_frmsizeenum struct { // size 44
+ index uint32 // offset 0, size 4
+ pixel_format uint32 // offset 4, size 4
+ typ uint32 // offset 8, size 4
+ discrete v4l2_frmsize_discrete // offset 12, size 8
+ _ [24]byte // filler
+}
+
+type v4l2_frmsize_discrete struct { // size 8
+ width uint32 // offset 0, size 4
+ height uint32 // offset 4, size 4
+}
+
+type v4l2_frmivalenum struct { // size 52
+ index uint32 // offset 0, size 4
+ pixel_format uint32 // offset 4, size 4
+ width uint32 // offset 8, size 4
+ height uint32 // offset 12, size 4
+ typ uint32 // offset 16, size 4
+ discrete v4l2_fract // offset 20, size 8
+ _ [24]byte // filler
+}
diff --git a/pkg/v4l2/device/videodev2_x64.go b/pkg/v4l2/device/videodev2_x64.go
new file mode 100644
index 00000000..6e1018e0
--- /dev/null
+++ b/pkg/v4l2/device/videodev2_x64.go
@@ -0,0 +1,151 @@
+//go:build amd64 || arm64
+
+package device
+
+const (
+ VIDIOC_QUERYCAP = 0x80685600
+ VIDIOC_ENUM_FMT = 0xc0405602
+ VIDIOC_G_FMT = 0xc0d05604
+ VIDIOC_S_FMT = 0xc0d05605
+ VIDIOC_REQBUFS = 0xc0145608
+ VIDIOC_QUERYBUF = 0xc0585609
+
+ VIDIOC_QBUF = 0xc058560f
+ VIDIOC_DQBUF = 0xc0585611
+ VIDIOC_STREAMON = 0x40045612
+ VIDIOC_STREAMOFF = 0x40045613
+ VIDIOC_G_PARM = 0xc0cc5615
+ VIDIOC_S_PARM = 0xc0cc5616
+
+ VIDIOC_ENUM_FRAMESIZES = 0xc02c564a
+ VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b
+)
+
+const (
+ V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
+ V4L2_COLORSPACE_DEFAULT = 0
+ V4L2_FIELD_NONE = 1
+ V4L2_FRMIVAL_TYPE_DISCRETE = 1
+ V4L2_FRMSIZE_TYPE_DISCRETE = 1
+ V4L2_MEMORY_MMAP = 1
+)
+
+type v4l2_capability struct { // size 104
+ driver [16]byte // offset 0, size 16
+ card [32]byte // offset 16, size 32
+ bus_info [32]byte // offset 48, size 32
+ version uint32 // offset 80, size 4
+ capabilities uint32 // offset 84, size 4
+ device_caps uint32 // offset 88, size 4
+ reserved [3]uint32 // offset 92, size 12
+}
+
+type v4l2_format struct { // size 208
+ typ uint32 // offset 0, size 4
+ _ [4]byte // align
+ pix v4l2_pix_format // offset 8, size 48
+ _ [152]byte // filler
+}
+
+type v4l2_pix_format struct { // size 48
+ width uint32 // offset 0, size 4
+ height uint32 // offset 4, size 4
+ pixelformat uint32 // offset 8, size 4
+ field uint32 // offset 12, size 4
+ bytesperline uint32 // offset 16, size 4
+ sizeimage uint32 // offset 20, size 4
+ colorspace uint32 // offset 24, size 4
+ priv uint32 // offset 28, size 4
+ flags uint32 // offset 32, size 4
+ ycbcr_enc uint32 // offset 36, size 4
+ quantization uint32 // offset 40, size 4
+ xfer_func uint32 // offset 44, size 4
+}
+
+type v4l2_streamparm struct { // size 204
+ typ uint32 // offset 0, size 4
+ capture v4l2_captureparm // offset 4, size 40
+ _ [160]byte // filler
+}
+
+type v4l2_captureparm struct { // size 40
+ capability uint32 // offset 0, size 4
+ capturemode uint32 // offset 4, size 4
+ timeperframe v4l2_fract // offset 8, size 8
+ extendedmode uint32 // offset 16, size 4
+ readbuffers uint32 // offset 20, size 4
+ reserved [4]uint32 // offset 24, size 16
+}
+
+type v4l2_fract struct { // size 8
+ numerator uint32 // offset 0, size 4
+ denominator uint32 // offset 4, size 4
+}
+
+type v4l2_requestbuffers struct { // size 20
+ count uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ memory uint32 // offset 8, size 4
+ capabilities uint32 // offset 12, size 4
+ flags uint8 // offset 16, size 1
+ reserved [3]uint8 // offset 17, size 3
+}
+
+type v4l2_buffer struct { // size 88
+ index uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ bytesused uint32 // offset 8, size 4
+ flags uint32 // offset 12, size 4
+ field uint32 // offset 16, size 4
+ _ [20]byte // align
+ timecode v4l2_timecode // offset 40, size 16
+ sequence uint32 // offset 56, size 4
+ memory uint32 // offset 60, size 4
+ offset uint32 // offset 64, size 4
+ _ [4]byte // align
+ length uint32 // offset 72, size 4
+ _ [12]byte // filler
+}
+
+type v4l2_timecode struct { // size 16
+ typ uint32 // offset 0, size 4
+ flags uint32 // offset 4, size 4
+ frames uint8 // offset 8, size 1
+ seconds uint8 // offset 9, size 1
+ minutes uint8 // offset 10, size 1
+ hours uint8 // offset 11, size 1
+ userbits [4]uint8 // offset 12, size 4
+}
+
+type v4l2_fmtdesc struct { // size 64
+ index uint32 // offset 0, size 4
+ typ uint32 // offset 4, size 4
+ flags uint32 // offset 8, size 4
+ description [32]byte // offset 12, size 32
+ pixelformat uint32 // offset 44, size 4
+ mbus_code uint32 // offset 48, size 4
+ reserved [3]uint32 // offset 52, size 12
+}
+
+type v4l2_frmsizeenum struct { // size 44
+ index uint32 // offset 0, size 4
+ pixel_format uint32 // offset 4, size 4
+ typ uint32 // offset 8, size 4
+ discrete v4l2_frmsize_discrete // offset 12, size 8
+ _ [24]byte // filler
+}
+
+type v4l2_frmsize_discrete struct { // size 8
+ width uint32 // offset 0, size 4
+ height uint32 // offset 4, size 4
+}
+
+type v4l2_frmivalenum struct { // size 52
+ index uint32 // offset 0, size 4
+ pixel_format uint32 // offset 4, size 4
+ width uint32 // offset 8, size 4
+ height uint32 // offset 12, size 4
+ typ uint32 // offset 16, size 4
+ discrete v4l2_fract // offset 20, size 8
+ _ [24]byte // filler
+}
diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go
new file mode 100644
index 00000000..87199762
--- /dev/null
+++ b/pkg/v4l2/producer.go
@@ -0,0 +1,121 @@
+//go:build linux
+
+package v4l2
+
+import (
+ "errors"
+ "net/url"
+ "strings"
+
+ "github.com/AlexxIT/go2rtc/pkg/core"
+ "github.com/AlexxIT/go2rtc/pkg/v4l2/device"
+ "github.com/pion/rtp"
+)
+
+type Producer struct {
+ core.Connection
+ dev *device.Device
+}
+
+func Open(rawURL string) (*Producer, error) {
+ // Example (ffmpeg source compatible):
+ // v4l2:device?video=/dev/video0&input_format=mjpeg&video_size=1280x720
+ u, err := url.Parse(rawURL)
+ if err != nil {
+ return nil, err
+ }
+
+ query := u.Query()
+
+ dev, err := device.Open(query.Get("video"))
+ if err != nil {
+ return nil, err
+ }
+
+ codec := &core.Codec{
+ ClockRate: 90000,
+ PayloadType: core.PayloadTypeRAW,
+ }
+
+ var width, height, pixFmt uint32
+
+ if wh := strings.Split(query.Get("video_size"), "x"); len(wh) == 2 {
+ codec.FmtpLine = "width=" + wh[0] + ";height=" + wh[1]
+ width = uint32(core.Atoi(wh[0]))
+ height = uint32(core.Atoi(wh[1]))
+ }
+
+ switch query.Get("input_format") {
+ case "mjpeg":
+ codec.Name = core.CodecJPEG
+ pixFmt = device.V4L2_PIX_FMT_MJPEG
+ case "yuyv422":
+ if codec.FmtpLine == "" {
+ return nil, errors.New("v4l2: invalid video_size")
+ }
+
+ codec.Name = core.CodecRAW
+ codec.FmtpLine += ";colorspace=422"
+ pixFmt = device.V4L2_PIX_FMT_YUYV
+ default:
+ return nil, errors.New("v4l2: invalid input_format")
+ }
+
+ if err = dev.SetFormat(width, height, pixFmt); err != nil {
+ return nil, err
+ }
+
+ if fps := core.Atoi(query.Get("framerate")); fps > 0 {
+ if err = dev.SetParam(uint32(fps)); err != nil {
+ return nil, err
+ }
+ }
+
+ medias := []*core.Media{
+ {
+ Kind: core.KindVideo,
+ Direction: core.DirectionRecvonly,
+ Codecs: []*core.Codec{codec},
+ },
+ }
+ return &Producer{
+ Connection: core.Connection{
+ ID: core.NewID(),
+ FormatName: "v4l2",
+ Medias: medias,
+ },
+ dev: dev,
+ }, nil
+}
+
+func (c *Producer) Start() error {
+ if err := c.dev.StreamOn(); err != nil {
+ return err
+ }
+
+ planarYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW
+
+ for {
+ buf, err := c.dev.Capture(planarYUV)
+ if err != nil {
+ return err
+ }
+
+ c.Recv += len(buf)
+
+ if len(c.Receivers) == 0 {
+ continue
+ }
+
+ pkt := &rtp.Packet{
+ Header: rtp.Header{Timestamp: core.Now90000()},
+ Payload: buf,
+ }
+ c.Receivers[0].WriteRTP(pkt)
+ }
+}
+
+func (c *Producer) Stop() error {
+ _ = c.Connection.Stop()
+ return errors.Join(c.dev.StreamOff(), c.dev.Close())
+}
diff --git a/pkg/y4m/README.md b/pkg/y4m/README.md
index 6f4d863e..ff97813b 100644
--- a/pkg/y4m/README.md
+++ b/pkg/y4m/README.md
@@ -1,5 +1,19 @@
+## Planar YUV formats
+
+Packed YUV - yuyv422 - YUYV 4:2:2
+Semi-Planar - nv12 - Y/CbCr 4:2:0
+Planar YUV - yuv420p - Planar YUV 4:2:0 - aka. [cosited](https://manned.org/yuv4mpeg.5)
+
+```
+[video4linux2,v4l2 @ 0x55fddc42a940] Raw : yuyv422 : YUYV 4:2:2 : 1920x1080
+[video4linux2,v4l2 @ 0x55fddc42a940] Raw : nv12 : Y/CbCr 4:2:0 : 1920x1080
+[video4linux2,v4l2 @ 0x55fddc42a940] Raw : yuv420p : Planar YUV 4:2:0 : 1920x1080
+```
+
## Useful links
- https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering
- https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_concepts
- https://fourcc.org/yuv.php#YV12
+- https://docs.kernel.org/userspace-api/media/v4l/pixfmt-yuv-planar.html
+- https://gist.github.com/Jim-Bar/3cbba684a71d1a9d468a6711a6eddbeb
diff --git a/pkg/y4m/y4m.go b/pkg/y4m/y4m.go
index 4ac54da6..24c43164 100644
--- a/pkg/y4m/y4m.go
+++ b/pkg/y4m/y4m.go
@@ -123,3 +123,27 @@ func NewImage(fmtp string) func(frame []byte) image.Image {
return nil
}
+
+// HasSameColor checks if all pixels has same color
+func HasSameColor(img image.Image) bool {
+ var pix []byte
+
+ switch img := img.(type) {
+ case *image.Gray:
+ pix = img.Pix
+ case *image.YCbCr:
+ pix = img.Y
+ }
+
+ if len(pix) == 0 {
+ return false
+ }
+
+ i0 := pix[0]
+ for _, i := range pix {
+ if i != i0 {
+ return false
+ }
+ }
+ return true
+}
diff --git a/scripts/README.md b/scripts/README.md
index 36f667b2..acc6e0c9 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -54,6 +54,41 @@ go list -deps .\cmd\go2rtc_rtsp\
- golang.org/x/tools
```
+## Licenses
+
+- github.com/asticode/go-astits - MIT
+- github.com/expr-lang/expr - MIT
+- github.com/gorilla/websocket - BSD-2
+- github.com/mattn/go-isatty - MIT
+- github.com/miekg/dns - BSD-3
+- github.com/pion/ice/v2 - MIT
+- github.com/pion/interceptor - MIT
+- github.com/pion/rtcp - MIT
+- github.com/pion/rtp - MIT
+- github.com/pion/sdp/v3 - MIT
+- github.com/pion/srtp/v2 - MIT
+- github.com/pion/stun - MIT
+- github.com/pion/webrtc/v3 - MIT
+- github.com/rs/zerolog - MIT
+- github.com/sigurn/crc16 - MIT
+- github.com/sigurn/crc8 - MIT
+- github.com/stretchr/testify - MIT
+- github.com/tadglines/go-pkgs - Apache
+- golang.org/x/crypto - BSD-3
+- gopkg.in/yaml.v3 - MIT and Apache
+- github.com/asticode/go-astikit - MIT
+- github.com/davecgh/go-spew - ISC (BSD/MIT like)
+- github.com/google/uuid - BSD-3
+- github.com/kr/pretty - MIT
+- github.com/mattn/go-colorable - MIT
+- github.com/pmezard/go-difflib - ???
+- github.com/wlynxg/anet - BSD-3
+- golang.org/x/mod - BSD-3
+- golang.org/x/net - BSD-3
+- golang.org/x/sync - BSD-3
+- golang.org/x/sys - BSD-3
+- golang.org/x/tools - BSD-3
+
## Virus
- https://go.dev/doc/faq#virus
diff --git a/www/add.html b/www/add.html
index 4b40f431..49e954d3 100644
--- a/www/add.html
+++ b/www/add.html
@@ -292,6 +292,18 @@
+
+
+
+
+