Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds two way audio and support for Abode IOTA via HomeKit #1429

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion internal/homekit/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func discovery() ([]*api.Source, error) {
log.Trace().Msgf("[homekit] mdns=%s", entry)

category := entry.Info[hap.TXTCategory]
if entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
if entry.Complete() && (category == hap.CategoryBridge || category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
source := &api.Source{
Name: entry.Name,
Info: entry.Info[hap.TXTModel],
Expand Down
13 changes: 13 additions & 0 deletions internal/homekit/homekit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"net"
"net/http"
"strconv"
"strings"

"github.com/AlexxIT/go2rtc/internal/api"
Expand Down Expand Up @@ -143,6 +144,18 @@ func streamHandler(rawURL string) (core.Producer, error) {
if client != nil && rawQuery != "" {
query := streams.ParseQuery(rawQuery)
client.Bitrate = parseBitrate(query.Get("bitrate"))

if widthStr := query.Get("width"); widthStr != "" {
if width, err := strconv.Atoi(widthStr); err == nil {
client.Width = width
}
}

if heightStr := query.Get("height"); heightStr != "" {
if height, err := strconv.Atoi(heightStr); err == nil {
client.Height = height
}
}
}

return client, err
Expand Down
3 changes: 3 additions & 0 deletions pkg/hap/camera/s113_speaker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package camera

const TypeSpeaker = "113"
8 changes: 7 additions & 1 deletion pkg/hap/camera/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Stream struct {

func NewStream(
client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec,
videoSession, audioSession *srtp.Session, bitrate int,
videoSession, audioSession *srtp.Session, bitrate int, width int, height int,
) (*Stream, error) {
stream := &Stream{
id: core.RandString(16, 0),
Expand Down Expand Up @@ -46,6 +46,12 @@ func NewStream(
MaxMTU: []uint16{1378},
},
}

if width != 0 && height != 0 {
videoCodec.VideoAttrs[0].Width = uint16(width)
videoCodec.VideoAttrs[0].Height = uint16(height)
}

audioCodec.RTPParams = []RTPParams{
{
PayloadType: 110,
Expand Down
64 changes: 19 additions & 45 deletions pkg/hap/tlv8/tlv8.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"reflect"
Expand Down Expand Up @@ -186,57 +185,32 @@ func Unmarshal(data []byte, v any) error {
}

func unmarshalStruct(b []byte, value reflect.Value) error {
var waitSlice bool

for len(b) >= 2 {
t := b[0]
l := int(b[1])

// array item divider
if t == 0 && l == 0 {
b = b[2:]
waitSlice = true
continue
}

var v []byte

for {
if len(b) < 2+l {
return errors.New("tlv8: wrong size: " + value.Type().Name())
}

v = append(v, b[2:2+l]...)
b = b[2+l:]

// if size == 255 and same tag - continue read big payload
if l < 255 || len(b) < 2 || b[0] != t {
break
}

l = int(b[1])
}

tag := strconv.Itoa(int(t))

valueField, ok := getStructField(value, tag)
if !ok {
return fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
}
if len(b) < 2 {
return nil // End of payload, nothing left to process
}

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
}
t := b[0] // Type
l := int(b[1]) // Length
v := b[2 : 2+l] // Value (can be empty if l == 0)
remainder := b[2+l:] // Move cursor to the next TLV item
tag := strconv.Itoa(int(t))
valueField, ok := getStructField(value, tag)

// Accumulate fragments if the value length is 255
for l == 255 && len(remainder) >= 2 && remainder[0] == t {
l = int(remainder[1])
v = append(v, remainder[2:2+l]...)
remainder = remainder[2+l:]
}

if ok {
if err := unmarshalValue(v, valueField); err != nil {
return err
}
}

return nil
// Recursively process the remaining payload
return unmarshalStruct(remainder, value)
}

func unmarshalValue(v []byte, value reflect.Value) error {
Expand Down
4 changes: 2 additions & 2 deletions pkg/homekit/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ func videoToMedia(codecs []camera.VideoCodec) *core.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 {
func audioToMedia(codecs []camera.AudioCodec, direction string) *core.Media {
media := &core.Media{
Kind: core.KindAudio, Direction: core.DirectionRecvonly,
Kind: core.KindAudio, Direction: direction,
}

for _, codec := range codecs {
Expand Down
80 changes: 47 additions & 33 deletions pkg/homekit/producer.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type Client struct {

stream *camera.Stream

Width int
Height int
Bitrate int // in bits/s
}

Expand Down Expand Up @@ -102,7 +104,7 @@ func (c *Client) GetMedias() []*core.Media {

c.Medias = []*core.Media{
videoToMedia(c.videoConfig.Codecs),
audioToMedia(c.audioConfig.Codecs),
audioToMedia(c.audioConfig.Codecs, core.DirectionRecvonly),
{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Expand All @@ -116,9 +118,46 @@ func (c *Client) GetMedias() []*core.Media {
},
}

if acc.GetService(camera.TypeSpeaker) != nil {
c.Medias = append(c.Medias,
audioToMedia(c.audioConfig.Codecs, core.DirectionSendonly),
&core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{
Name: core.CodecOpus,
ClockRate: 48000,
Channels: 2,
},
},
},
)
}

return c.Medias
}

func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
switch codec.Name {
case core.CodecOpus:
sender := core.NewSender(media, track.Codec)

sender.Handler = func(packet *rtp.Packet) {
if c.audioSession != nil {
if n, err := c.audioSession.WriteRTP(packet); err == nil {
c.Send += n
}
}
}

sender.HandleRTP(track)
c.Senders = append(c.Senders, sender)
}

return nil
}

func (c *Client) Start() error {
if c.Receivers == nil {
return errors.New("producer without tracks")
Expand All @@ -138,14 +177,20 @@ func (c *Client) Start() error {
c.audioSession = &srtp.Session{Local: c.srtpEndpoint()}

var err error
c.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession, c.Bitrate)
c.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession, c.Bitrate, c.Width, c.Height)
if err != nil {
return err
}

c.srtp.AddSession(c.videoSession)
c.srtp.AddSession(c.audioSession)

c.videoSession.PayloadType = videoCodec.RTPParams[0].PayloadType
c.videoSession.RTCPInterval = toDuration(videoCodec.RTPParams[0].RTCPInterval)

c.audioSession.PayloadType = audioCodec.RTPParams[0].PayloadType
c.audioSession.RTCPInterval = toDuration(audioCodec.RTPParams[0].RTCPInterval)

deadline := time.NewTimer(core.ConnDeadline)

if videoTrack != nil {
Expand All @@ -169,10 +214,6 @@ func (c *Client) Start() error {
}
}

if c.audioSession.OnReadRTP != nil {
c.audioSession.OnReadRTP = timekeeper(c.audioSession.OnReadRTP)
}

<-deadline.C

return nil
Expand Down Expand Up @@ -226,30 +267,3 @@ func (c *Client) srtpEndpoint() *srtp.Endpoint {
SSRC: rand.Uint32(),
}
}

func timekeeper(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)
}
}