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
+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)
}