Rewrite HomeKit client
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user