Rewrite HomeKit client
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
+2
-19
@@ -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
|
||||
|
||||
+57
-7
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" //...
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+6
-7
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+124
-12
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ type StreamingStatus struct {
|
||||
Status byte `tlv8:"1"`
|
||||
}
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
StreamingStatusAvailable = 0
|
||||
StreamingStatusBusy = 1
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+21
-12
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+53
-48
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+36
-41
@@ -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
|
||||
|
||||
+36
-34
@@ -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
|
||||
}
|
||||
|
||||
+27
-35
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+106
-287
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
+60
-44
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+70
-116
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user