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