diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index 8376fd2e..8c81bcb4 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -21,12 +21,5 @@ func Init() { var log zerolog.Logger func streamHandler(url string) (core.Producer, error) { - conn, err := homekit.NewClient(url, srtp.Server) - if err != nil { - return nil, err - } - if err = conn.Dial(); err != nil { - return nil, err - } - return conn, nil + return homekit.Dial(url, srtp.Server) } diff --git a/internal/srtp/srtp.go b/internal/srtp/srtp.go index 90b8e145..aa946f07 100644 --- a/internal/srtp/srtp.go +++ b/internal/srtp/srtp.go @@ -1,8 +1,6 @@ package srtp import ( - "net" - "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/srtp" ) @@ -24,23 +22,8 @@ func Init() { return } - log := app.GetLogger("srtp") - - // create SRTP server (endpoint) for receiving video from HomeKit camera - conn, err := net.ListenPacket("udp", cfg.Mod.Listen) - if err != nil { - log.Warn().Err(err).Caller().Send() - } - - log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen") - - // run server - go func() { - Server = &srtp.Server{} - if err = Server.Serve(conn); err != nil { - log.Warn().Err(err).Caller().Send() - } - }() + // create SRTP server (endpoint) for receiving video from HomeKit cameras + Server = srtp.NewServer(cfg.Mod.Listen) } var Server *srtp.Server diff --git a/pkg/aac/aac.go b/pkg/aac/aac.go index 294717c7..c991431d 100644 --- a/pkg/aac/aac.go +++ b/pkg/aac/aac.go @@ -10,16 +10,18 @@ import ( const ( TypeAACMain = 1 - TypeAACLC = 2 + TypeAACLC = 2 // Low Complexity + TypeAACLD = 23 // Low Delay (48000, 44100, 32000, 24000, 22050) TypeESCAPE = 31 + TypeAACELD = 39 // Enhanced Low Delay AUTime = 1024 + + // FMTP streamtype=5 - audio stream + FMTP = "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" ) -// streamtype=5 - audio stream -const fmtp = "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" - -var sampleRates = []uint32{ +var sampleRates = [16]uint32{ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, 0, 0, 0, // protection from request sampleRates[15] } @@ -29,7 +31,7 @@ func ConfigToCodec(conf []byte) *core.Codec { rd := bits.NewReader(conf) codec := &core.Codec{ - FmtpLine: fmtp + hex.EncodeToString(conf), + FmtpLine: FMTP + hex.EncodeToString(conf), PayloadType: core.PayloadTypeRAW, } @@ -39,7 +41,7 @@ func ConfigToCodec(conf []byte) *core.Codec { } switch objType { - case TypeAACLC: + case TypeAACLC, TypeAACLD, TypeAACELD: codec.Name = core.CodecAAC default: codec.Name = fmt.Sprintf("AAC-%X", objType) @@ -72,3 +74,51 @@ func DecodeConfig(b []byte) (objType, sampleFreqIdx, channels byte, sampleRate u channels = rd.ReadBits8(4) return } + +func EncodeConfig(objType byte, sampleRate uint32, channels byte, shortFrame bool) []byte { + wr := bits.NewWriter(nil) + + if objType < TypeESCAPE { + wr.WriteBits8(objType, 5) + } else { + wr.WriteBits8(TypeESCAPE, 5) + wr.WriteBits8(objType-32, 6) + } + + i := indexUint32(sampleRates[:], sampleRate) + if i >= 0 { + wr.WriteBits8(byte(i), 4) + } else { + wr.WriteBits8(0xF, 4) + wr.WriteBits(sampleRate, 24) + } + + wr.WriteBits8(channels, 4) + + switch objType { + case TypeAACLD: + // https://github.com/FFmpeg/FFmpeg/blob/67d392b97941bb51fb7af3a3c9387f5ab895fa46/libavcodec/aacdec_template.c#L841 + wr.WriteBool(shortFrame) + wr.WriteBit(0) // dependsOnCoreCoder + wr.WriteBit(0) // extension_flag + wr.WriteBits8(0, 2) // ep_config + case TypeAACELD: + // https://github.com/FFmpeg/FFmpeg/blob/67d392b97941bb51fb7af3a3c9387f5ab895fa46/libavcodec/aacdec_template.c#L922 + wr.WriteBool(shortFrame) + wr.WriteBits8(0, 3) // res_flags + wr.WriteBit(0) // ldSbrPresentFlag + wr.WriteBits8(0, 4) // ELDEXT_TERM + wr.WriteBits8(0, 2) // ep_config + } + + return wr.Bytes() +} + +func indexUint32(s []uint32, v uint32) int { + for i := range s { + if v == s[i] { + return i + } + } + return -1 +} diff --git a/pkg/aac/adts_test.go b/pkg/aac/aac_test.go similarity index 52% rename from pkg/aac/adts_test.go rename to pkg/aac/aac_test.go index 7b4d69f6..08d9c436 100644 --- a/pkg/aac/adts_test.go +++ b/pkg/aac/aac_test.go @@ -4,9 +4,25 @@ import ( "encoding/hex" "testing" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/stretchr/testify/require" ) +func TestConfigToCodec(t *testing.T) { + s := "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000" + s = core.Between(s, "config=", ";") + src, err := hex.DecodeString(s) + require.Nil(t, err) + + codec := ConfigToCodec(src) + require.Equal(t, core.CodecAAC, codec.Name) + require.Equal(t, uint32(24000), codec.ClockRate) + require.Equal(t, uint16(1), codec.Channels) + + dst := EncodeConfig(TypeAACELD, 24000, 1, true) + require.Equal(t, src, dst) +} + func TestADTS(t *testing.T) { // FFmpeg MPEG-TS AAC (one packet) s := "fff15080021ffc210049900219002380fff15080021ffc212049900219002380" //... diff --git a/pkg/aac/adts.go b/pkg/aac/adts.go index 483dcf4f..45925903 100644 --- a/pkg/aac/adts.go +++ b/pkg/aac/adts.go @@ -51,7 +51,7 @@ func ADTSToCodec(b []byte) *core.Codec { Name: core.CodecAAC, ClockRate: sampleRates[sampleRateIdx], Channels: channels, - FmtpLine: fmtp + hex.EncodeToString(conf), + FmtpLine: FMTP + hex.EncodeToString(conf), } return codec } diff --git a/pkg/bits/writer.go b/pkg/bits/writer.go index a54abe55..307166b1 100644 --- a/pkg/bits/writer.go +++ b/pkg/bits/writer.go @@ -55,6 +55,14 @@ func (w *Writer) WriteAllBits(bit, n byte) { } } +func (w *Writer) WriteBool(b bool) { + if b { + w.WriteBit(1) + } else { + w.WriteBit(0) + } +} + func (w *Writer) WriteUint16(v uint16) { if w.bits != 0 { w.WriteBits16(v, 16) diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index c65a84df..9c67709d 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -22,12 +22,16 @@ func Now90000() uint32 { const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" -// RandString base10 - numbers, base16 - hex, base36 - digits+letters, base64 - URL safe symbols +// RandString base10 - numbers, base16 - hex, base36 - digits+letters +// base64 - URL safe symbols, base0 - crypto random func RandString(size, base byte) string { b := make([]byte, size) if _, err := rand.Read(b); err != nil { panic(err) } + if base == 0 { + return string(b) + } for i := byte(0); i < size; i++ { b[i] = symbols[b[i]%base] } @@ -50,12 +54,7 @@ func Between(s, sub1, sub2 string) string { } s = s[i+len(sub1):] - if len(sub2) == 1 { - i = strings.IndexByte(s, sub2[0]) - } else { - i = strings.Index(s, sub2) - } - if i >= 0 { + if i = strings.Index(s, sub2); i >= 0 { return s[:i] } diff --git a/pkg/hap/README.md b/pkg/hap/README.md index a3a1a4f0..33341fea 100644 --- a/pkg/hap/README.md +++ b/pkg/hap/README.md @@ -35,6 +35,14 @@ Requires ffmpeg built with `--enable-libfdk-aac` -acodec libfdk_aac -aprofile aac_eld ``` +| SampleRate | RTPTime | constantDuration | objectType | +|------------|---------|--------------------|--------------| +| 8000 | 60 | =8000/1000*60=480 | 39 (AAC ELD) | +| 16000 | 30 | =16000/1000*30=480 | 39 (AAC ELD) | +| 24000 | 20 | =24000/1000*20=480 | 39 (AAC ELD) | +| 16000 | 60 | =16000/1000*60=960 | 23 (AAC LD) | +| 24000 | 40 | =24000/1000*40=960 | 23 (AAC LD) | + ## Useful links - https://github.com/apple/HomeKitADK/blob/master/Documentation/crypto.md diff --git a/pkg/hap/accessory.go b/pkg/hap/accessory.go index b86a4509..4bdd86f7 100644 --- a/pkg/hap/accessory.go +++ b/pkg/hap/accessory.go @@ -1,16 +1,64 @@ package hap +import ( + "fmt" + "strconv" +) + +const ( + FormatString = "string" + FormatBool = "bool" + FormatFloat = "float" + FormatUInt8 = "uint8" + FormatUInt16 = "uint16" + FormatUInt32 = "uint32" + FormatInt32 = "int32" + FormatUInt64 = "uint64" + FormatData = "data" + FormatTLV8 = "tlv8" + + UnitPercentage = "percentage" +) + +var PR = []string{"pr"} +var PW = []string{"pw"} +var PRPW = []string{"pr", "pw"} +var EVPRPW = []string{"ev", "pr", "pw"} +var EVPR = []string{"ev", "pr"} + type Accessory struct { - AID int `json:"aid"` + AID uint8 `json:"aid"` // 150 unique accessories per bridge Services []*Service `json:"services"` } -type Accessories struct { - Accessories []*Accessory `json:"accessories"` -} +func (a *Accessory) InitIID() { + serviceN := map[string]byte{} + for _, service := range a.Services { + if len(service.Type) > 3 { + panic(service.Type) + } -type Characters struct { - Characters []*Character `json:"characteristics"` + n := serviceN[service.Type] + 1 + serviceN[service.Type] = n + + if n > 15 { + panic(n) + } + + // ServiceID = ANSSS000 + s := fmt.Sprintf("%x%x%03s000", a.AID, n, service.Type) + service.IID, _ = strconv.ParseUint(s, 16, 64) + + for _, character := range service.Characters { + if len(character.Type) > 3 { + panic(character.Type) + } + + // CharacterID = ANSSSCCC + character.IID, _ = strconv.ParseUint(character.Type, 16, 64) + character.IID += service.IID + } + } } func (a *Accessory) GetService(servType string) *Service { @@ -33,7 +81,7 @@ func (a *Accessory) GetCharacter(charType string) *Character { return nil } -func (a *Accessory) GetCharacterByID(iid int) *Character { +func (a *Accessory) GetCharacterByID(iid uint64) *Character { for _, serv := range a.Services { for _, char := range serv.Characters { if char.IID == iid { @@ -45,11 +93,11 @@ func (a *Accessory) GetCharacterByID(iid int) *Character { } type Service struct { - IID int `json:"iid"` - Type string `json:"type"` - Primary bool `json:"primary,omitempty"` - Hidden bool `json:"hidden,omitempty"` - Characters []*Character `json:"characteristics"` + Type string `json:"type"` + IID uint64 `json:"iid"` + Primary bool `json:"primary,omitempty"` + Characters []*Character `json:"characteristics"` + Linked []int `json:"linked,omitempty"` } func (s *Service) GetCharacter(charType string) *Character { @@ -60,3 +108,67 @@ func (s *Service) GetCharacter(charType string) *Character { } return nil } + +func ServiceAccessoryInformation(manuf, model, name, serial, firmware string) *Service { + return &Service{ + Type: "3E", // AccessoryInformation + Characters: []*Character{ + { + Type: "14", + Format: FormatBool, + Perms: PW, + //Descr: "Identify", + }, { + Type: "20", + Format: FormatString, + Value: manuf, + Perms: PR, + //Descr: "Manufacturer", + //MaxLen: 64, + }, { + Type: "21", + Format: FormatString, + Value: model, + Perms: PR, + //Descr: "Model", + //MaxLen: 64, + }, { + Type: "23", + Format: FormatString, + Value: name, + Perms: PR, + //Descr: "Name", + //MaxLen: 64, + }, { + Type: "30", + Format: FormatString, + Value: serial, + Perms: PR, + //Descr: "Serial Number", + //MaxLen: 64, + }, { + Type: "52", + Format: FormatString, + Value: firmware, + Perms: PR, + //Descr: "Firmware Revision", + }, + }, + } +} + +func ServiceHAPProtocolInformation() *Service { + return &Service{ + Type: "A2", // 'HAPProtocolInformation' + Characters: []*Character{ + { + Type: "37", + Format: FormatString, + Value: "1.1.0", + Perms: PR, + //Descr: "Version", + //MaxLen: 64, + }, + }, + } +} diff --git a/pkg/hap/camera/ch114_supported_video.go b/pkg/hap/camera/ch114_supported_video.go index 0e9ef66f..196f0286 100644 --- a/pkg/hap/camera/ch114_supported_video.go +++ b/pkg/hap/camera/ch114_supported_video.go @@ -3,15 +3,17 @@ package camera const TypeSupportedVideoStreamConfiguration = "114" type SupportedVideoStreamConfig struct { - Codecs []VideoCodecConfig `tlv8:"1"` + Codecs []VideoCodec `tlv8:"1"` } -type VideoCodecConfig struct { - CodecType byte `tlv8:"1"` - CodecParams []VideoCodecParams `tlv8:"2"` - VideoAttrs []VideoAttrs `tlv8:"3"` +type VideoCodec struct { + CodecType byte `tlv8:"1"` + CodecParams []VideoParams `tlv8:"2"` + VideoAttrs []VideoAttrs `tlv8:"3"` + RTPParams []RTPParams `tlv8:"4"` } +//goland:noinspection ALL const ( VideoCodecTypeH264 = 0 @@ -29,12 +31,12 @@ const ( VideoCodecCvoSuppported = 1 ) -type VideoCodecParams struct { - ProfileID byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high - Level byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0 - PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved - CVOEnabled byte `tlv8:"4"` // 0 - not supported, 1 - supported - CVOID byte `tlv8:"5"` // ??? +type VideoParams struct { + ProfileID []byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high + Level []byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0 + PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved + CVOEnabled []byte `tlv8:"4"` // 0 - not supported, 1 - supported + CVOID []byte `tlv8:"5"` // ??? } type VideoAttrs struct { diff --git a/pkg/hap/camera/ch115_supported_audio.go b/pkg/hap/camera/ch115_supported_audio.go index c9aa8c31..efb0d881 100644 --- a/pkg/hap/camera/ch115_supported_audio.go +++ b/pkg/hap/camera/ch115_supported_audio.go @@ -3,10 +3,11 @@ package camera const TypeSupportedAudioStreamConfiguration = "115" type SupportedAudioStreamConfig struct { - Codecs []AudioCodecConfig `tlv8:"1"` - ComfortNoise byte `tlv8:"2"` + Codecs []AudioCodec `tlv8:"1"` + ComfortNoise byte `tlv8:"2"` } +//goland:noinspection ALL const ( AudioCodecTypePCMU = 0 AudioCodecTypePCMA = 1 @@ -22,16 +23,24 @@ const ( AudioCodecSampleRate8Khz = 0 AudioCodecSampleRate16Khz = 1 AudioCodecSampleRate24Khz = 2 + + RTPTimeAACELD8 = 60 // 8000/1000*60=480 + RTPTimeAACELD16 = 30 // 16000/1000*30=480 + RTPTimeAACELD24 = 20 // 24000/1000*20=480 + RTPTimeAACLD16 = 60 // 16000/1000*60=960 + RTPTimeAACLD24 = 40 // 24000/1000*40=960 ) -type AudioCodecConfig struct { - CodecType byte `tlv8:"1"` - CodecParams []AudioCodecParams `tlv8:"2"` +type AudioCodec struct { + CodecType byte `tlv8:"1"` + CodecParams []AudioParams `tlv8:"2"` + RTPParams []RTPParams `tlv8:"3"` + ComfortNoise []byte `tlv8:"4"` } -type AudioCodecParams struct { - Channels byte `tlv8:"1"` - Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant - SampleRate byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000 - RTPTime byte `tlv8:"4"` +type AudioParams struct { + Channels uint8 `tlv8:"1"` + Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant + SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000 + RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60 } diff --git a/pkg/hap/camera/ch116_rtp_config.go b/pkg/hap/camera/ch116_rtp_config.go new file mode 100644 index 00000000..fb4be550 --- /dev/null +++ b/pkg/hap/camera/ch116_rtp_config.go @@ -0,0 +1,14 @@ +package camera + +const TypeSupportedRTPConfiguration = "116" + +//goland:noinspection ALL +const ( + CryptoAES_CM_128_HMAC_SHA1_80 = 0 + CryptoAES_CM_256_HMAC_SHA1_80 = 1 + CryptoNone = 2 +) + +type SupportedRTPConfig struct { + CryptoType []byte `tlv8:"2"` +} diff --git a/pkg/hap/camera/ch117_selected_stream.go b/pkg/hap/camera/ch117_selected_stream.go index f6629c95..219a391a 100644 --- a/pkg/hap/camera/ch117_selected_stream.go +++ b/pkg/hap/camera/ch117_selected_stream.go @@ -3,11 +3,12 @@ package camera const TypeSelectedStreamConfiguration = "117" type SelectedStreamConfig struct { - Control SessionControl `tlv8:"1"` - VideoParams SelectedVideoParams `tlv8:"2"` - AudioParams SelectedAudioParams `tlv8:"3"` + Control SessionControl `tlv8:"1"` + VideoCodec VideoCodec `tlv8:"2"` + AudioCodec AudioCodec `tlv8:"3"` } +//goland:noinspection ALL const ( SessionCommandEnd = 0 SessionCommandStart = 1 @@ -17,36 +18,15 @@ const ( ) type SessionControl struct { - Session string `tlv8:"1"` - Command byte `tlv8:"2"` + SessionID string `tlv8:"1"` + Command byte `tlv8:"2"` } -type SelectedVideoParams struct { - CodecType byte `tlv8:"1"` // only 0 - H264 - CodecParams VideoCodecParams `tlv8:"2"` - VideoAttrs VideoAttrs `tlv8:"3"` - RTPParams VideoRTPParams `tlv8:"4"` -} - -type VideoRTPParams struct { - PayloadType uint8 `tlv8:"1"` - SSRC uint32 `tlv8:"2"` - MaxBitrate uint16 `tlv8:"3"` - MinRTCPInterval float32 `tlv8:"4"` - MaxMTU uint16 `tlv8:"5"` -} - -type SelectedAudioParams struct { - CodecType byte `tlv8:"1"` // 2 - AAC_ELD, 3 - OPUS, 5 - AMR, 6 - AMR_WB - CodecParams AudioCodecParams `tlv8:"2"` - RTPParams AudioRTPParams `tlv8:"3"` - ComfortNoise uint8 `tlv8:"4"` -} - -type AudioRTPParams struct { - PayloadType uint8 `tlv8:"1"` - SSRC uint32 `tlv8:"2"` - MaxBitrate uint16 `tlv8:"3"` - MinRTCPInterval float32 `tlv8:"4"` - ComfortNoisePayloadType uint8 `tlv8:"6"` +type RTPParams struct { + PayloadType uint8 `tlv8:"1"` + SSRC uint32 `tlv8:"2"` + MaxBitrate uint16 `tlv8:"3"` + MinRTCPInterval float32 `tlv8:"4"` + MaxMTU []uint16 `tlv8:"5"` + ComfortNoisePayloadType []uint8 `tlv8:"6"` } diff --git a/pkg/hap/camera/ch118_setup_endpoints.go b/pkg/hap/camera/ch118_setup_endpoints.go index efd5557f..9405de4a 100644 --- a/pkg/hap/camera/ch118_setup_endpoints.go +++ b/pkg/hap/camera/ch118_setup_endpoints.go @@ -3,10 +3,13 @@ package camera const TypeSetupEndpoints = "118" type SetupEndpoints struct { - SessionID []byte `tlv8:"1"` - ControllerAddr Addr `tlv8:"3"` - VideoCrypto CryptoSuite `tlv8:"4"` - AudioCrypto CryptoSuite `tlv8:"5"` + SessionID string `tlv8:"1"` + Status []byte `tlv8:"2"` + Address Addr `tlv8:"3"` + VideoCrypto CryptoSuite `tlv8:"4"` + AudioCrypto CryptoSuite `tlv8:"5"` + VideoSSRC []uint32 `tlv8:"6"` + AudioSSRC []uint32 `tlv8:"7"` } type Addr struct { @@ -18,16 +21,6 @@ type Addr struct { type CryptoSuite struct { CryptoType byte `tlv8:"1"` - MasterKey []byte `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM) - MasterSalt []byte `tlv8:"3"` // 14 byte -} - -type SetupEndpointsResponse struct { - SessionID []byte `tlv8:"1"` - Status byte `tlv8:"2"` - AccessoryAddr Addr `tlv8:"3"` - VideoCrypto CryptoSuite `tlv8:"4"` - AudioCrypto CryptoSuite `tlv8:"5"` - VideoSSRC uint32 `tlv8:"6"` - AudioSSRC uint32 `tlv8:"7"` + MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM) + MasterSalt string `tlv8:"3"` // 14 byte } diff --git a/pkg/hap/camera/ch120_streaming_status.go b/pkg/hap/camera/ch120_streaming_status.go index 6cde34e6..2fe53911 100644 --- a/pkg/hap/camera/ch120_streaming_status.go +++ b/pkg/hap/camera/ch120_streaming_status.go @@ -6,6 +6,7 @@ type StreamingStatus struct { Status byte `tlv8:"1"` } +//goland:noinspection ALL const ( StreamingStatusAvailable = 0 StreamingStatusBusy = 1 diff --git a/pkg/hap/camera/client.go b/pkg/hap/camera/client.go deleted file mode 100644 index 5e25b923..00000000 --- a/pkg/hap/camera/client.go +++ /dev/null @@ -1,97 +0,0 @@ -package camera - -import ( - "errors" - - "github.com/AlexxIT/go2rtc/pkg/hap" -) - -type Client struct { - client *hap.Client -} - -func NewClient(client *hap.Client) *Client { - return &Client{client: client} -} - -func (c *Client) StartStream(ses *Session) error { - // Step 1. Check if camera ready (free) to stream - srv, err := c.GetFreeStream() - if err != nil { - return err - } - if srv == nil { - return errors.New("no free streams") - } - - if ses.Answer, err = c.SetupEndpoins(srv, ses.Offer); err != nil { - return err - } - - return c.SetConfig(srv, ses.Config) -} - -// GetFreeStream search free streaming service. -// Usual every HomeKit camera can stream only to two clients simultaniosly. -// So it has two similar services for streaming. -func (c *Client) GetFreeStream() (srv *hap.Service, err error) { - accs, err := c.client.GetAccessories() - if err != nil { - return - } - - for _, srv = range accs[0].Services { - for _, char := range srv.Characters { - if char.Type == TypeStreamingStatus { - var status StreamingStatus - if err = char.ReadTLV8(&status); err != nil { - return - } - - if status.Status == StreamingStatusAvailable { - return - } - } - } - } - - return nil, nil -} - -func (c *Client) SetupEndpoins(srv *hap.Service, req *SetupEndpoints) (res *SetupEndpointsResponse, err error) { - // get setup endpoint character ID - char := srv.GetCharacter(TypeSetupEndpoints) - char.Event = nil - // encode new character value - if err = char.Write(req); err != nil { - return - } - // write (put) new endpoint value to device - if err = c.client.PutCharacters(char); err != nil { - return - } - - // get new endpoint value from device (response) - if err = c.client.GetCharacter(char); err != nil { - return - } - // decode new endpoint value - res = &SetupEndpointsResponse{} - if err = char.ReadTLV8(res); err != nil { - return - } - - return -} - -func (c *Client) SetConfig(srv *hap.Service, config *SelectedStreamConfig) error { - // get setup endpoint character ID - char := srv.GetCharacter(TypeSelectedStreamConfiguration) - char.Event = nil - // encode new character value - if err := char.Write(config); err != nil { - return err - } - // write (put) new character value to device - return c.client.PutCharacters(char) -} diff --git a/pkg/hap/camera/session.go b/pkg/hap/camera/session.go deleted file mode 100644 index 5ba3d4c1..00000000 --- a/pkg/hap/camera/session.go +++ /dev/null @@ -1,73 +0,0 @@ -package camera - -import ( - "crypto/rand" - "encoding/binary" -) - -type Session struct { - Offer *SetupEndpoints - Answer *SetupEndpointsResponse - Config *SelectedStreamConfig -} - -func NewSession(vp *SelectedVideoParams, ap *SelectedAudioParams) *Session { - vp.RTPParams = VideoRTPParams{ - PayloadType: 99, - SSRC: RandomUint32(), - MaxBitrate: 2048, - MinRTCPInterval: 10, - MaxMTU: 1200, // like WebRTC - } - ap.RTPParams = AudioRTPParams{ - PayloadType: 110, - SSRC: RandomUint32(), - MaxBitrate: 32, - MinRTCPInterval: 10, - ComfortNoisePayloadType: 98, - } - - sessionID := RandomBytes(16) - s := &Session{ - Offer: &SetupEndpoints{ - SessionID: sessionID, - VideoCrypto: CryptoSuite{ - MasterKey: RandomBytes(16), - MasterSalt: RandomBytes(14), - }, - AudioCrypto: CryptoSuite{ - MasterKey: RandomBytes(16), - MasterSalt: RandomBytes(14), - }, - }, - Config: &SelectedStreamConfig{ - Control: SessionControl{ - Session: string(sessionID), - Command: SessionCommandStart, - }, - VideoParams: *vp, - AudioParams: *ap, - }, - } - return s -} - -func (s *Session) SetLocalEndpoint(host string, port uint16) { - s.Offer.ControllerAddr = Addr{ - IPAddr: host, - VideoRTPPort: port, - AudioRTPPort: port, - } -} - -func RandomBytes(size int) []byte { - data := make([]byte, size) - _, _ = rand.Read(data) - return data -} - -func RandomUint32() uint32 { - data := make([]byte, 4) - _, _ = rand.Read(data) - return binary.BigEndian.Uint32(data) -} diff --git a/pkg/hap/camera/stream.go b/pkg/hap/camera/stream.go new file mode 100644 index 00000000..87c28f8d --- /dev/null +++ b/pkg/hap/camera/stream.go @@ -0,0 +1,177 @@ +package camera + +import ( + "errors" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/hap" + "github.com/AlexxIT/go2rtc/pkg/srtp" +) + +type Stream struct { + id string + client *hap.Client + service *hap.Service +} + +func NewStream( + client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, videoSession, audioSession *srtp.Session, +) (*Stream, error) { + stream := &Stream{ + id: core.RandString(16, 0), + client: client, + } + + if err := stream.GetFreeStream(); err != nil { + return nil, err + } + + if err := stream.ExchangeEndpoints(videoSession, audioSession); err != nil { + return nil, err + } + + videoCodec.RTPParams = []RTPParams{ + { + PayloadType: 99, + SSRC: videoSession.Local.SSRC, + MaxBitrate: 299, + MinRTCPInterval: 0.5, + MaxMTU: []uint16{1378}, + }, + } + audioCodec.RTPParams = []RTPParams{ + { + PayloadType: 110, + SSRC: audioSession.Local.SSRC, + MaxBitrate: 24, + MinRTCPInterval: 5, + + ComfortNoisePayloadType: []uint8{13}, + }, + } + audioCodec.ComfortNoise = []byte{0} + + config := &SelectedStreamConfig{ + Control: SessionControl{ + SessionID: stream.id, + Command: SessionCommandStart, + }, + VideoCodec: *videoCodec, + AudioCodec: *audioCodec, + } + + if err := stream.SetStreamConfig(config); err != nil { + return nil, err + } + + return stream, nil +} + +// GetFreeStream search free streaming service. +// Usual every HomeKit camera can stream only to two clients simultaniosly. +// So it has two similar services for streaming. +func (s *Stream) GetFreeStream() error { + acc, err := s.client.GetFirstAccessory() + if err != nil { + return err + } + + for _, srv := range acc.Services { + for _, char := range srv.Characters { + if char.Type == TypeStreamingStatus { + var status StreamingStatus + if err = char.ReadTLV8(&status); err != nil { + return err + } + + if status.Status == StreamingStatusAvailable { + s.service = srv + return nil + } + } + } + } + + return errors.New("hap: no free streams") +} + +func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) error { + req := SetupEndpoints{ + SessionID: s.id, + Address: Addr{ + IPVersion: 0, + IPAddr: videoSession.Local.Addr, + VideoRTPPort: videoSession.Local.Port, + AudioRTPPort: audioSession.Local.Port, + }, + VideoCrypto: CryptoSuite{ + MasterKey: string(videoSession.Local.MasterKey), + MasterSalt: string(videoSession.Local.MasterSalt), + }, + AudioCrypto: CryptoSuite{ + MasterKey: string(audioSession.Local.MasterKey), + MasterSalt: string(audioSession.Local.MasterSalt), + }, + } + + char := s.service.GetCharacter(TypeSetupEndpoints) + if err := char.Write(&req); err != nil { + return err + } + if err := s.client.PutCharacters(char); err != nil { + return err + } + + var res SetupEndpoints + if err := s.client.GetCharacter(char); err != nil { + return err + } + if err := char.ReadTLV8(&res); err != nil { + return err + } + + videoSession.Remote = &srtp.Endpoint{ + Addr: res.Address.IPAddr, + Port: res.Address.VideoRTPPort, + MasterKey: []byte(res.VideoCrypto.MasterKey), + MasterSalt: []byte(res.VideoCrypto.MasterSalt), + SSRC: res.VideoSSRC[0], + } + + audioSession.Remote = &srtp.Endpoint{ + Addr: res.Address.IPAddr, + Port: res.Address.AudioRTPPort, + MasterKey: []byte(res.AudioCrypto.MasterKey), + MasterSalt: []byte(res.AudioCrypto.MasterSalt), + SSRC: res.AudioSSRC[0], + } + + return nil +} + +func (s *Stream) SetStreamConfig(config *SelectedStreamConfig) error { + char := s.service.GetCharacter(TypeSelectedStreamConfiguration) + if err := char.Write(config); err != nil { + return err + } + if err := s.client.PutCharacters(char); err != nil { + return err + } + + return s.client.GetCharacter(char) +} + +func (s *Stream) Close() error { + config := &SelectedStreamConfig{ + Control: SessionControl{ + SessionID: s.id, + Command: SessionCommandEnd, + }, + } + + char := s.service.GetCharacter(TypeSelectedStreamConfiguration) + if err := char.Write(config); err != nil { + return err + } + return s.client.PutCharacters(char) +} diff --git a/pkg/hap/character.go b/pkg/hap/character.go index 6fc1c64f..90b8dee1 100644 --- a/pkg/hap/character.go +++ b/pkg/hap/character.go @@ -9,16 +9,23 @@ import ( "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" ) +// Character - Aqara props order +// Value should be omit for PW +// Value may be empty for PR type Character struct { - AID int `json:"aid,omitempty"` - IID int `json:"iid"` - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Value any `json:"value,omitempty"` - Event any `json:"ev,omitempty"` - Perms []string `json:"perms,omitempty"` - Description string `json:"description,omitempty"` - //MaxDataLen int `json:"maxDataLen"` + IID uint64 `json:"iid"` + Type string `json:"type"` + Format string `json:"format"` + Value any `json:"value,omitempty"` + Perms []string `json:"perms"` + + //Descr string `json:"description,omitempty"` + //MaxLen int `json:"maxLen,omitempty"` + //Unit string `json:"unit,omitempty"` + //MinValue any `json:"minValue,omitempty"` + //MaxValue any `json:"maxValue,omitempty"` + //MinStep any `json:"minStep,omitempty"` + //ValidVal []any `json:"valid-values,omitempty"` listeners map[io.Writer]bool } @@ -64,10 +71,12 @@ func (c *Character) NotifyListeners(ignore io.Writer) error { // GenerateEvent with raw HTTP headers func (c *Character) GenerateEvent() (data []byte, err error) { - chars := Characters{ - Characters: []*Character{{AID: DeviceAID, IID: c.IID, Value: c.Value}}, + v := JSONCharacters{ + Value: []JSONCharacter{ + {AID: DeviceAID, IID: c.IID, Value: c.Value}, + }, } - if data, err = json.Marshal(chars); err != nil { + if data, err = json.Marshal(v); err != nil { return } diff --git a/pkg/hap/client.go b/pkg/hap/client.go index f02ce7d8..0c7c3b11 100644 --- a/pkg/hap/client.go +++ b/pkg/hap/client.go @@ -37,9 +37,9 @@ type Client struct { ClientPrivate []byte OnEvent func(res *http.Response) - Output func(msg any) + //Output func(msg any) - conn net.Conn + Conn net.Conn reader *bufio.Reader } @@ -89,21 +89,21 @@ func (c *Client) Dial() (err error) { return false }) - if c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil { + if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil { return } - c.reader = bufio.NewReader(c.conn) + c.reader = bufio.NewReader(c.Conn) // STEP M1: send our session public to device sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() // 1. Send sessionPublic plainM1 := struct { - PublicKey []byte `tlv8:"3"` + PublicKey string `tlv8:"3"` State byte `tlv8:"6"` }{ - PublicKey: sessionPublic, + PublicKey: string(sessionPublic), State: StateM1, } res, err := c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(plainM1)) @@ -113,19 +113,19 @@ func (c *Client) Dial() (err error) { // STEP M2: unpack deviceID from response var cipherM2 struct { - PublicKey []byte `tlv8:"3"` - EncryptedData []byte `tlv8:"5"` + PublicKey string `tlv8:"3"` + EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` } if err = tlv8.UnmarshalReader(res.Body, &cipherM2); err != nil { return err } if cipherM2.State != StateM2 { - return NewResponseError(plainM1, cipherM2) + return newResponseError(plainM1, cipherM2) } // 1. generate session shared key - sessionShared, err := curve25519.SharedSecret(sessionPrivate, cipherM2.PublicKey) + sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(cipherM2.PublicKey)) if err != nil { return } @@ -138,7 +138,7 @@ func (c *Client) Dial() (err error) { } // 2. decrypt M2 response with session key - b, err := chacha20poly1305.Decrypt(sessionKey, "PV-Msg02", cipherM2.EncryptedData) + b, err := chacha20poly1305.Decrypt(sessionKey, "PV-Msg02", []byte(cipherM2.EncryptedData)) if err != nil { return } @@ -146,7 +146,7 @@ func (c *Client) Dial() (err error) { // 3. unpack payload from TLV8 var plainM2 struct { Identifier string `tlv8:"1"` - Signature []byte `tlv8:"10"` + Signature string `tlv8:"10"` } if err = tlv8.Unmarshal(b, &plainM2); err != nil { return @@ -156,7 +156,7 @@ func (c *Client) Dial() (err error) { // device session + device id + our session if c.DevicePublic != nil { b = Append(cipherM2.PublicKey, plainM2.Identifier, sessionPublic) - if !ed25519.ValidateSignature(c.DevicePublic, b, plainM2.Signature) { + if !ed25519.ValidateSignature(c.DevicePublic, b, []byte(plainM2.Signature)) { return errors.New("hap: ValidateSignature") } } @@ -172,10 +172,10 @@ func (c *Client) Dial() (err error) { // 2. generate payload plainM3 := struct { Identifier string `tlv8:"1"` - Signature []byte `tlv8:"10"` + Signature string `tlv8:"10"` }{ Identifier: c.ClientID, - Signature: b, + Signature: string(b), } if b, err = tlv8.Marshal(plainM3); err != nil { return @@ -188,11 +188,11 @@ func (c *Client) Dial() (err error) { // 4. generate request cipherM3 := struct { - EncryptedData []byte `tlv8:"5"` + EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` }{ State: StateM3, - EncryptedData: b, + EncryptedData: string(b), } if res, err = c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(cipherM3)); err != nil { return @@ -206,25 +206,25 @@ func (c *Client) Dial() (err error) { return } if plainM4.State != StateM4 { - return NewResponseError(cipherM3, plainM4) + return newResponseError(cipherM3, plainM4) } // like tls.Client wrapper over net.Conn - if c.conn, err = secure.Client(c.conn, sessionShared, true); err != nil { + if c.Conn, err = secure.Client(c.Conn, sessionShared, true); err != nil { return } // new reader for new conn - c.reader = bufio.NewReaderSize(c.conn, 32*1024) // 32K like default request body + c.reader = bufio.NewReaderSize(c.Conn, 32*1024) // 32K like default request body return } func (c *Client) Close() error { - if c.conn == nil { + if c.Conn == nil { return nil } - conn := c.conn - c.conn = nil + conn := c.Conn + c.Conn = nil return conn.Close() } @@ -234,23 +234,26 @@ func (c *Client) GetAccessories() ([]*Accessory, error) { return nil, err } - var ac Accessories - if err = json.NewDecoder(res.Body).Decode(&ac); err != nil { + var v JSONAccessories + if err = json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, err } - for _, accs := range ac.Accessories { - for _, serv := range accs.Services { - for _, char := range serv.Characters { - char.AID = accs.AID - } - } - } - - return ac.Accessories, nil + return v.Value, nil } -func (c *Client) GetCharacters(query string) ([]*Character, error) { +func (c *Client) GetFirstAccessory() (*Accessory, error) { + accs, err := c.GetAccessories() + if err != nil { + return nil, err + } + if len(accs) == 0 { + return nil, errors.New("hap: GetAccessories zero answer") + } + return accs[0], nil +} + +func (c *Client) GetCharacters(query string) ([]JSONCharacter, error) { res, err := c.Get(PathCharacteristics + "?id=" + query) if err != nil { return nil, err @@ -261,15 +264,15 @@ func (c *Client) GetCharacters(query string) ([]*Character, error) { return nil, err } - var ch Characters - if err = json.Unmarshal(data, &ch); err != nil { + var v JSONCharacters + if err = json.Unmarshal(data, &v); err != nil { return nil, err } - return ch.Characters, nil + return v.Value, nil } func (c *Client) GetCharacter(char *Character) error { - query := fmt.Sprintf("%d.%d", char.AID, char.IID) + query := fmt.Sprintf("%d.%d", DeviceAID, char.IID) chars, err := c.GetCharacters(query) if err != nil { return err @@ -279,20 +282,21 @@ func (c *Client) GetCharacter(char *Character) error { } func (c *Client) PutCharacters(characters ...*Character) error { + var v JSONCharacters for i, char := range characters { - if char.Event != nil { - char = &Character{AID: char.AID, IID: char.IID, Event: char.Event} - } else { - char = &Character{AID: char.AID, IID: char.IID, Value: char.Value} - } + v.Value = append(v.Value, JSONCharacter{ + AID: 1, + IID: char.IID, + Value: char.Value, + }) characters[i] = char } - data, err := json.Marshal(Characters{characters}) + body, err := json.Marshal(v) if err != nil { return err } - _, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(data)) + _, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body)) if err != nil { return err } @@ -312,8 +316,9 @@ func (c *Client) GetImage(width, height int) ([]byte, error) { return io.ReadAll(res.Body) } -func (c *Client) LocalAddr() string { - return c.conn.LocalAddr().String() +func (c *Client) LocalIP() string { + addr := c.Conn.LocalAddr().(*net.TCPAddr) + return addr.IP.To4().String() } func DecodeKey(s string) []byte { diff --git a/pkg/hap/client_http.go b/pkg/hap/client_http.go index c3464fb4..a6c775a2 100644 --- a/pkg/hap/client_http.go +++ b/pkg/hap/client_http.go @@ -4,7 +4,6 @@ import ( "errors" "io" "net/http" - "time" ) const ( @@ -20,10 +19,7 @@ const ( ) func (c *Client) Do(req *http.Request) (*http.Response, error) { - if err := c.conn.SetWriteDeadline(time.Now().Add(ConnDeadline)); err != nil { - return nil, err - } - if err := req.Write(c.conn); err != nil { + if err := req.Write(c.Conn); err != nil { return nil, err } return http.ReadResponse(c.reader, req) diff --git a/pkg/hap/client_pairing.go b/pkg/hap/client_pairing.go index 56a2cd1a..bb114391 100644 --- a/pkg/hap/client_pairing.go +++ b/pkg/hap/client_pairing.go @@ -4,9 +4,7 @@ import ( "bufio" "crypto/sha512" "errors" - "fmt" "net" - "strings" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" @@ -21,9 +19,9 @@ func Pair(deviceID, pin string) (*Client, error) { var mfi bool _ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool { - if entry.Complete() && entry.Info["id"] == deviceID { + if entry.Complete() && entry.Info[TXTDeviceID] == deviceID { addr = entry.Addr() - mfi = entry.Info["ff"] == "1" + mfi = entry.Info[TXTFeatureFlags] == "1" return true } return false @@ -44,19 +42,16 @@ func Pair(deviceID, pin string) (*Client, error) { } func (c *Client) Pair(mfi bool, pin string) (err error) { - pin = strings.ReplaceAll(pin, "-", "") - if len(pin) != 8 { - return fmt.Errorf("wrong PIN format: %s", pin) + if pin, err = SanitizePin(pin); err != nil { + return err } - pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:] // 123-45-678 - - c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout) + c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout) if err != nil { return } - c.reader = bufio.NewReader(c.conn) + c.reader = bufio.NewReader(c.Conn) // STEP M1. Send HELLO plainM1 := struct { @@ -76,8 +71,8 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { // STEP M2. Read Device Salt and session PublicKey var plainM2 struct { - Salt []byte `tlv8:"2"` - SessionKey []byte `tlv8:"3"` // server public key, aka session.B + Salt string `tlv8:"2"` + SessionKey string `tlv8:"3"` // server public key, aka session.B State byte `tlv8:"6"` Error byte `tlv8:"7"` } @@ -85,7 +80,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { return } if plainM2.State != StateM2 { - return NewResponseError(plainM1, plainM2) + return newResponseError(plainM1, plainM2) } if plainM2.Error != 0 { return newPairingError(plainM2.Error) @@ -106,19 +101,19 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { // username: "Pair-Setup", password: PIN (with dashes) session := pake.NewClientSession(username, []byte(pin)) - sessionShared, err := session.ComputeKey(plainM2.Salt, plainM2.SessionKey) + sessionShared, err := session.ComputeKey([]byte(plainM2.Salt), []byte(plainM2.SessionKey)) if err != nil { return } // STEP M3. Send request plainM3 := struct { - SessionKey []byte `tlv8:"3"` - Proof []byte `tlv8:"4"` + SessionKey string `tlv8:"3"` + Proof string `tlv8:"4"` State byte `tlv8:"6"` }{ - SessionKey: session.GetA(), // client public key, aka session.A - Proof: session.ComputeAuthenticator(), + SessionKey: string(session.GetA()), // client public key, aka session.A + Proof: string(session.ComputeAuthenticator()), State: StateM3, } if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM3)); err != nil { @@ -127,7 +122,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { // STEP M4. Read response var plainM4 struct { - Proof []byte `tlv8:"4"` // server proof + Proof string `tlv8:"4"` // server proof State byte `tlv8:"6"` Error byte `tlv8:"7"` } @@ -135,15 +130,15 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { return } if plainM4.State != StateM4 { - return NewResponseError(plainM3, plainM4) + return newResponseError(plainM3, plainM4) } if plainM4.Error != 0 { return newPairingError(plainM4.Error) } // STEP M4. Verify response - if !session.VerifyServerAuthenticator(plainM4.Proof) { - return errors.New("hap: wrong server auth") + if !session.VerifyServerAuthenticator([]byte(plainM4.Proof)) { + return errors.New("hap: VerifyServerAuthenticator") } // STEP M5. Generate signature @@ -163,12 +158,12 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { // STEP M5. Generate payload plainM5 := struct { Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - Signature []byte `tlv8:"10"` + PublicKey string `tlv8:"3"` + Signature string `tlv8:"10"` }{ Identifier: c.ClientID, - PublicKey: c.ClientPublic(), - Signature: signature, + PublicKey: string(c.ClientPublic()), + Signature: string(signature), } if b, err = tlv8.Marshal(plainM5); err != nil { return @@ -188,10 +183,10 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { // STEP M5. Send request cipherM5 := struct { - EncryptedData []byte `tlv8:"5"` + EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` }{ - EncryptedData: b, + EncryptedData: string(b), State: StateM5, } if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(cipherM5)); err != nil { @@ -200,7 +195,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { // STEP M6. Read response cipherM6 := struct { - EncryptedData []byte `tlv8:"5"` + EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` Error byte `tlv8:"7"` }{} @@ -208,19 +203,19 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { return } if cipherM6.State != StateM6 || cipherM6.Error != 0 { - return NewResponseError(plainM5, cipherM6) + return newResponseError(plainM5, cipherM6) } // STEP M6. Decrypt payload - b, err = chacha20poly1305.Decrypt(encryptKey, "PS-Msg06", cipherM6.EncryptedData) + b, err = chacha20poly1305.Decrypt(encryptKey, "PS-Msg06", []byte(cipherM6.EncryptedData)) if err != nil { return } plainM6 := struct { Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - Signature []byte `tlv8:"10"` + PublicKey string `tlv8:"3"` + Signature string `tlv8:"10"` }{} if err = tlv8.Unmarshal(b, &plainM6); err != nil { return @@ -235,15 +230,15 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { } b = Append(remoteSign, plainM6.Identifier, plainM6.PublicKey) - if !ed25519.ValidateSignature(plainM6.PublicKey, b, plainM6.Signature) { - return errors.New("hap: wrong accessory sign") + if !ed25519.ValidateSignature([]byte(plainM6.PublicKey), b, []byte(plainM6.Signature)) { + return errors.New("hap: ValidateSignature") } if c.DeviceID != plainM6.Identifier { return errors.New("hap: wrong DeviceID: " + plainM6.Identifier) } - c.DevicePublic = plainM6.PublicKey + c.DevicePublic = []byte(plainM6.PublicKey) return nil } @@ -264,7 +259,7 @@ func (c *Client) ListPairings() error { // TODO: don't know how to fix array of items var plainM2 struct { Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` + PublicKey string `tlv8:"3"` State byte `tlv8:"6"` Permission byte `tlv8:"11"` } @@ -279,13 +274,13 @@ func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) e plainM1 := struct { Method byte `tlv8:"0"` Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` + PublicKey string `tlv8:"3"` State byte `tlv8:"6"` Permission byte `tlv8:"11"` }{ Method: MethodAddPairing, Identifier: clientID, - PublicKey: clientPublic, + PublicKey: string(clientPublic), State: StateM1, Permission: PermissionUser, } @@ -330,7 +325,7 @@ func (c *Client) DeletePairing(id string) error { return err } if plainM2.State != StateM2 { - return NewResponseError(plainM1, plainM2) + return newResponseError(plainM1, plainM2) } return nil diff --git a/pkg/hap/helpers.go b/pkg/hap/helpers.go index b23a4975..df98c88d 100644 --- a/pkg/hap/helpers.go +++ b/pkg/hap/helpers.go @@ -3,12 +3,10 @@ package hap import ( "crypto/ed25519" "crypto/rand" - "crypto/sha512" "encoding/hex" - "encoding/json" + "errors" "fmt" - "io" - "net/http" + "strings" ) const ( @@ -30,6 +28,12 @@ const ( // - 0100b - A problem has been detected on the accessory TXTStatusFlags = "sf" // Status flags (ex. 0, 1) + StatusPaired = "0" + StatusNotPaired = "1" + + CategoryBridge = "2" + CategoryCamera = "17" + StateM1 = 1 StateM2 = 2 StateM3 = 3 @@ -43,28 +47,41 @@ const ( MethodAddPairing = 3 MethodDeletePairing = 4 MethodListPairings = 5 -) -const ( PermissionUser = 0 PermissionAdmin = 1 ) const DeviceAID = 1 // TODO: fix someday +type JSONAccessories struct { + Value []*Accessory `json:"accessories"` +} + +type JSONCharacters struct { + Value []JSONCharacter `json:"characteristics"` +} + +type JSONCharacter struct { + AID uint8 `json:"aid"` + IID uint64 `json:"iid"` + Value any `json:"value"` +} + +func SanitizePin(pin string) (string, error) { + s := strings.ReplaceAll(pin, "-", "") + if len(s) != 8 { + return "", errors.New("hap: wrong PIN format: " + pin) + } + // 123-45-678 + return s[:3] + "-" + s[3:5] + "-" + s[5:], nil +} + func GenerateKey() []byte { _, key, _ := ed25519.GenerateKey(nil) return key } -func GenerateID(name string) string { - sum := sha512.Sum512([]byte(name)) - return fmt.Sprintf( - "%02X:%02X:%02X:%02X:%02X:%02X", - sum[0], sum[1], sum[2], sum[3], sum[4], sum[5], - ) -} - func GenerateUUID() string { //12345678-9012-3456-7890-123456789012 data := make([]byte, 16) @@ -87,25 +104,10 @@ func Append(items ...any) (b []byte) { return } -func NewResponseError(req, res any) error { +func newRequestError(req any) error { + return fmt.Errorf("hap: wrong request: %#v", req) +} + +func newResponseError(req, res any) error { return fmt.Errorf("hap: wrong response: %#v, on request: %#v", res, req) } - -func UnmarshalEvent(res *http.Response) (char *Character, err error) { - var data []byte - if data, err = io.ReadAll(res.Body); err != nil { - return - } - - ch := Characters{} - if err = json.Unmarshal(data, &ch); err != nil { - return - } - - if len(ch.Characters) > 1 { - panic("not implemented") - } - - char = ch.Characters[0] - return -} diff --git a/pkg/hap/tlv8/tlv8.go b/pkg/hap/tlv8/tlv8.go index 69edb9ee..41a6de58 100644 --- a/pkg/hap/tlv8/tlv8.go +++ b/pkg/hap/tlv8/tlv8.go @@ -85,10 +85,6 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) { v := value.Uint() return append(b, tag, 1, byte(v)), nil - case reflect.Int8: - v := value.Int() - return append(b, tag, 1, byte(v)), nil - case reflect.Uint16: v := value.Uint() return append(b, tag, 2, byte(v), byte(v>>8)), nil @@ -103,7 +99,13 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) { case reflect.String: v := value.String() - b = append(b, tag, byte(len(v))) + l := len(v) // support "big" string + for ; l > 255; l -= 255 { + b = append(b, tag, 255) + b = append(b, v[:255]...) + v = v[255:] + } + b = append(b, tag, byte(l)) return append(b, v...), nil case reflect.Array: @@ -117,19 +119,6 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) { } case reflect.Slice: - // byte array - if value.Type().Elem().Kind() == reflect.Uint8 { - v := value.Bytes() - l := len(v) - for ; l > 255; l -= 255 { - b = append(b, tag, 255) - b = append(b, v[:255]...) - v = v[255:] - } - b = append(b, tag, byte(l)) - return append(b, v...), nil - } - for i := 0; i < value.Len(); i++ { if i > 0 { b = append(b, 0, 0) @@ -175,24 +164,30 @@ func Unmarshal(data []byte, v any) error { } value := reflect.ValueOf(v) - kind := value.Type().Kind() + kind := value.Kind() if kind != reflect.Pointer { return errors.New("tlv8: value should be pointer: " + kind.String()) } value = value.Elem() - kind = value.Type().Kind() + kind = value.Kind() - switch kind { - case reflect.Struct: - return unmarshalStruct(data, value) + if kind == reflect.Interface { + value = value.Elem() + kind = value.Kind() } - return errors.New("tlv8: not implemented: " + kind.String()) + if kind != reflect.Struct { + return errors.New("tlv8: not implemented: " + kind.String()) + } + + return unmarshalStruct(data, value) } func unmarshalStruct(b []byte, value reflect.Value) error { + var waitSlice bool + for len(b) >= 2 { t := b[0] l := int(b[1]) @@ -200,6 +195,7 @@ func unmarshalStruct(b []byte, value reflect.Value) error { // array item divider if t == 0 && l == 0 { b = b[2:] + waitSlice = true continue } @@ -228,6 +224,13 @@ func unmarshalStruct(b []byte, value reflect.Value) error { return fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name()) } + if waitSlice { + if valueField.Kind() != reflect.Slice { + return fmt.Errorf("tlv8: should be slice T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name()) + } + waitSlice = false + } + if err := unmarshalValue(v, valueField); err != nil { return err } @@ -244,12 +247,6 @@ func unmarshalValue(v []byte, value reflect.Value) error { } value.SetUint(uint64(v[0])) - case reflect.Int8: - if len(v) != 1 { - return errors.New("tlv8: wrong size: " + value.Type().Name()) - } - value.SetInt(int64(v[0])) - case reflect.Uint16: if len(v) != 2 { return errors.New("tlv8: wrong size: " + value.Type().Name()) @@ -280,11 +277,6 @@ func unmarshalValue(v []byte, value reflect.Value) error { return nil case reflect.Slice: - if value.Type().Elem().Kind() == reflect.Uint8 { - value.SetBytes(v) - return nil - } - i := growSlice(value) return unmarshalValue(v, value.Index(i)) diff --git a/pkg/hap/tlv8/tlv8_test.go b/pkg/hap/tlv8/tlv8_test.go index 0cab43bc..5ac41fec 100644 --- a/pkg/hap/tlv8/tlv8_test.go +++ b/pkg/hap/tlv8/tlv8_test.go @@ -1,6 +1,7 @@ package tlv8 import ( + "encoding/hex" "testing" "github.com/stretchr/testify/require" @@ -36,3 +37,73 @@ func TestMarshal(t *testing.T) { require.Equal(t, src, dst) } + +func TestBytes(t *testing.T) { + bytes := make([]byte, 255) + for i := 0; i < len(bytes); i++ { + bytes[i] = byte(i) + } + + type Struct struct { + String string `tlv8:"1"` + } + src := Struct{ + String: string(bytes), + } + + b, err := Marshal(src) + require.Nil(t, err) + + var dst Struct + err = Unmarshal(b, &dst) + require.Nil(t, err) + + require.Equal(t, src, dst) + require.Equal(t, bytes, []byte(dst.String)) +} + +func TestVideoCodecParams(t *testing.T) { + type VideoCodecParams struct { + ProfileID []byte `tlv8:"1"` + Level []byte `tlv8:"2"` + PacketizationMode byte `tlv8:"3"` + CVOEnabled []byte `tlv8:"4"` + CVOID []byte `tlv8:"5"` + } + + src, err := hex.DecodeString("0101010201000000020102030100040100") + require.Nil(t, err) + + var v VideoCodecParams + err = Unmarshal(src, &v) + require.Nil(t, err) + + dst, err := Marshal(v) + require.Nil(t, err) + + require.Equal(t, src, dst) +} + +func TestInterface(t *testing.T) { + type Struct struct { + Byte byte `tlv8:"1"` + } + + src := Struct{ + Byte: 1, + } + var v1 any = &src + + b, err := Marshal(v1) + require.Nil(t, err) + + require.Equal(t, []byte{1, 1, 1}, b) + + var dst Struct + var v2 any = &dst + + err = Unmarshal(b, v2) + require.Nil(t, err) + + require.Equal(t, src, dst) +} diff --git a/pkg/homekit/client.go b/pkg/homekit/client.go index 9d4a3bfa..5969da40 100644 --- a/pkg/homekit/client.go +++ b/pkg/homekit/client.go @@ -3,14 +3,12 @@ package homekit import ( "encoding/json" "errors" - "fmt" + "math/rand" "net" "net/url" - "sync/atomic" "time" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/camera" "github.com/AlexxIT/go2rtc/pkg/srtp" @@ -18,31 +16,28 @@ import ( ) type Client struct { - core.Listener + core.SuperProducer - conn *hap.Client - server *srtp.Server - config *StreamConfig + hap *hap.Client + srtp *srtp.Server - medias []*core.Media - receivers []*core.Receiver + videoConfig camera.SupportedVideoStreamConfig + audioConfig camera.SupportedAudioStreamConfig - sessions []*srtp.Session + videoSession *srtp.Session + audioSession *srtp.Session + + stream *camera.Stream } -type StreamConfig struct { - Video camera.SupportedVideoStreamConfig - Audio camera.SupportedAudioStreamConfig -} - -func NewClient(rawURL string, server *srtp.Server) (*Client, error) { +func Dial(rawURL string, server *srtp.Server) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err } query := u.Query() - c := &hap.Client{ + conn := &hap.Client{ DeviceAddress: u.Host, DeviceID: query.Get("device_id"), DevicePublic: hap.DecodeKey(query.Get("device_public")), @@ -50,338 +45,125 @@ func NewClient(rawURL string, server *srtp.Server) (*Client, error) { ClientPrivate: hap.DecodeKey(query.Get("client_private")), } - return &Client{conn: c, server: server}, nil + if err = conn.Dial(); err != nil { + return nil, err + } + + return &Client{hap: conn, srtp: server}, nil } -func (c *Client) Dial() error { - return c.conn.Dial() +func (c *Client) Conn() net.Conn { + return c.hap.Conn } func (c *Client) GetMedias() []*core.Media { - if c.medias != nil { - return c.medias + if c.Medias != nil { + return c.Medias } - accs, err := c.conn.GetAccessories() + acc, err := c.hap.GetFirstAccessory() if err != nil { return nil } - acc := accs[0] - - c.config = &StreamConfig{} - - // get supported video config (not really necessary) char := acc.GetCharacter(camera.TypeSupportedVideoStreamConfiguration) if char == nil { return nil } - if err = char.ReadTLV8(&c.config.Video); err != nil { + if err = char.ReadTLV8(&c.videoConfig); err != nil { return nil } - for _, videoCodec := range c.config.Video.Codecs { - var name string - - switch videoCodec.CodecType { - case camera.VideoCodecTypeH264: - name = core.CodecH264 - default: - continue - } - - for _, params := range videoCodec.CodecParams { - codec := &core.Codec{ - Name: name, - ClockRate: 90000, - FmtpLine: "profile-level-id=", - } - - switch params.ProfileID { - case camera.VideoCodecProfileConstrainedBaseline: - codec.FmtpLine += "4200" // 4240? - case camera.VideoCodecProfileMain: - codec.FmtpLine += "4D00" // 4D40? - case camera.VideoCodecProfileHigh: - codec.FmtpLine += "6400" - default: - continue - } - - switch params.Level { - case camera.VideoCodecLevel31: - codec.FmtpLine += "1F" - case camera.VideoCodecLevel32: - codec.FmtpLine += "20" - case camera.VideoCodecLevel40: - codec.FmtpLine += "28" - default: - continue - } - - media := &core.Media{ - Kind: core.KindVideo, Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{codec}, - } - c.medias = append(c.medias, media) - } - } - char = acc.GetCharacter(camera.TypeSupportedAudioStreamConfiguration) if char == nil { return nil } - if err = char.ReadTLV8(&c.config.Audio); err != nil { + if err = char.ReadTLV8(&c.audioConfig); err != nil { return nil } - for _, audioCodec := range c.config.Audio.Codecs { - var name string - - switch audioCodec.CodecType { - case camera.AudioCodecTypePCMU: - name = core.CodecPCMU - case camera.AudioCodecTypePCMA: - name = core.CodecPCMA - case camera.AudioCodecTypeAACELD: - name = core.CodecELD - case camera.AudioCodecTypeOpus: - name = core.CodecOpus - default: - continue - } - - for _, params := range audioCodec.CodecParams { - codec := &core.Codec{ - Name: name, - Channels: uint16(params.Channels), - } - - if name == core.CodecELD { - // only this value supported by FFmpeg - codec.FmtpLine = "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000" - } - - switch params.SampleRate { - case camera.AudioCodecSampleRate8Khz: - codec.ClockRate = 8000 - case camera.AudioCodecSampleRate16Khz: - codec.ClockRate = 16000 - case camera.AudioCodecSampleRate24Khz: - codec.ClockRate = 24000 - default: - continue - } - - media := &core.Media{ - Kind: core.KindAudio, Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{codec}, - } - c.medias = append(c.medias, media) - } + c.Medias = []*core.Media{ + videoToMedia(c.videoConfig.Codecs), + audioToMedia(c.audioConfig.Codecs), } - media := &core.Media{ - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: core.CodecJPEG, - ClockRate: 90000, - PayloadType: core.PayloadTypeRAW, - }, - }, - } - c.medias = append(c.medias, media) - - return c.medias -} - -func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { - for _, track := range c.receivers { - if track.Codec == codec { - return track, nil - } - } - - track := core.NewReceiver(media, codec) - c.receivers = append(c.receivers, track) - return track, nil + return c.Medias } func (c *Client) Start() error { - if c.receivers == nil { + if c.Receivers == nil { return errors.New("producer without tracks") } - if c.receivers[0].Codec.Name == core.CodecJPEG { + if c.Receivers[0].Codec.Name == core.CodecJPEG { return c.startMJPEG() } - // get our server local IP-address - host, _, err := net.SplitHostPort(c.conn.LocalAddr()) + videoTrack := c.trackByKind(core.KindVideo) + videoCodec := trackToVideo(videoTrack, &c.videoConfig.Codecs[0]) + + audioTrack := c.trackByKind(core.KindAudio) + audioCodec := trackToAudio(audioTrack, &c.audioConfig.Codecs[0]) + + c.videoSession = &srtp.Session{Local: c.srtpEndpoint()} + c.audioSession = &srtp.Session{Local: c.srtpEndpoint()} + + var err error + c.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession) if err != nil { return err } - videoParams := &camera.SelectedVideoParams{ - CodecType: camera.VideoCodecTypeH264, - VideoAttrs: camera.VideoAttrs{ - Width: 1920, Height: 1080, Framerate: 30, - }, - } + c.srtp.AddSession(c.videoSession) + c.srtp.AddSession(c.audioSession) + + deadline := time.NewTimer(core.ConnDeadline) - videoTrack := c.trackByKind(core.KindVideo) if videoTrack != nil { - profile := h264.GetProfileLevelID(videoTrack.Codec.FmtpLine) - - switch profile[:2] { - case "42": - videoParams.CodecParams.ProfileID = camera.VideoCodecProfileConstrainedBaseline - case "4D": - videoParams.CodecParams.ProfileID = camera.VideoCodecProfileMain - case "64": - videoParams.CodecParams.ProfileID = camera.VideoCodecProfileHigh + c.videoSession.OnReadRTP = func(packet *rtp.Packet) { + deadline.Reset(core.ConnDeadline) + videoTrack.WriteRTP(packet) } - switch profile[4:] { - case "1F": - videoParams.CodecParams.Level = camera.VideoCodecLevel31 - case "20": - videoParams.CodecParams.Level = camera.VideoCodecLevel32 - case "28": - videoParams.CodecParams.Level = camera.VideoCodecLevel40 + if audioTrack != nil { + c.audioSession.OnReadRTP = audioTrack.WriteRTP } } else { - // if consumer don't need track - ask first track from camera - codec0 := c.config.Video.Codecs[0] - videoParams.CodecParams.ProfileID = codec0.CodecParams[0].ProfileID - videoParams.CodecParams.Level = codec0.CodecParams[0].Level - } - - audioParams := &camera.SelectedAudioParams{ - CodecParams: camera.AudioCodecParams{ - Bitrate: camera.AudioCodecBitrateVariable, - // RTPTime=20 => AAC-ELD packet size=480 - // RTPTime=30 => AAC-ELD packet size=480 - // RTPTime=40 => AAC-ELD packet size=480 - // RTPTime=60 => AAC-LD packet size=960 - RTPTime: 40, - }, - } - - audioTrack := c.trackByKind(core.KindAudio) - if audioTrack != nil { - audioParams.CodecParams.Channels = byte(audioTrack.Codec.Channels) - - switch audioTrack.Codec.Name { - case core.CodecPCMU: - audioParams.CodecType = camera.AudioCodecTypePCMU - case core.CodecPCMA: - audioParams.CodecType = camera.AudioCodecTypePCMA - case core.CodecELD: - audioParams.CodecType = camera.AudioCodecTypeAACELD - case core.CodecOpus: - audioParams.CodecType = camera.AudioCodecTypeOpus + c.audioSession.OnReadRTP = func(packet *rtp.Packet) { + deadline.Reset(core.ConnDeadline) + audioTrack.WriteRTP(packet) } - - switch audioTrack.Codec.ClockRate { - case 8000: - audioParams.CodecParams.SampleRate = camera.AudioCodecSampleRate8Khz - case 16000: - audioParams.CodecParams.SampleRate = camera.AudioCodecSampleRate16Khz - case 24000: - audioParams.CodecParams.SampleRate = camera.AudioCodecSampleRate24Khz - } - } else { - // if consumer don't need track - ask first track from camera - codec0 := c.config.Audio.Codecs[0] - audioParams.CodecType = codec0.CodecType - audioParams.CodecParams.Channels = codec0.CodecParams[0].Channels - audioParams.CodecParams.SampleRate = codec0.CodecParams[0].SampleRate } - // setup HomeKit stream session - session := camera.NewSession(videoParams, audioParams) - session.SetLocalEndpoint(host, c.server.Port()) - - // create client for processing camera accessory - cam := camera.NewClient(c.conn) - // try to start HomeKit stream - if err = cam.StartStream(session); err != nil { - return err - } - - // SRTP Video Session - videoSession := &srtp.Session{ - LocalSSRC: session.Config.VideoParams.RTPParams.SSRC, - RemoteSSRC: session.Answer.VideoSSRC, - Track: videoTrack, - } - if err = videoSession.SetKeys( - session.Offer.VideoCrypto.MasterKey, session.Offer.VideoCrypto.MasterSalt, - session.Answer.VideoCrypto.MasterKey, session.Answer.VideoCrypto.MasterSalt, - ); err != nil { - return err - } - - // SRTP Audio Session - audioSession := &srtp.Session{ - LocalSSRC: session.Config.AudioParams.RTPParams.SSRC, - RemoteSSRC: session.Answer.AudioSSRC, - Track: audioTrack, - } - if err = audioSession.SetKeys( - session.Offer.AudioCrypto.MasterKey, session.Offer.AudioCrypto.MasterSalt, - session.Answer.AudioCrypto.MasterKey, session.Answer.AudioCrypto.MasterSalt, - ); err != nil { - return err - } - - c.server.AddSession(videoSession) - c.server.AddSession(audioSession) - - c.sessions = []*srtp.Session{videoSession, audioSession} - - if audioSession.Track != nil { - audioSession.Deadline = time.NewTimer(core.ConnDeadline) - <-audioSession.Deadline.C - } else if videoSession.Track != nil { - videoSession.Deadline = time.NewTimer(core.ConnDeadline) - <-videoSession.Deadline.C - } + <-deadline.C return nil } func (c *Client) Stop() error { - for _, session := range c.sessions { - c.server.RemoveSession(session) - } + _ = c.SuperProducer.Close() - return c.conn.Close() + c.srtp.DelSession(c.videoSession) + c.srtp.DelSession(c.audioSession) + + return c.hap.Close() } func (c *Client) MarshalJSON() ([]byte, error) { - var recv uint32 - for _, session := range c.sessions { - recv += atomic.LoadUint32(&session.Recv) - } - info := &core.Info{ - Type: "HomeKit active producer", - URL: c.conn.URL(), - SDP: fmt.Sprintf("%+v", *c.config), - Medias: c.medias, - Receivers: c.receivers, - Recv: int(recv), + Type: "HomeKit active producer", + URL: c.hap.URL(), + //SDP: fmt.Sprintf("%+v", *c.config), + Medias: c.Medias, + Receivers: c.Receivers, + Recv: c.videoSession.Recv + c.audioSession.Recv, } return json.Marshal(info) } func (c *Client) trackByKind(kind string) *core.Receiver { - for _, receiver := range c.receivers { - if core.GetKind(receiver.Codec.Name) == kind { + for _, receiver := range c.Receivers { + if receiver.Codec.Kind() == kind { return receiver } } @@ -389,10 +171,10 @@ func (c *Client) trackByKind(kind string) *core.Receiver { } func (c *Client) startMJPEG() error { - receiver := c.receivers[0] + receiver := c.Receivers[0] for { - b, err := c.conn.GetImage(1920, 1080) + b, err := c.hap.GetImage(1920, 1080) if err != nil { return err } @@ -404,3 +186,40 @@ func (c *Client) startMJPEG() error { receiver.WriteRTP(packet) } } + +func (c *Client) srtpEndpoint() *srtp.Endpoint { + return &srtp.Endpoint{ + Addr: c.hap.LocalIP(), + Port: uint16(c.srtp.Port()), + MasterKey: []byte(core.RandString(16, 0)), + MasterSalt: []byte(core.RandString(14, 0)), + SSRC: rand.Uint32(), + } +} + +func limitter(handler core.HandlerFunc) core.HandlerFunc { + const sampleRate = 16000 + const sampleSize = 480 + + var send time.Duration + var firstTime time.Time + + return func(packet *rtp.Packet) { + now := time.Now() + + if send != 0 { + elapsed := now.Sub(firstTime) * sampleRate / time.Second + if send+sampleSize > elapsed { + return // drop overflow frame + } + } else { + firstTime = now + } + + send += sampleSize + + packet.Timestamp = uint32(send) + + handler(packet) + } +} diff --git a/pkg/homekit/helpers.go b/pkg/homekit/helpers.go new file mode 100644 index 00000000..0af8192e --- /dev/null +++ b/pkg/homekit/helpers.go @@ -0,0 +1,140 @@ +package homekit + +import ( + "encoding/hex" + + "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/hap/camera" +) + +var videoCodecs = [...]string{core.CodecH264} +var videoProfiles = [...]string{"4200", "4D00", "6400"} +var videoLevels = [...]string{"1F", "20", "28"} + +func videoToMedia(codecs []camera.VideoCodec) *core.Media { + media := &core.Media{ + Kind: core.KindVideo, Direction: core.DirectionRecvonly, + } + + for _, codec := range codecs { + for _, param := range codec.CodecParams { + for _, profileID := range param.ProfileID { + for _, level := range param.Level { + profile := videoProfiles[profileID] + videoLevels[level] + mediaCodec := &core.Codec{ + Name: videoCodecs[codec.CodecType], + ClockRate: 90000, + FmtpLine: "profile-level-id=" + profile, + } + media.Codecs = append(media.Codecs, mediaCodec) + } + } + } + } + + return media +} + +var audioCodecs = [...]string{core.CodecPCMU, core.CodecPCMA, core.CodecELD, core.CodecOpus} +var audioSampleRates = [...]uint32{8000, 16000, 24000} + +func audioToMedia(codecs []camera.AudioCodec) *core.Media { + media := &core.Media{ + Kind: core.KindAudio, Direction: core.DirectionRecvonly, + } + + for _, codec := range codecs { + for _, param := range codec.CodecParams { + for _, sampleRate := range param.SampleRate { + mediaCodec := &core.Codec{ + Name: audioCodecs[codec.CodecType], + ClockRate: audioSampleRates[sampleRate], + Channels: uint16(param.Channels), + } + + if mediaCodec.Name == core.CodecELD { + // onli this version works with FFmpeg + conf := aac.EncodeConfig(aac.TypeAACELD, 24000, 1, true) + mediaCodec.FmtpLine = aac.FMTP + hex.EncodeToString(conf) + } + + media.Codecs = append(media.Codecs, mediaCodec) + } + } + } + + return media +} + +func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.VideoCodec { + profileID := video0.CodecParams[0].ProfileID[0] + level := video0.CodecParams[0].Level[0] + + if track != nil { + profile := h264.GetProfileLevelID(track.Codec.FmtpLine) + + for i, s := range videoProfiles { + if s == profile[:4] { + profileID = byte(i) + break + } + } + + for i, s := range videoLevels { + if s == profile[4:] { + level = byte(i) + break + } + } + } + + return &camera.VideoCodec{ + CodecType: video0.CodecType, + CodecParams: []camera.VideoParams{ + { + ProfileID: []byte{profileID}, + Level: []byte{level}, + }, + }, + VideoAttrs: []camera.VideoAttrs{ + {Width: 1920, Height: 1080, Framerate: 30}, + }, + } +} + +func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodec) *camera.AudioCodec { + codecType := audio0.CodecType + channels := audio0.CodecParams[0].Channels + sampleRate := audio0.CodecParams[0].SampleRate[0] + + if track != nil { + channels = uint8(track.Codec.Channels) + + for i, s := range audioCodecs { + if s == track.Codec.Name { + codecType = byte(i) + break + } + } + + for i, s := range audioSampleRates { + if s == track.Codec.ClockRate { + sampleRate = byte(i) + break + } + } + } + + return &camera.AudioCodec{ + CodecType: codecType, + CodecParams: []camera.AudioParams{ + { + Channels: channels, + SampleRate: []byte{sampleRate}, + RTPTime: []uint8{20}, + }, + }, + } +} diff --git a/pkg/srtp/server.go b/pkg/srtp/server.go index ac206765..3115980e 100644 --- a/pkg/srtp/server.go +++ b/pkg/srtp/server.go @@ -3,80 +3,96 @@ package srtp import ( "encoding/binary" "net" - "sync/atomic" + "strconv" + "sync" ) -// Server using same UDP port for SRTP and for SRTCP as the iPhone does -// this is not really necessary but anyway type Server struct { + address string conn net.PacketConn sessions map[uint32]*Session + mu sync.Mutex } -func (s *Server) Port() uint16 { - addr := s.conn.LocalAddr().(*net.UDPAddr) - return uint16(addr.Port) +func NewServer(address string) *Server { + return &Server{address: address} } -func (s *Server) Close() error { - return s.conn.Close() +func (s *Server) Port() int { + if s.conn != nil { + return s.conn.LocalAddr().(*net.UDPAddr).Port + } + + _, a, _ := net.SplitHostPort(s.address) + i, _ := strconv.Atoi(a) + return i } func (s *Server) AddSession(session *Session) { + s.mu.Lock() + defer s.mu.Unlock() + + if err := session.Local.Init(); err != nil { + return + } + if err := session.Remote.Init(); err != nil { + return + } + + if len(s.sessions) == 0 { + var err error + if s.conn, err = net.ListenPacket("udp", s.address); err != nil { + return + } + go s.handle() + } + + session.conn = s.conn + if s.sessions == nil { s.sessions = map[uint32]*Session{} } - s.sessions[session.RemoteSSRC] = session + + s.sessions[session.Remote.SSRC] = session } -func (s *Server) RemoveSession(session *Session) { - delete(s.sessions, session.RemoteSSRC) +func (s *Server) DelSession(session *Session) { + s.mu.Lock() + + delete(s.sessions, session.Remote.SSRC) + + if len(s.sessions) == 0 { + _ = s.conn.Close() + } + + s.mu.Unlock() } -func (s *Server) Serve(conn net.PacketConn) error { - s.conn = conn - - buf := make([]byte, 2048) +func (s *Server) handle() error { + b := make([]byte, 2048) for { - n, addr, err := conn.ReadFrom(buf) + n, _, err := s.conn.ReadFrom(b) if err != nil { return err } - if s.sessions == nil { - continue - } - // Multiplexing RTP Data and Control Packets on a Single Port // https://datatracker.ietf.org/doc/html/rfc5761 - var handle func([]byte) error + switch packetType := b[1]; packetType { + case 99, 110, 0x80 | 99, 0x80 | 110: + // this is default position for SSRC in RTP packet + ssrc := binary.BigEndian.Uint32(b[8:]) + if session, ok := s.sessions[ssrc]; ok { + session.ReadRTP(b[:n]) + } - // this is default position for SSRC in RTP packet - ssrc := binary.BigEndian.Uint32(buf[8:]) - session, ok := s.sessions[ssrc] - if ok { - handle = session.HandleRTP - } else { + case 200, 201, 202, 203, 204, 205, 206, 207: // this is default position for SSRC in RTCP packet - ssrc = binary.BigEndian.Uint32(buf[4:]) - if session, ok = s.sessions[ssrc]; !ok { - continue // skip unknown ssrc + ssrc := binary.BigEndian.Uint32(b[4:]) + if session, ok := s.sessions[ssrc]; ok { + session.ReadRTCP(b[:n]) } - - handle = session.HandleRTCP - } - - if session.Write == nil { - session.Write = func(b []byte) (int, error) { - return conn.WriteTo(b, addr) - } - } - - atomic.AddUint32(&session.Recv, uint32(n)) - - if err = handle(buf[:n]); err != nil { - return err } } } diff --git a/pkg/srtp/session.go b/pkg/srtp/session.go index 82988c0a..adc590d5 100644 --- a/pkg/srtp/session.go +++ b/pkg/srtp/session.go @@ -1,154 +1,108 @@ package srtp import ( - "time" + "net" - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/pion/srtp/v2" ) type Session struct { - LocalSSRC uint32 // outgoing SSRC - RemoteSSRC uint32 // incoming SSRC + Local *Endpoint + Remote *Endpoint - localCtx *srtp.Context // write context - remoteCtx *srtp.Context // read context + OnReadRTP func(packet *rtp.Packet) - Write func(b []byte) (int, error) - Track *core.Receiver - Recv uint32 + Recv int // bytes recv + Send int // bytes send - Deadline *time.Timer - - lastSequence uint32 - lastTimestamp uint32 - //lastPacket *rtp.Packet - lastTime time.Time - jitter float64 - //sequenceCycle uint16 - totalLost uint32 + conn net.PacketConn // local conn endpoint + addr net.Addr // remote addr } -func (s *Session) LastTime() time.Time { - return s.lastTime +type Endpoint struct { + Addr string + Port uint16 + MasterKey []byte + MasterSalt []byte + SSRC uint32 + + srtp *srtp.Context } -func (s *Session) SetKeys(localKey, localSalt, remoteKey, remoteSalt []byte) (err error) { - s.localCtx, err = srtp.CreateContext(localKey, localSalt, GuessProfile(localKey)) +func (e *Endpoint) Init() error { + var profile srtp.ProtectionProfile + + switch len(e.MasterKey) { + case 16: + profile = srtp.ProtectionProfileAes128CmHmacSha1_80 + //case 32: + // return srtp.ProtectionProfileAes256CmHmacSha1_80 + } + + var err error + e.srtp, err = srtp.CreateContext(e.MasterKey, e.MasterSalt, profile) + return err +} + +func (s *Session) WriteRTP(packet *rtp.Packet) (int, error) { + b, err := packet.Marshal() + if err != nil { + return 0, err + } + + if b, err = s.Local.srtp.EncryptRTP(nil, b, nil); err != nil { + return 0, err + } + + return s.conn.WriteTo(b, s.addr) +} + +func (s *Session) ReadRTP(b []byte) { + packet := &rtp.Packet{} + + b, err := s.Remote.srtp.DecryptRTP(nil, b, &packet.Header) if err != nil { return } - s.remoteCtx, err = srtp.CreateContext(remoteKey, remoteSalt, GuessProfile(remoteKey)) - return + + if err = packet.Unmarshal(b); err != nil { + return + } + + if s.OnReadRTP != nil { + s.OnReadRTP(packet) + } } -func (s *Session) HandleRTP(data []byte) (err error) { - if data, err = s.remoteCtx.DecryptRTP(nil, data, nil); err != nil { - return - } - - if s.Track == nil { - return - } - - packet := &rtp.Packet{} - if err = packet.Unmarshal(data); err != nil { - return - } - - if s.Deadline != nil { - s.Deadline.Reset(core.ConnDeadline) - } - - now := time.Now() - - // https://www.ietf.org/rfc/rfc3550.txt - if s.lastTimestamp != 0 { - delta := packet.SequenceNumber - uint16(s.lastSequence) - - // lost packet - if delta > 1 { - s.totalLost += uint32(delta - 1) - } - - // D(i,j) = (Rj - Ri) - (Sj - Si) = (Rj - Sj) - (Ri - Si) - dTime := now.Sub(s.lastTime).Seconds()*float64(s.Track.Codec.ClockRate) - - float64(packet.Timestamp-s.lastTimestamp) - if dTime < 0 { - dTime = -dTime - } - // J(i) = J(i-1) + (|D(i-1,i)| - J(i-1))/16 - s.jitter += (dTime - s.jitter) / 16 - } - - // keeping cycles (overflow) - s.lastSequence = s.lastSequence&0xFFFF0000 | uint32(packet.SequenceNumber) - s.lastTimestamp = packet.Timestamp - s.lastTime = now - - s.Track.WriteRTP(packet) - - return -} - -func (s *Session) HandleRTCP(data []byte) (err error) { +func (s *Session) ReadRTCP(b []byte) { header := &rtcp.Header{} - if data, err = s.remoteCtx.DecryptRTCP(nil, data, header); err != nil { + b, err := s.Remote.srtp.DecryptRTCP(nil, b, header) + if err != nil { return } - if _, err = rtcp.Unmarshal(data); err != nil { + if _, err = rtcp.Unmarshal(b); err != nil { return } if header.Type == rtcp.TypeSenderReport { - err = s.KeepAlive() + _ = s.KeepAlive() } - - return } -func (s *Session) KeepAlive() (err error) { - rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC} - - if s.lastTimestamp > 0 { - //log.Printf("[RTCP] ssrc=%d seq=%d lost=%d jit=%.2f", s.RemoteSSRC, s.lastSequence, s.totalLost, s.jitter) - - rep.Reports = []rtcp.ReceptionReport{{ - SSRC: s.RemoteSSRC, - LastSequenceNumber: s.lastSequence, - LastSenderReport: s.lastTimestamp, - FractionLost: 0, // TODO - TotalLost: s.totalLost, - Delay: 0, // send just after receive - Jitter: uint32(s.jitter), - }} +func (s *Session) KeepAlive() error { + rep := rtcp.ReceiverReport{SSRC: s.Local.SSRC} + b, err := rep.Marshal() + if err != nil { + return err } - // we can send empty receiver response, but should send it to hold the connection - - var data []byte - if data, err = rep.Marshal(); err != nil { - return + if b, err = s.Local.srtp.EncryptRTCP(nil, b, nil); err != nil { + return err } - if data, err = s.localCtx.EncryptRTCP(nil, data, nil); err != nil { - return - } - - _, err = s.Write(data) - - return -} - -func GuessProfile(masterKey []byte) srtp.ProtectionProfile { - switch len(masterKey) { - case 16: - return srtp.ProtectionProfileAes128CmHmacSha1_80 - //case 32: - // return srtp.ProtectionProfileAes256CmHmacSha1_80 - } - return 0 + _, err = s.conn.WriteTo(b, s.addr) + return err } diff --git a/pkg/yaml/yaml.go b/pkg/yaml/yaml.go index 7d9d6345..6864a87f 100644 --- a/pkg/yaml/yaml.go +++ b/pkg/yaml/yaml.go @@ -7,6 +7,10 @@ import ( "gopkg.in/yaml.v3" ) +func Unmarshal(in []byte, out interface{}) (err error) { + return yaml.Unmarshal(in, out) +} + func Encode(v any, indent int) ([]byte, error) { b := bytes.NewBuffer(nil) e := yaml.NewEncoder(b)