Rewrite HomeKit client

This commit is contained in:
Alexey Khit
2023-08-30 21:52:06 +03:00
parent 7d65c60711
commit 22787b979d
30 changed files with 1094 additions and 916 deletions
+1 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+8
View File
@@ -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
View File
@@ -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]
}
+8
View File
@@ -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
View File
@@ -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,
},
},
}
}
+13 -11
View File
@@ -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 {
+19 -10
View File
@@ -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
}
+14
View File
@@ -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"`
}
+13 -33
View File
@@ -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"`
}
+9 -16
View File
@@ -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
}
+1
View File
@@ -6,6 +6,7 @@ type StreamingStatus struct {
Status byte `tlv8:"1"`
}
//goland:noinspection ALL
const (
StreamingStatusAvailable = 0
StreamingStatusBusy = 1
-97
View File
@@ -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)
}
-73
View File
@@ -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)
}
+177
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+1 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))
+71
View File
@@ -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
View File
@@ -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)
}
}
+140
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+4
View File
@@ -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)