install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||
"github.com/AlexxIT/go2rtc/pkg/opus"
|
||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.Connection
|
||||
conn net.Conn
|
||||
srtp *srtp.Server
|
||||
|
||||
deadline *time.Timer
|
||||
|
||||
sessionID string
|
||||
videoSession *srtp.Session
|
||||
audioSession *srtp.Session
|
||||
audioRTPTime byte
|
||||
}
|
||||
|
||||
func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecOpus},
|
||||
},
|
||||
},
|
||||
}
|
||||
return &Consumer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "homekit",
|
||||
Protocol: "rtp",
|
||||
RemoteAddr: conn.RemoteAddr().String(),
|
||||
Medias: medias,
|
||||
Transport: conn,
|
||||
},
|
||||
conn: conn,
|
||||
srtp: server,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) SessionID() string {
|
||||
return c.sessionID
|
||||
}
|
||||
|
||||
func (c *Consumer) SetOffer(offer *camera.SetupEndpointsRequest) {
|
||||
c.sessionID = offer.SessionID
|
||||
c.videoSession = &srtp.Session{
|
||||
Remote: &srtp.Endpoint{
|
||||
Addr: offer.Address.IPAddr,
|
||||
Port: offer.Address.VideoRTPPort,
|
||||
MasterKey: []byte(offer.VideoCrypto.MasterKey),
|
||||
MasterSalt: []byte(offer.VideoCrypto.MasterSalt),
|
||||
},
|
||||
}
|
||||
c.audioSession = &srtp.Session{
|
||||
Remote: &srtp.Endpoint{
|
||||
Addr: offer.Address.IPAddr,
|
||||
Port: offer.Address.AudioRTPPort,
|
||||
MasterKey: []byte(offer.AudioCrypto.MasterKey),
|
||||
MasterSalt: []byte(offer.AudioCrypto.MasterSalt),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) GetAnswer() *camera.SetupEndpointsResponse {
|
||||
c.videoSession.Local = c.srtpEndpoint()
|
||||
c.audioSession.Local = c.srtpEndpoint()
|
||||
|
||||
return &camera.SetupEndpointsResponse{
|
||||
SessionID: c.sessionID,
|
||||
Status: camera.StreamingStatusAvailable,
|
||||
Address: camera.Address{
|
||||
IPAddr: c.videoSession.Local.Addr,
|
||||
VideoRTPPort: c.videoSession.Local.Port,
|
||||
AudioRTPPort: c.audioSession.Local.Port,
|
||||
},
|
||||
VideoCrypto: camera.SRTPCryptoSuite{
|
||||
MasterKey: string(c.videoSession.Local.MasterKey),
|
||||
MasterSalt: string(c.videoSession.Local.MasterSalt),
|
||||
},
|
||||
AudioCrypto: camera.SRTPCryptoSuite{
|
||||
MasterKey: string(c.audioSession.Local.MasterKey),
|
||||
MasterSalt: string(c.audioSession.Local.MasterSalt),
|
||||
},
|
||||
VideoSSRC: c.videoSession.Local.SSRC,
|
||||
AudioSSRC: c.audioSession.Local.SSRC,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfiguration) bool {
|
||||
if c.sessionID != conf.Control.SessionID {
|
||||
return false
|
||||
}
|
||||
|
||||
c.SDP = fmt.Sprintf("%+v\n%+v", conf.VideoCodec, conf.AudioCodec)
|
||||
|
||||
c.videoSession.Remote.SSRC = conf.VideoCodec.RTPParams[0].SSRC
|
||||
c.videoSession.PayloadType = conf.VideoCodec.RTPParams[0].PayloadType
|
||||
c.videoSession.RTCPInterval = toDuration(conf.VideoCodec.RTPParams[0].RTCPInterval)
|
||||
|
||||
c.audioSession.Remote.SSRC = conf.AudioCodec.RTPParams[0].SSRC
|
||||
c.audioSession.PayloadType = conf.AudioCodec.RTPParams[0].PayloadType
|
||||
c.audioSession.RTCPInterval = toDuration(conf.AudioCodec.RTPParams[0].RTCPInterval)
|
||||
c.audioRTPTime = conf.AudioCodec.CodecParams[0].RTPTime[0]
|
||||
|
||||
c.srtp.AddSession(c.videoSession)
|
||||
c.srtp.AddSession(c.audioSession)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
var session *srtp.Session
|
||||
if codec.Kind() == core.KindVideo {
|
||||
session = c.videoSession
|
||||
} else {
|
||||
session = c.audioSession
|
||||
}
|
||||
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
|
||||
if c.deadline == nil {
|
||||
c.deadline = time.NewTimer(time.Second * 30)
|
||||
|
||||
sender.Handler = func(packet *rtp.Packet) {
|
||||
c.deadline.Reset(core.ConnDeadline)
|
||||
if n, err := session.WriteRTP(packet); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender.Handler = func(packet *rtp.Packet) {
|
||||
if n, err := session.WriteRTP(packet); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
sender.Handler = h264.RTPPay(1378, sender.Handler)
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
}
|
||||
case core.CodecOpus:
|
||||
sender.Handler = opus.RepackToHAP(c.audioRTPTime, sender.Handler)
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) WriteTo(io.Writer) (int64, error) {
|
||||
if c.deadline != nil {
|
||||
<-c.deadline.C
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
if c.deadline != nil {
|
||||
c.deadline.Reset(0)
|
||||
}
|
||||
return c.Connection.Stop()
|
||||
}
|
||||
|
||||
func (c *Consumer) srtpEndpoint() *srtp.Endpoint {
|
||||
addr := c.conn.LocalAddr().(*net.TCPAddr)
|
||||
return &srtp.Endpoint{
|
||||
Addr: addr.IP.To4().String(),
|
||||
Port: uint16(c.srtp.Port()),
|
||||
MasterKey: []byte(core.RandString(16, 0)),
|
||||
MasterSalt: []byte(core.RandString(14, 0)),
|
||||
SSRC: rand.Uint32(),
|
||||
}
|
||||
}
|
||||
|
||||
func toDuration(seconds float32) time.Duration {
|
||||
return time.Duration(seconds * float32(time.Second))
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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.VideoCodecConfiguration) *core.Media {
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo, Direction: core.DirectionRecvonly,
|
||||
}
|
||||
|
||||
for _, codec := range codecs {
|
||||
for _, param := range codec.CodecParams {
|
||||
// get best profile and level
|
||||
profileID := core.Max(param.ProfileID)
|
||||
level := core.Max(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.AudioCodecConfiguration) *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: param.Channels,
|
||||
}
|
||||
|
||||
if mediaCodec.Name == core.CodecELD {
|
||||
// only 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.VideoCodecConfiguration, maxWidth, maxHeight int) *camera.VideoCodecConfiguration {
|
||||
profileID := video0.CodecParams[0].ProfileID[0]
|
||||
level := video0.CodecParams[0].Level[0]
|
||||
var attrs camera.VideoCodecAttributes
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range video0.VideoAttrs {
|
||||
if (maxWidth > 0 && int(s.Width) > maxWidth) || (maxHeight > 0 && int(s.Height) > maxHeight) {
|
||||
continue
|
||||
}
|
||||
if s.Width > attrs.Width || s.Height > attrs.Height {
|
||||
attrs = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &camera.VideoCodecConfiguration{
|
||||
CodecType: video0.CodecType,
|
||||
CodecParams: []camera.VideoCodecParameters{
|
||||
{
|
||||
ProfileID: []byte{profileID},
|
||||
Level: []byte{level},
|
||||
},
|
||||
},
|
||||
VideoAttrs: []camera.VideoCodecAttributes{attrs},
|
||||
}
|
||||
}
|
||||
|
||||
func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodecConfiguration) *camera.AudioCodecConfiguration {
|
||||
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.AudioCodecConfiguration{
|
||||
CodecType: codecType,
|
||||
CodecParams: []camera.AudioCodecParameters{
|
||||
{
|
||||
Channels: channels,
|
||||
SampleRate: []byte{sampleRate},
|
||||
RTPTime: []uint8{20},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Debug(v any) {
|
||||
switch v := v.(type) {
|
||||
case *http.Request:
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
if v.ContentLength != 0 {
|
||||
b, err := io.ReadAll(v.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
v.Body = io.NopCloser(bytes.NewReader(b))
|
||||
log.Printf("[homekit] request: %s %s\n%s", v.Method, v.RequestURI, b)
|
||||
} else {
|
||||
log.Printf("[homekit] request: %s %s <nobody>", v.Method, v.RequestURI)
|
||||
}
|
||||
case *http.Response:
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
if v.Header.Get("Content-Type") == "image/jpeg" {
|
||||
log.Printf("[homekit] response: %d <jpeg>", v.StatusCode)
|
||||
return
|
||||
}
|
||||
if v.ContentLength != 0 {
|
||||
b, err := io.ReadAll(v.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
v.Body = io.NopCloser(bytes.NewReader(b))
|
||||
log.Printf("[homekit] response: %s %d\n%s", v.Proto, v.StatusCode, b)
|
||||
} else {
|
||||
log.Printf("[homekit] response: %s %d <nobody>", v.Proto, v.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
// Deprecated: rename to Producer
|
||||
type Client struct {
|
||||
core.Connection
|
||||
|
||||
hap *hap.Client
|
||||
srtp *srtp.Server
|
||||
|
||||
videoConfig camera.SupportedVideoStreamConfiguration
|
||||
audioConfig camera.SupportedAudioStreamConfiguration
|
||||
|
||||
videoSession *srtp.Session
|
||||
audioSession *srtp.Session
|
||||
|
||||
stream *camera.Stream
|
||||
|
||||
MaxWidth int `json:"-"`
|
||||
MaxHeight int `json:"-"`
|
||||
Bitrate int `json:"-"` // in bits/s
|
||||
}
|
||||
|
||||
func Dial(rawURL string, server *srtp.Server) (*Client, error) {
|
||||
conn, err := hap.Dial(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "homekit",
|
||||
Protocol: "udp",
|
||||
RemoteAddr: conn.Conn.RemoteAddr().String(),
|
||||
Source: rawURL,
|
||||
Transport: conn,
|
||||
},
|
||||
hap: conn,
|
||||
srtp: server,
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) Conn() net.Conn {
|
||||
return c.hap.Conn
|
||||
}
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
acc, err := c.hap.GetFirstAccessory()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
char := acc.GetCharacter(camera.TypeSupportedVideoStreamConfiguration)
|
||||
if char == nil {
|
||||
return nil
|
||||
}
|
||||
if err = char.ReadTLV8(&c.videoConfig); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
char = acc.GetCharacter(camera.TypeSupportedAudioStreamConfiguration)
|
||||
if char == nil {
|
||||
return nil
|
||||
}
|
||||
if err = char.ReadTLV8(&c.audioConfig); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.SDP = fmt.Sprintf("%+v\n%+v", c.videoConfig, c.audioConfig)
|
||||
|
||||
c.Medias = []*core.Media{
|
||||
videoToMedia(c.videoConfig.Codecs),
|
||||
audioToMedia(c.audioConfig.Codecs),
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{
|
||||
Name: core.CodecJPEG,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
if c.Receivers == nil {
|
||||
return errors.New("producer without tracks")
|
||||
}
|
||||
|
||||
if c.Receivers[0].Codec.Name == core.CodecJPEG {
|
||||
return c.startMJPEG()
|
||||
}
|
||||
|
||||
videoTrack := c.trackByKind(core.KindVideo)
|
||||
videoCodec := trackToVideo(videoTrack, &c.videoConfig.Codecs[0], c.MaxWidth, c.MaxHeight)
|
||||
|
||||
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, c.Bitrate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.srtp.AddSession(c.videoSession)
|
||||
c.srtp.AddSession(c.audioSession)
|
||||
|
||||
deadline := time.NewTimer(core.ConnDeadline)
|
||||
|
||||
if videoTrack != nil {
|
||||
c.videoSession.OnReadRTP = func(packet *rtp.Packet) {
|
||||
deadline.Reset(core.ConnDeadline)
|
||||
videoTrack.WriteRTP(packet)
|
||||
c.Recv += len(packet.Payload)
|
||||
}
|
||||
|
||||
if audioTrack != nil {
|
||||
c.audioSession.OnReadRTP = func(packet *rtp.Packet) {
|
||||
audioTrack.WriteRTP(packet)
|
||||
c.Recv += len(packet.Payload)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.audioSession.OnReadRTP = func(packet *rtp.Packet) {
|
||||
deadline.Reset(core.ConnDeadline)
|
||||
audioTrack.WriteRTP(packet)
|
||||
c.Recv += len(packet.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
if c.audioSession.OnReadRTP != nil {
|
||||
c.audioSession.OnReadRTP = timekeeper(c.audioSession.OnReadRTP)
|
||||
}
|
||||
|
||||
<-deadline.C
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
if c.videoSession != nil && c.videoSession.Remote != nil {
|
||||
c.srtp.DelSession(c.videoSession)
|
||||
}
|
||||
if c.audioSession != nil && c.audioSession.Remote != nil {
|
||||
c.srtp.DelSession(c.audioSession)
|
||||
}
|
||||
|
||||
return c.Connection.Stop()
|
||||
}
|
||||
|
||||
func (c *Client) trackByKind(kind string) *core.Receiver {
|
||||
for _, receiver := range c.Receivers {
|
||||
if receiver.Codec.Kind() == kind {
|
||||
return receiver
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) startMJPEG() error {
|
||||
receiver := c.Receivers[0]
|
||||
|
||||
for {
|
||||
b, err := c.hap.GetImage(1920, 1080)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Recv += len(b)
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||
Payload: b,
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/hds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||
)
|
||||
|
||||
type ServerProxy interface {
|
||||
ServerPair
|
||||
AddConn(conn any)
|
||||
DelConn(conn any)
|
||||
}
|
||||
|
||||
func ProxyHandler(srv ServerProxy, acc net.Conn) HandlerFunc {
|
||||
return func(con net.Conn) error {
|
||||
defer con.Close()
|
||||
|
||||
pr := &Proxy{
|
||||
con: con.(*hap.Conn),
|
||||
acc: acc.(*hap.Conn),
|
||||
res: make(chan *http.Response),
|
||||
}
|
||||
|
||||
// accessory (ex. Camera) => controller (ex. iPhone)
|
||||
go pr.handleAcc()
|
||||
|
||||
// controller => accessory
|
||||
return pr.handleCon(srv)
|
||||
}
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
con *hap.Conn
|
||||
acc *hap.Conn
|
||||
res chan *http.Response
|
||||
}
|
||||
|
||||
func (p *Proxy) handleCon(srv ServerProxy) error {
|
||||
var hdsCharIID uint64
|
||||
|
||||
rd := bufio.NewReader(p.con)
|
||||
for {
|
||||
req, err := http.ReadRequest(rd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var hdsConSalt string
|
||||
|
||||
switch {
|
||||
case req.Method == "POST" && req.URL.Path == hap.PathPairings:
|
||||
var res *http.Response
|
||||
if res, err = handlePairings(req, srv); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = res.Write(p.con); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
case req.Method == "PUT" && req.URL.Path == hap.PathCharacteristics && hdsCharIID != 0:
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
var v hap.JSONCharacters
|
||||
_ = json.Unmarshal(body, &v)
|
||||
for _, char := range v.Value {
|
||||
if char.IID == hdsCharIID {
|
||||
var hdsReq camera.SetupDataStreamTransportRequest
|
||||
_ = tlv8.UnmarshalBase64(char.Value, &hdsReq)
|
||||
hdsConSalt = hdsReq.ControllerKeySalt
|
||||
break
|
||||
}
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
}
|
||||
|
||||
if err = req.Write(p.acc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res := <-p.res
|
||||
|
||||
switch {
|
||||
case req.Method == "GET" && req.URL.Path == hap.PathAccessories:
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
var v hap.JSONAccessories
|
||||
if err = json.Unmarshal(body, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, acc := range v.Value {
|
||||
if char := acc.GetCharacter(camera.TypeSetupDataStreamTransport); char != nil {
|
||||
hdsCharIID = char.IID
|
||||
}
|
||||
break
|
||||
}
|
||||
res.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
case hdsConSalt != "":
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
var v hap.JSONCharacters
|
||||
_ = json.Unmarshal(body, &v)
|
||||
for i, char := range v.Value {
|
||||
if char.IID == hdsCharIID {
|
||||
var hdsRes camera.SetupDataStreamTransportResponse
|
||||
_ = tlv8.UnmarshalBase64(char.Value, &hdsRes)
|
||||
|
||||
hdsAccSalt := hdsRes.AccessoryKeySalt
|
||||
hdsPort := int(hdsRes.TransportTypeSessionParameters.TCPListeningPort)
|
||||
|
||||
// swtich accPort to conPort
|
||||
hdsPort, err = p.listenHDS(srv, hdsPort, hdsConSalt+hdsAccSalt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hdsRes.TransportTypeSessionParameters.TCPListeningPort = uint16(hdsPort)
|
||||
if v.Value[i].Value, err = tlv8.MarshalBase64(hdsRes); err != nil {
|
||||
return err
|
||||
}
|
||||
body, _ = json.Marshal(v)
|
||||
res.ContentLength = int64(len(body))
|
||||
break
|
||||
}
|
||||
}
|
||||
res.Body = io.NopCloser(bytes.NewReader(body))
|
||||
}
|
||||
|
||||
if err = res.Write(p.con); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) handleAcc() error {
|
||||
rd := bufio.NewReader(p.acc)
|
||||
for {
|
||||
res, err := hap.ReadResponse(rd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Proto == hap.ProtoEvent {
|
||||
if err = hap.WriteEvent(p.con, res); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// important to read body before next read response
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
p.res <- res
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) listenHDS(srv ServerProxy, accPort int, salt string) (int, error) {
|
||||
// The TCP port range for HDS must be >= 32768.
|
||||
ln, err := net.ListenTCP("tcp", nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer ln.Close()
|
||||
|
||||
_ = ln.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
// raw controller conn
|
||||
conn1, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer conn1.Close()
|
||||
|
||||
// secured controller conn (controlle=false because we are accessory)
|
||||
con, err := hds.NewConn(conn1, p.con.SharedKey, salt, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
srv.AddConn(con)
|
||||
defer srv.DelConn(con)
|
||||
|
||||
accIP := p.acc.RemoteAddr().(*net.TCPAddr).IP
|
||||
|
||||
// raw accessory conn
|
||||
conn2, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: accIP, Port: accPort})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn2.Close()
|
||||
|
||||
// secured accessory conn (controller=true because we are controller)
|
||||
acc, err := hds.NewConn(conn2, p.acc.SharedKey, salt, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go io.Copy(con, acc)
|
||||
_, _ = io.Copy(acc, con)
|
||||
}()
|
||||
|
||||
conPort := ln.Addr().(*net.TCPAddr).Port
|
||||
return conPort, nil
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||
)
|
||||
|
||||
type HandlerFunc func(net.Conn) error
|
||||
|
||||
type Server interface {
|
||||
ServerPair
|
||||
ServerAccessory
|
||||
}
|
||||
|
||||
type ServerPair interface {
|
||||
GetPair(id string) []byte
|
||||
AddPair(id string, public []byte, permissions byte)
|
||||
DelPair(id string)
|
||||
}
|
||||
|
||||
type ServerAccessory interface {
|
||||
GetAccessories(conn net.Conn) []*hap.Accessory
|
||||
GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any
|
||||
SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any)
|
||||
GetImage(conn net.Conn, width, height int) []byte
|
||||
}
|
||||
|
||||
func ServerHandler(server Server) HandlerFunc {
|
||||
return handleRequest(func(conn net.Conn, req *http.Request) (*http.Response, error) {
|
||||
switch req.URL.Path {
|
||||
case hap.PathPairings:
|
||||
return handlePairings(req, server)
|
||||
|
||||
case hap.PathAccessories:
|
||||
body := hap.JSONAccessories{Value: server.GetAccessories(conn)}
|
||||
return makeResponse(hap.MimeJSON, body)
|
||||
|
||||
case hap.PathCharacteristics:
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
var v hap.JSONCharacters
|
||||
|
||||
id := req.URL.Query().Get("id")
|
||||
for _, id = range strings.Split(id, ",") {
|
||||
s1, s2, _ := strings.Cut(id, ".")
|
||||
aid, _ := strconv.Atoi(s1)
|
||||
iid, _ := strconv.ParseUint(s2, 10, 64)
|
||||
val := server.GetCharacteristic(conn, uint8(aid), iid)
|
||||
|
||||
v.Value = append(v.Value, hap.JSONCharacter{AID: uint8(aid), IID: iid, Value: val})
|
||||
}
|
||||
|
||||
return makeResponse(hap.MimeJSON, v)
|
||||
|
||||
case "PUT":
|
||||
var v struct {
|
||||
Value []struct {
|
||||
AID uint8 `json:"aid"`
|
||||
IID uint64 `json:"iid"`
|
||||
Value any `json:"value"`
|
||||
} `json:"characteristics"`
|
||||
}
|
||||
if err := json.NewDecoder(req.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, char := range v.Value {
|
||||
server.SetCharacteristic(conn, char.AID, char.IID, char.Value)
|
||||
}
|
||||
|
||||
res := &http.Response{
|
||||
StatusCode: http.StatusNoContent,
|
||||
Proto: "HTTP",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
case hap.PathResource:
|
||||
var v struct {
|
||||
Width int `json:"image-width"`
|
||||
Height int `json:"image-height"`
|
||||
Type string `json:"resource-type"`
|
||||
}
|
||||
if err := json.NewDecoder(req.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body := server.GetImage(conn, v.Width, v.Height)
|
||||
return makeResponse("image/jpeg", body)
|
||||
}
|
||||
|
||||
return nil, errors.New("hap: unsupported path: " + req.RequestURI)
|
||||
})
|
||||
}
|
||||
|
||||
func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) HandlerFunc {
|
||||
return func(conn net.Conn) error {
|
||||
rw := bufio.NewReaderSize(conn, 16*1024)
|
||||
wr := bufio.NewWriterSize(conn, 16*1024)
|
||||
for {
|
||||
req, err := http.ReadRequest(rw)
|
||||
//debug(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := handle(conn, req)
|
||||
//debug(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = res.Write(wr); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = wr.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlePairings(req *http.Request, srv ServerPair) (*http.Response, error) {
|
||||
cmd := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
State byte `tlv8:"6"`
|
||||
Permissions byte `tlv8:"11"`
|
||||
}{}
|
||||
|
||||
if err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch cmd.Method {
|
||||
case 3: // add
|
||||
srv.AddPair(cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions)
|
||||
case 4: // delete
|
||||
srv.DelPair(cmd.Identifier)
|
||||
}
|
||||
|
||||
body := struct {
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
State: hap.StateM2,
|
||||
}
|
||||
|
||||
return makeResponse(hap.MimeTLV8, body)
|
||||
}
|
||||
|
||||
func makeResponse(mime string, v any) (*http.Response, error) {
|
||||
var body []byte
|
||||
var err error
|
||||
|
||||
switch mime {
|
||||
case hap.MimeJSON:
|
||||
body, err = json.Marshal(v)
|
||||
case hap.MimeTLV8:
|
||||
body, err = tlv8.Marshal(v)
|
||||
case "image/jpeg":
|
||||
body = v.([]byte)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Proto: "HTTP",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{mime},
|
||||
"Content-Length": []string{strconv.Itoa(len(body))},
|
||||
},
|
||||
ContentLength: int64(len(body)),
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
Reference in New Issue
Block a user