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

Conversation

los93sol
Copy link

@los93sol los93sol commented Nov 2, 2024

Fixes #1295

This adds the ability to discover the Abode IOTA and pair it over HomeKit. It also fixes the TLV8 parsing that would make this particular camera fail to playback due to it producing 0xff, 0x00 in between it's TLV's. I updated the TLV8 parsing to handle scenarios like this generically because in this case 0xff is type 255 with length 0 so you can just handle them like normal 0 length TLV's and move on.

@AlexxIT AlexxIT self-assigned this Nov 2, 2024
@AlexxIT
Copy link
Owner

AlexxIT commented Nov 2, 2024

I'm not sure your TLV changes don't break anything.

@los93sol
Copy link
Author

los93sol commented Nov 2, 2024

I’m not either lol but I did run through pairing which has TLVs larger than 255 bytes and that worked fine and I’m pretty familiar with HAP as I originally patched the PDU fragmentation issue that prevented certain BLE devices from functioning properly with the lib so I am fairly confident this change is solid.

@AlexxIT
Copy link
Owner

AlexxIT commented Nov 2, 2024

Oh, great. I wrote that code completely blind. Just on a guess as to how it should work.

@los93sol
Copy link
Author

los93sol commented Nov 5, 2024

@AlexxIT I'm looking more into the HomeKit side and wanted to get your thoughts on some things.

My camera has two instances of the CameraRTPStreamManagement service and there actually is a difference in the video and audio formats offered on them. My thought is it would be nice to be able to specify the iid in the configuration to force it to use the specific service as their streaming statuses do appear to accurately reflect which one is in use.

The other thing I was looking at is allowing the user to specify the max width and max height to try to cap out the highest negotiated resolution. The idea with this would be for use with Frigate where I may want one stream to be lower resolution for detect and a higher resolution for record.

The last thing I was looking at is two way audio to the homekit device. It looks like there's a handful of scenarios to differentiate around it

  1. Proxying a HomeKit device to HomeKit. I tested this and it worked fine with Apple Home.
  2. Proxying any other device to HomeKit. I have not tested this, but I do have some onvif cameras that I have two way audio working with already so I am able to test it.
  3. Two-way audio between go2rtc, frigate, home assistant, etc. to the HomeKit device. I found that frigate's UI is looking specifically for recvonly to display the speaker button to listen to the camera, and sendonly to display the microphone to at least start the enablement of two way audio. I also am able to conditionally add the sendonly media based on the presence of the speaker service on the camera. I'm a bit confused here though as it seems like the direction should be DirectionSendRecv in this case as the audio codec is the same in both directions so trying to understand if frigate's logic should account for DirectionSendRecv as well or if I should actually add the same media twice.

@seydx
Copy link

seydx commented Nov 5, 2024

@los93sol

I'm also interested in two-way audio. I haven't tested it yet, but do native homekit cameras with two-way audio proxied from go2rtc to homekit still have a working talkback button?

In the case of non-native homekit cameras with two-way audio, like a tapo camera, there is no talkback button, right?

@AlexxIT
Copy link
Owner

AlexxIT commented Nov 6, 2024

@los93sol

  1. I have not seen any cameras that have a different CameraRTPStreamManagement. Can you send me the response from the camera? Preferably in raw JSON format.
  2. Resolution is a nice feature. I've already added a customizable bitrate. Resolution can be added, it's not complicated.
  3. Two way audio from and to HomeKit is not supported yet. I just didn't have time to implement it.

@los93sol
Copy link
Author

los93sol commented Nov 7, 2024

@los93sol

  1. I have not seen any cameras that have a different CameraRTPStreamManagement. Can you send me the response from the camera? Preferably in raw JSON format.
  2. Resolution is a nice feature. I've already added a customizable bitrate. Resolution can be added, it's not complicated.
  3. Two way audio from and to HomeKit is not supported yet. I just didn't have time to implement it.

I’ll try to get that payload for you.

in the meantime I’ve been trying to get two way audio going and I am hitting some walls that I’m not sure how to properly address.

The scenario I’m targeting is getting it to work from webrtc to the HomeKit device. The problem is the protocol does not even support opus/48000/2 and the non standard aac won’t work either. The best I’ve been able to do is add opus/48000/2 to the producer manually and get the pipelining to actually route the audio packets to me, but then I need to transcode and the only codecs I think can actually be supported are opus formats unless you’ve compiled ffmpeg with fdk_aac. The other route i tried was to just add support for opus/16000/1 (what my camera supports) to the webrtc consumer and I was able to get the pipelining setup correctly that way, but haven’t been able to get the rtp packets to actually land in the sender’s handler. It seems like from a compatibility position that what I really need is a layer between similar to how ffmpeg can be used to convert audio from the device to the browser in a supported format, but in this case it needs to go the other way, from browser to the device. Curious what your thoughts are on the best way to handle this.

@AlexxIT
Copy link
Owner

AlexxIT commented Nov 7, 2024

Apple uses non standard OPUS format. You can't use it anywhere without fixing.

@los93sol
Copy link
Author

los93sol commented Nov 7, 2024

Ah, I thought it was just the aac format that was non-standard.

@AlexxIT
Copy link
Owner

AlexxIT commented Nov 7, 2024

@los93sol
Copy link
Author

los93sol commented Nov 7, 2024

Here's the json payload you wanted, this thing has a ton of accessories connected so I cleaned those out for brevity. If you want to see those as well then just lmk and I can send them

{
  "accessories":
  [
    {
      "aid":1,
      "services":
      [
        {
          "iid":1, "type":"3E", "primary":false, "hidden":false, "linked":[],
          "characteristics":
          [
            { "iid":2, "type":"14", "format":"bool", "perms":["pw"] },
            { "iid":3, "type":"20", "format":"string", "value":"abode", "perms":["pr"], "ev":false },
            { "iid":4, "type":"21", "format":"string", "value":"Bridge", "perms":["pr"], "ev":false },
            { "iid":5, "type":"23", "format":"string", "value":"IOTA-[REDACTED]", "perms":["pr"], "ev":false },
            { "iid":6, "type":"30", "format":"string", "value":"[REDACTED]", "perms":["pr"], "ev":false },
            { "iid":7, "type":"52", "format":"string", "value":"2.0.20", "perms": ["pr"], "ev":false },
            { "iid":8, "type":"53", "format":"string", "value":"1.0", "perms": ["pr"], "ev":false },
            { "iid":9, "type":"34AB8811-AC7F-4340-BAC3-FD6A85F9943B", "format":"string", "value":"2.0.1;16A75", "perms": ["pr","hd"], "ev":false }
          ]
        },
        {
          "iid":16, "type":"A2", "primary":false, "hidden":false, "linked": [],
          "characteristics":
          [
            { "iid":18, "type":"37", "format":"string", "value":"1.1.0", "perms":["pr"], "ev":false }
          ]
        },
        {
          "iid":816, "type":"110", "primary":false, "hidden":false, "linked":[],
          "characteristics":
          [
            { "iid":819, "type":"120", "format":"tlv8", "value":"AQEA", "perms":["pr","ev"], "ev":false },
            { "iid":820, "type":"114", "format":"tlv8", "value":"AaUBAQACDAEBAQIBAgMBAAQBAAMLAQKABwICOAQDAR7/AAMLAQIABQIC0AIDAR7/AAMLAQKAAgICaAEDAR7/AAMLAQLgAQICDgEDAR7/AAMLAQJAAQICtAADAR7/AAMLAQIABQICwAMDAR7/AAMLAQIABAICAAMDAR7/AAMLAQKAAgIC4AEDAR7/AAMLAQLgAQICaAEDAR7/AAMLAQJAAQIC8AADAR4=", "perms":["pr"], "ev":false },
            { "iid":821, "type":"115", "format":"tlv8", "value":"AQ4BAQMCCQEBAQIBAAMBAQIBAA==", "perms":["pr"], "ev":false },
            { "iid":822, "type":"116", "format":"tlv8", "value":"AgEA/wACAQH/AAIBAg==", "perms":["pr"], "ev":false },
            { "iid":823, "type":"118", "format":"tlv8", "value":"", "perms":["pr","pw"], "ev":false },
            { "iid":824, "type":"117", "format":"tlv8", "value":"", "perms":["pr","pw"], "ev":false }
          ]
        },
        { "iid":832, "type":"110", "primary":false, "hidden":false, "linked":[],
          "characteristics":
          [
            { "iid":835, "type":"120", "format":"tlv8", "value":"AQEA", "perms":["pr","ev"], "ev":false },
            { "iid":836, "type":"114", "format":"tlv8", "value":"AYcBAQACDAEBAQIBAgMBAAQBAAMLAQIABQIC0AIDAR7/AAMLAQKAAgICaAEDAR7/AAMLAQLgAQICDgEDAR7/AAMLAQJAAQICtAADAR7/AAMLAQIABAICAAMDAR7/AAMLAQKAAgIC4AEDAR7/AAMLAQLgAQICaAEDAR7/AAMLAQJAAQIC8AADAR4=", "perms":["pr"], "ev":false },
            { "iid":837, "type":"115", "format":"tlv8", "value":"AQ4BAQMCCQEBAQIBAAMBAQIBAA==", "perms":["pr"], "ev":false },
            { "iid":838, "type":"116", "format":"tlv8", "value":"AgEA/wACAQH/AAIBAg==", "perms":["pr"], "ev":false },
            { "iid":839, "type":"118", "format":"tlv8", "value":"", "perms":["pr","pw"], "ev":false },
            { "iid":840, "type":"117", "format":"tlv8", "value":"", "perms":["pr","pw"], "ev":false }
          ]
        },
        { "iid":848, "type":"112", "primary":false, "hidden":false, "linked":[],
          "characteristics":
          [
            { "iid":850, "type":"23", "format":"string", "value":"Microphone", "perms":["pr"], "ev":false },
            { "iid":851, "type":"11A", "format":"bool", "value":0, "perms":["pr","pw","ev"], "ev":false },
            { "iid":852, "type":"119", "format":"uint8", "value":50, "perms":["pr","pw","ev"], "ev":false, "unit":"percentage", "minValue":0, "maxValue":100, "minStep":1 }
          ]
        },
        {
          "iid":864, "type":"113", "primary":false, "hidden":false, "linked":[],
          "characteristics":
          [
            { "iid":866, "type":"23", "format":"string", "value":"Speaker", "perms":["pr"], "ev":false },
            { "iid":867, "type":"11A", "format":"bool", "value":0, "perms":["pr","pw","ev"], "ev":false },
            { "iid":868, "type":"119", "format":"uint8", "value":50, "perms":["pr","pw","ev"], "ev":false, "unit":"percentage", "minValue":0, "maxValue":100, "minStep":1 }
          ]
        },
        {
          "iid":512, "type":"7E", "primary":true, "hidden":false, "linked":[],
          "characteristics":
          [
            { "iid":513, "type":"66", "format":"uint8", "value":3, "perms":["pr","ev"], "ev":false, "minValue":0, "maxValue":4, "minStep":1 },
            { "iid":514, "type":"67", "format":"uint8", "value":3, "perms":["pr","pw","ev"], "ev":false, "minValue":0, "maxValue":3, "minStep":1 }
          ]
        },
        {
          "iid":353, "type":"85", "primary":false, "hidden":false, "linked":[],
          "characteristics":
          [
            { "iid":354, "type":"22", "format":"bool", "value":0, "perms":["pr","ev"], "ev":false }
          ]
        }
      ]
    }
  ]
}	

@los93sol
Copy link
Author

los93sol commented Nov 8, 2024

I have two way audio working now from webrtc to the camera. The way I did this is to just slip opus/48000/2 as a sendonly in the producer and implemented AddTrack in the producer so it will match up and basically just slam the packet as is at the audioSession. The only thing I did have to do is set the PayloadType to 110 and swap the SSRC on the packet with the one that we told HomeKit about when exchanging endpoints. I didn't see it until I did a packet capture, but despite setting the PayloadType and SSRC in the handler, they were not set on the wire because inside WriteRTP there's....

	clone := rtp.Packet{
		Header: rtp.Header{
			Version:        2,
			Marker:         packet.Marker,
			PayloadType:    s.PayloadType,
			SequenceNumber: packet.SequenceNumber,
			Timestamp:      packet.Timestamp,
			SSRC:           s.Local.SSRC,
		},
		Payload: packet.Payload,
	}

I changed that to just copy it off the packet, but my question really is were those taken off the session intentionally or was that just not a concern previously?

@los93sol los93sol changed the title Adds support for Abode IOTA via HomeKit Adds two way audio and support for Abode IOTA via HomeKit Nov 8, 2024
@AlexxIT
Copy link
Owner

AlexxIT commented Nov 9, 2024

I don't understand what part of the code you are referring to.

@@ -83,10 +83,10 @@ func (s *Session) WriteRTP(packet *rtp.Packet) (int, error) {
Header: rtp.Header{
Version: 2,
Marker: packet.Marker,
PayloadType: s.PayloadType,
PayloadType: packet.PayloadType,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line and a few lines down is what was preventing me from changing the payload type and ssrc on the rtp packets

@AlexxIT
Copy link
Owner

AlexxIT commented Nov 10, 2024

We set these values when we set up the connection. I don't see why they need to be changed.

func (c *Consumer) GetAnswer() *camera.SetupEndpoints {
c.videoSession.Local = c.srtpEndpoint()
c.audioSession.Local = c.srtpEndpoint()
return &camera.SetupEndpoints{
SessionID: c.sessionID,
Status: []byte{0},
Address: camera.Addr{
IPAddr: c.videoSession.Local.Addr,
VideoRTPPort: c.videoSession.Local.Port,
AudioRTPPort: c.audioSession.Local.Port,
},
VideoCrypto: camera.CryptoSuite{
MasterKey: string(c.videoSession.Local.MasterKey),
MasterSalt: string(c.videoSession.Local.MasterSalt),
},
AudioCrypto: camera.CryptoSuite{
MasterKey: string(c.audioSession.Local.MasterKey),
MasterSalt: string(c.audioSession.Local.MasterSalt),
},
VideoSSRC: []uint32{c.videoSession.Local.SSRC},
AudioSSRC: []uint32{c.audioSession.Local.SSRC},
}
}
func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfig) 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
}

@los93sol
Copy link
Author

los93sol commented Nov 10, 2024

Ah, it's because the HomeKit producer doesn't call that, from what I see that's only in play when your running the HomeKit module to proxy a device. I'll take a closer look and see if I can bring the producer inline.

Does this method look better?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Info] go2rtc works with the security video cameras from Abode, which utilize an AWS Kinesis stream.
3 participants