Merge branch 'master' into rtsp-backchannel

This commit is contained in:
seydx
2025-01-12 08:19:45 +01:00
39 changed files with 2204 additions and 394 deletions
+27 -15
View File
@@ -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
}
}
}
+10
View File
@@ -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)
+1 -1
View File
@@ -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)
+58 -14
View File
@@ -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
}
+10
View File
@@ -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
)
+15 -15
View File
@@ -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
}
+38
View File
@@ -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
-
+26 -87
View File
@@ -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,
`<tds:GetCapabilities xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Category>All</tds:Category>
</tds:GetCapabilities>`,
c.mediaURL, `<trt:GetProfile><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetProfile>`,
)
}
func (c *Client) GetNetworkInterfaces() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetNetworkInterfaces xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
}
func (c *Client) GetDeviceInformation() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetDeviceInformation xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
}
func (c *Client) GetProfiles() ([]byte, error) {
return c.Request(
c.mediaURL, `<trt:GetProfiles xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
)
func (c *Client) GetVideoSourceConfiguration(token string) ([]byte, error) {
return c.Request(c.mediaURL, `<trt:GetVideoSourceConfiguration>
<trt:ConfigurationToken>`+token+`</trt:ConfigurationToken>
</trt:GetVideoSourceConfiguration>`)
}
func (c *Client) GetStreamUri(token string) ([]byte, error) {
return c.Request(
c.mediaURL,
`<trt:GetStreamUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
return c.Request(c.mediaURL, `<trt:GetStreamUri>
<trt:StreamSetup>
<tt:Stream>RTP-Unicast</tt:Stream>
<tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport>
</trt:StreamSetup>
<trt:ProfileToken>`+token+`</trt:ProfileToken>
</trt:GetStreamUri>`,
)
</trt:GetStreamUri>`)
}
func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
return c.Request(
c.imaginURL,
`<trt:GetSnapshotUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:ProfileToken>`+token+`</trt:ProfileToken>
</trt:GetSnapshotUri>`,
)
}
func (c *Client) GetSystemDateAndTime() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetSystemDateAndTime xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
c.imaginURL, `<trt:GetSnapshotUri><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetSnapshotUri>`,
)
}
func (c *Client) GetServiceCapabilities() ([]byte, error) {
// some cameras answer GetServiceCapabilities for media only for path = "/onvif/media"
return c.Request(
c.mediaURL, `<trt:GetServiceCapabilities xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
c.mediaURL, `<trt:GetServiceCapabilities />`,
)
}
func (c *Client) SystemReboot() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:SystemReboot xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
func (c *Client) DeviceRequest(operation string) ([]byte, error) {
if operation == DeviceGetServices {
operation = `<tds:GetServices><tds:IncludeCapability>true</tds:IncludeCapability></tds:GetServices>`
} else {
operation = `<tds:` + operation + `/>`
}
return c.Request(c.deviceURL, operation)
}
func (c *Client) GetServices() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetServices xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:IncludeCapability>true</tds:IncludeCapability>
</tds:GetServices>`,
)
}
func (c *Client) GetScopes() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetScopes xmlns:tds="http://www.onvif.org/ver10/device/wsdl" />`,
)
func (c *Client) MediaRequest(operation string) ([]byte, error) {
operation = `<trt:` + 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(
`<?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">`,
)
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(`<s:Header>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>` + user.Username() + `</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + `</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">` + base64.StdEncoding.EncodeToString([]byte(nonce)) + `</wsse:Nonce>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">` + created + `</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>`)
}
buf.WriteString(`<s:Body>` + body + `</s:Body></s:Envelope>`)
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
}
+79
View File
@@ -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 = `<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
`
prefix2 = `<s:Body>
`
suffix = `
</s:Body>
</s:Envelope>`
)
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:Header>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>%s</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">%s</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">%s</wsse:Nonce>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">%s</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>
`,
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...)
}
+24 -1
View File
@@ -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)
}
+28
View File
@@ -84,6 +84,34 @@ func TestGetStreamUri(t *testing.T) {
</SOAP-ENV:Envelope>`,
url: "rtsp://192.168.5.53:8090/profile1=r",
},
{
name: "go2rtc 1.9.4",
xml: `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:MediaUri>
<tt:Uri xmlns:tt="http://www.onvif.org/ver10/schema">rtsp://192.168.1.123:8554/rtsp-dahua1</tt:Uri>
</trt:MediaUri>
</trt:GetStreamUriResponse>
</s:Body>
</s:Envelope>`,
url: "rtsp://192.168.1.123:8554/rtsp-dahua1",
},
{
name: "go2rtc 1.9.8",
xml: `<?xml version="1.0" encoding="utf-8" standalone="no"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
<s:Body>
<trt:GetStreamUriResponse>
<trt:MediaUri>
<tt:Uri>rtsp://192.168.1.123:8554/rtsp-dahua2</tt:Uri>
</trt:MediaUri>
</trt:GetStreamUriResponse>
</s:Body>
</s:Envelope>
`,
url: "rtsp://192.168.1.123:8554/rtsp-dahua2",
},
}
for _, test := range tests {
+203 -155
View File
@@ -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 `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:GetCapabilitiesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Capabilities xmlns:tt="http://www.onvif.org/ver10/schema">
<tt:Device>
<tt:XAddr>http://` + host + `/onvif/device_service</tt:XAddr>
</tt:Device>
<tt:Media>
<tt:XAddr>http://` + host + `/onvif/media_service</tt:XAddr>
<tt:StreamingCapabilities>
<tt:RTPMulticast>false</tt:RTPMulticast>
<tt:RTP_TCP>false</tt:RTP_TCP>
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
</tt:StreamingCapabilities>
</tt:Media>
</tds:Capabilities>
</tds:GetCapabilitiesResponse>
</s:Body>
</s:Envelope>`
func GetCapabilitiesResponse(host string) []byte {
e := NewEnvelope()
e.Append(`<tds:GetCapabilitiesResponse>
<tds:Capabilities>
<tt:Device>
<tt:XAddr>http://`, host, `/onvif/device_service</tt:XAddr>
</tt:Device>
<tt:Media>
<tt:XAddr>http://`, host, `/onvif/media_service</tt:XAddr>
<tt:StreamingCapabilities>
<tt:RTPMulticast>false</tt:RTPMulticast>
<tt:RTP_TCP>false</tt:RTP_TCP>
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
</tt:StreamingCapabilities>
</tt:Media>
</tds:Capabilities>
</tds:GetCapabilitiesResponse>`)
return e.Bytes()
}
func GetSystemDateAndTimeResponse() string {
func GetServicesResponse(host string) []byte {
e := NewEnvelope()
e.Append(`<tds:GetServicesResponse>
<tds:Service>
<tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace>
<tds:XAddr>http://`, host, `/onvif/device_service</tds:XAddr>
<tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>
</tds:Service>
<tds:Service>
<tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace>
<tds:XAddr>http://`, host, `/onvif/media_service</tds:XAddr>
<tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>
</tds:Service>
</tds:GetServicesResponse>`)
return e.Bytes()
}
func GetSystemDateAndTimeResponse() []byte {
loc := time.Now()
utc := loc.UTC()
return fmt.Sprintf(`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:GetSystemDateAndTimeResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:SystemDateAndTime xmlns:tt="http://www.onvif.org/ver10/schema">
<tt:DateTimeType>NTP</tt:DateTimeType>
<tt:DaylightSavings>false</tt:DaylightSavings>
<tt:TimeZone>
<tt:TZ>GMT%s</tt:TZ>
</tt:TimeZone>
<tt:UTCDateTime>
<tt:Time>
<tt:Hour>%d</tt:Hour>
<tt:Minute>%d</tt:Minute>
<tt:Second>%d</tt:Second>
</tt:Time>
<tt:Date>
<tt:Year>%d</tt:Year>
<tt:Month>%d</tt:Month>
<tt:Day>%d</tt:Day>
</tt:Date>
</tt:UTCDateTime>
<tt:LocalDateTime>
<tt:Time>
<tt:Hour>%d</tt:Hour>
<tt:Minute>%d</tt:Minute>
<tt:Second>%d</tt:Second>
</tt:Time>
<tt:Date>
<tt:Year>%d</tt:Year>
<tt:Month>%d</tt:Month>
<tt:Day>%d</tt:Day>
</tt:Date>
</tt:LocalDateTime>
</tds:SystemDateAndTime>
</tds:GetSystemDateAndTimeResponse>
</s:Body>
</s:Envelope>`,
loc.Format("-07:00"),
e := NewEnvelope()
e.Appendf(`<tds:GetSystemDateAndTimeResponse>
<tds:SystemDateAndTime>
<tt:DateTimeType>NTP</tt:DateTimeType>
<tt:DaylightSavings>true</tt:DaylightSavings>
<tt:TimeZone>
<tt:TZ>%s</tt:TZ>
</tt:TimeZone>
<tt:UTCDateTime>
<tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>
<tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>
</tt:UTCDateTime>
<tt:LocalDateTime>
<tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>
<tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>
</tt:LocalDateTime>
</tds:SystemDateAndTime>
</tds:GetSystemDateAndTimeResponse>`,
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 `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:GetNetworkInterfacesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>
</s:Body>
</s:Envelope>`
func GetDeviceInformationResponse(manuf, model, firmware, serial string) []byte {
e := NewEnvelope()
e.Append(`<tds:GetDeviceInformationResponse>
<tds:Manufacturer>`, manuf, `</tds:Manufacturer>
<tds:Model>`, model, `</tds:Model>
<tds:FirmwareVersion>`, firmware, `</tds:FirmwareVersion>
<tds:SerialNumber>`, serial, `</tds:SerialNumber>
<tds:HardwareId>1.00</tds:HardwareId>
</tds:GetDeviceInformationResponse>`)
return e.Bytes()
}
func GetDeviceInformationResponse(manuf, model, firmware, serial string) string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:GetDeviceInformationResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Manufacturer>` + manuf + `</tds:Manufacturer>
<tds:Model>` + model + `</tds:Model>
<tds:FirmwareVersion>` + firmware + `</tds:FirmwareVersion>
<tds:SerialNumber>` + serial + `</tds:SerialNumber>
<tds:HardwareId>1.00</tds:HardwareId>
</tds:GetDeviceInformationResponse>
</s:Body>
</s:Envelope>`
func GetMediaServiceCapabilitiesResponse() []byte {
e := NewEnvelope()
e.Append(`<trt:GetServiceCapabilitiesResponse>
<trt:Capabilities SnapshotUri="true" Rotation="false" VideoSourceMode="false" OSD="false" TemporaryOSDText="false" EXICompression="false">
<trt:StreamingCapabilities RTPMulticast="false" RTP_TCP="false" RTP_RTSP_TCP="true" NonAggregateControl="false" NoRTSPStreaming="false" />
</trt:Capabilities>
</trt:GetServiceCapabilitiesResponse>`)
return e.Bytes()
}
func GetServiceCapabilitiesResponse() string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<trt:GetServiceCapabilitiesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:Capabilities SnapshotUri="false" Rotation="false" VideoSourceMode="false" OSD="false" TemporaryOSDText="false" EXICompression="false">
<trt:StreamingCapabilities RTPMulticast="false" RTP_TCP="false" RTP_RTSP_TCP="true" NonAggregateControl="false" NoRTSPStreaming="false" />
</trt:Capabilities>
</trt:GetServiceCapabilitiesResponse>
</s:Body>
</s:Envelope>`
func GetProfilesResponse(names []string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetProfilesResponse>
`)
for _, name := range names {
appendProfile(e, "Profiles", name)
}
e.Append(`</trt:GetProfilesResponse>`)
return e.Bytes()
}
func SystemRebootResponse() string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:SystemRebootResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Message>system reboot in 1 second...</tds:Message>
</tds:SystemRebootResponse>
</s:Body>
</s:Envelope>`
func GetProfileResponse(name string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetProfileResponse>
`)
appendProfile(e, "Profile", name)
e.Append(`</trt:GetProfileResponse>`)
return e.Bytes()
}
func GetProfilesResponse(names []string) string {
buf := bytes.NewBuffer(nil)
buf.WriteString(`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<trt:GetProfilesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">`)
func appendProfile(e *Envelope, tag, name string) {
// empty `RateControl` important for UniFi Protect
e.Append(`<trt:`, tag, ` token="`, name, `" fixed="true">
<tt:Name>`, name, `</tt:Name>
<tt:VideoSourceConfiguration token="`, name, `">
<tt:Name>VSC</tt:Name>
<tt:SourceToken>`, name, `</tt:SourceToken>
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
</tt:VideoSourceConfiguration>
<tt:VideoEncoderConfiguration token="vec">
<tt:Name>VEC</tt:Name>
<tt:Encoding>H264</tt:Encoding>
<tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
<tt:RateControl />
</tt:VideoEncoderConfiguration>
</trt:`, tag, `>
`)
}
for i, name := range names {
buf.WriteString(`
<trt:Profiles token="` + name + `" fixed="true">
<tt:Name>` + name + `</tt:Name>
<tt:VideoEncoderConfiguration token="` + strconv.Itoa(i) + `">
<tt:Encoding>H264</tt:Encoding>
<tt:Resolution>
<tt:Width>1920</tt:Width>
<tt:Height>1080</tt:Height>
</tt:Resolution>
</tt:VideoEncoderConfiguration>
</trt:Profiles>`)
func GetVideoSourceConfigurationResponse(name string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetVideoSourceConfigurationResponse>
<trt:Configuration token="`, name, `">
<tt:Name>VSC</tt:Name>
<tt:SourceToken>`, name, `</tt:SourceToken>
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
</trt:Configuration>
</trt:GetVideoSourceConfigurationResponse>`)
return e.Bytes()
}
func GetVideoSourcesResponse(names []string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetVideoSourcesResponse>
`)
for _, name := range names {
e.Append(`<trt:VideoSources token="`, name, `">
<tt:Framerate>30.000000</tt:Framerate>
<tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
</trt:VideoSources>
`)
}
e.Append(`</trt:GetVideoSourcesResponse>`)
return e.Bytes()
}
func GetStreamUriResponse(uri string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>`, uri, `</tt:Uri></trt:MediaUri></trt:GetStreamUriResponse>`)
return e.Bytes()
}
func GetSnapshotUriResponse(uri string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>`, uri, `</tt:Uri></trt:MediaUri></trt:GetSnapshotUriResponse>`)
return e.Bytes()
}
func StaticResponse(operation string) []byte {
switch operation {
case DeviceGetSystemDateAndTime:
return GetSystemDateAndTimeResponse()
}
buf.WriteString(`
</trt:GetProfilesResponse>
</s:Body>
</s:Envelope>`)
return buf.String()
e := NewEnvelope()
e.Append(responses[operation])
b := e.Bytes()
if operation == DeviceGetNetworkInterfaces {
println()
}
return b
}
func GetStreamUriResponse(uri string) string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:MediaUri>
<tt:Uri xmlns:tt="http://www.onvif.org/ver10/schema">` + uri + `</tt:Uri>
</trt:MediaUri>
</trt:GetStreamUriResponse>
</s:Body>
</s:Envelope>`
var responses = map[string]string{
DeviceGetDiscoveryMode: `<tds:GetDiscoveryModeResponse><tds:DiscoveryMode>Discoverable</tds:DiscoveryMode></tds:GetDiscoveryModeResponse>`,
DeviceGetDNS: `<tds:GetDNSResponse><tds:DNSInformation /></tds:GetDNSResponse>`,
DeviceGetHostname: `<tds:GetHostnameResponse><tds:HostnameInformation /></tds:GetHostnameResponse>`,
DeviceGetNetworkDefaultGateway: `<tds:GetNetworkDefaultGatewayResponse><tds:NetworkGateway /></tds:GetNetworkDefaultGatewayResponse>`,
DeviceGetNTP: `<tds:GetNTPResponse><tds:NTPInformation /></tds:GetNTPResponse>`,
DeviceSystemReboot: `<tds:SystemRebootResponse><tds:Message>OK</tds:Message></tds:SystemRebootResponse>`,
DeviceGetNetworkInterfaces: `<tds:GetNetworkInterfacesResponse />`,
DeviceGetNetworkProtocols: `<tds:GetNetworkProtocolsResponse />`,
DeviceGetScopes: `<tds:GetScopesResponse>
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/name/go2rtc</tt:ScopeItem></tds:Scopes>
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/location/github</tt:ScopeItem></tds:Scopes>
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/Profile/Streaming</tt:ScopeItem></tds:Scopes>
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/type/Network_Video_Transmitter</tt:ScopeItem></tds:Scopes>
</tds:GetScopesResponse>`,
}
+7 -6
View File
@@ -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)
}
+9 -2
View File
@@ -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
}
}
+74 -32
View File
@@ -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)
}
+1 -1
View File
@@ -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,
+21
View File
@@ -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
+244
View File
@@ -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)
}
+40
View File
@@ -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++
}
}
+149
View File
@@ -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
}
+163
View File
@@ -0,0 +1,163 @@
#include <stdio.h>
#include <stddef.h>
#include <linux/videodev2.h>
#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;
}
+149
View File
@@ -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
}
+149
View File
@@ -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
}
+151
View File
@@ -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
}
+121
View File
@@ -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())
}
+14
View File
@@ -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
+24
View File
@@ -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
}