Skip to content

Commit

Permalink
first steps of camera support
Browse files Browse the repository at this point in the history
  • Loading branch information
wiomoc committed Jan 17, 2020
1 parent c7d1740 commit b5a862e
Show file tree
Hide file tree
Showing 17 changed files with 944 additions and 12 deletions.
99 changes: 99 additions & 0 deletions democameraserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from homekit import AccessoryServer
from homekit.model import CameraAccessory, ManagedRTPStreamService, MicrophoneService
from homekit.model.characteristics.rtp_stream.setup_endpoints import Address, IPVersion
from homekit.model.characteristics.rtp_stream.supported_audio_stream_configuration import \
SupportedAudioStreamConfiguration, AudioCodecConfiguration, AudioCodecType, AudioCodecParameters, BitRate, \
SampleRate
from homekit.model.characteristics.rtp_stream.supported_rtp_configuration import SupportedRTPConfiguration, \
CameraSRTPCryptoSuite
from homekit.model.characteristics.rtp_stream.supported_video_stream_configuration import \
SupportedVideoStreamConfiguration, VideoCodecConfiguration, VideoCodecParameters, H264Profile, H264Level, \
VideoAttributes

if __name__ == '__main__':
try:
httpd = AccessoryServer('demoserver.json')

accessory = CameraAccessory('Testkamera', 'wiomoc', 'Demoserver', '0001', '0.1')


# accessory.set_get_image_snapshot_callback(
# lambda f: open('cam-preview.jpg', 'rb').read())

class StreamHandler:
def __init__(self, controller_address, srtp_params_video):
self.srtp_params_video = srtp_params_video
self.controller_address = controller_address
self.ffmpeg_process = None

def on_start(self, attrs):
import subprocess
import base64

self.ffmpeg_process = subprocess.Popen(
['ffmpeg', '-re',
'-f', 'avfoundation',
'-r', '30.000030', '-i', 'FaceTime HD-Kamera (integriert)', '-threads', '0',
'-vcodec', 'libx264', '-an', '-pix_fmt', 'yuv420p',
'-r', str(attrs.attributes.frame_rate),
'-f', 'rawvideo', '-tune', 'zerolatency', '-vf',
f'scale={attrs.attributes.width}:{attrs.attributes.height}',
'-b:v', '300k', '-bufsize', '300k',
'-payload_type', '99', '-ssrc', '32', '-f', 'rtp',
'-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params', base64.b64encode(
self.srtp_params_video.master_key + self.srtp_params_video.master_salt).decode('ascii'),
f'srtp://{self.controller_address.ip_address}:{self.controller_address.video_rtp_port}'
f'?rtcpport={self.controller_address.video_rtp_port}&localrtcpport={self.controller_address.video_rtp_port}'
'&pkt_size=1378'
])

def on_end(self):
if self.ffmpeg_process is not None:
self.ffmpeg_process.terminate()

def get_ssrc(self):
return (32, 32)

def get_address(self):
return Address(IPVersion.IPV4, httpd.data.ip, self.controller_address.video_rtp_port,
self.controller_address.audio_rtp_port)


stream_service = ManagedRTPStreamService(
StreamHandler,
SupportedRTPConfiguration(
[
CameraSRTPCryptoSuite.AES_CM_128_HMAC_SHA1_80,
]),
SupportedVideoStreamConfiguration(
VideoCodecConfiguration(
VideoCodecParameters(
[H264Profile.CONSTRAINED_BASELINE_PROFILE, H264Profile.MAIN_PROFILE, H264Profile.HIGH_PROFILE],
[H264Level.L_3_1, H264Level.L_3_2, H264Level.L_4]
), [
VideoAttributes(1920, 1080, 30),
VideoAttributes(320, 240, 15),
VideoAttributes(1280, 960, 30),
VideoAttributes(1280, 720, 30),
VideoAttributes(1280, 768, 30),
VideoAttributes(640, 480, 30),
VideoAttributes(640, 360, 30)
])),
SupportedAudioStreamConfiguration([
AudioCodecConfiguration(AudioCodecType.OPUS,
AudioCodecParameters(1, BitRate.VARIABLE, SampleRate.KHZ_24)),
AudioCodecConfiguration(AudioCodecType.AAC_ELD,
AudioCodecParameters(1, BitRate.VARIABLE, SampleRate.KHZ_16))
], 0))
accessory.services.append(stream_service)
microphone_service = MicrophoneService()
accessory.services.append(microphone_service)
httpd.accessories.add_accessory(accessory)

httpd.publish_device()
print('published device and start serving')
httpd.serve_forever()
except KeyboardInterrupt:
print('unpublish device')
httpd.unpublish_device()
26 changes: 25 additions & 1 deletion homekit/accessoryserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from homekit.exceptions import ConfigurationError, ConfigLoadingError, ConfigSavingError, FormatError, \
CharacteristicPermissionError, DisconnectedControllerError
from homekit.http_impl import HttpStatusCodes
from homekit.model import Accessories, Categories
from homekit.model import Accessories, Categories, CameraAccessory
from homekit.model.characteristics import CharacteristicsTypes
from homekit.protocol import TLV
from homekit.protocol.statuscodes import HapStatusCodes
Expand Down Expand Up @@ -316,6 +316,9 @@ def __init__(self, request, client_address, server):
},
'/pairings': {
'POST': self._post_pairings
},
'/resource': {
'POST': self._post_resource
}
}
self.protocol_version = 'HTTP/1.1'
Expand Down Expand Up @@ -861,6 +864,27 @@ def _post_pair_verify(self):

self.send_error(HttpStatusCodes.METHOD_NOT_ALLOWED)

def _post_resource(self):
format = json.loads(self.body)
accessories = self.server.accessories.accessories

if 'aid' in format:
aid = format['aid']
accessories = [accessory for accessory in accessories if accessory.aid == aid]

if len(accessories) != 0 and isinstance(accessories[0], CameraAccessory) and \
accessories[0].get_image_snapshot_callback is not None:
accessory = accessories[0]
image = accessory.get_image_snapshot_callback(format)

self.send_response(HttpStatusCodes.OK)
self.send_header('Content-Type', 'image/jpeg')
self.send_header('Content-Length', len(image))
self.end_headers()
self.wfile.write(image)
else:
self.send_error(HttpStatusCodes.NOT_FOUND)

def _post_pairings(self):
d_req = TLV.decode_bytes(self.body)

Expand Down
18 changes: 15 additions & 3 deletions homekit/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
#

__all__ = [
'AccessoryInformationService', 'BHSLightBulbService', 'FanService', 'LightBulbService', 'ThermostatService',
'Categories', 'CharacteristicPermissions', 'CharacteristicFormats', 'FeatureFlags', 'Accessory'
'AccessoryInformationService', 'BHSLightBulbService', 'RTPStreamService', 'ManagedRTPStreamService', 'FanService',
'LightBulbService', 'ThermostatService', 'MicrophoneService', 'Categories', 'CharacteristicPermissions',
'CharacteristicFormats', 'FeatureFlags', 'Accessory', 'CameraAccessory'
]

import json
from homekit.model.mixin import ToDictMixin, get_id
from homekit.model.services import AccessoryInformationService, LightBulbService, FanService, \
BHSLightBulbService, ThermostatService
BHSLightBulbService, ThermostatService, RTPStreamService, ManagedRTPStreamService, MicrophoneService
from homekit.model.categories import Categories
from homekit.model.characteristics import CharacteristicPermissions, CharacteristicFormats
from homekit.model.feature_flags import FeatureFlags
Expand Down Expand Up @@ -66,6 +67,17 @@ def to_accessory_and_service_list(self):
return d


# def __init__(self, session_id, ):

class CameraAccessory(Accessory):
def __init__(self, name, manufacturer, model, serial_number, firmware_revision):
super().__init__(name, manufacturer, model, serial_number, firmware_revision)
self.get_image_snapshot_callback = None

def set_get_image_snapshot_callback(self, callback):
self.get_image_snapshot_callback = callback


class Accessories(ToDictMixin):
def __init__(self):
self.accessories = []
Expand Down
5 changes: 4 additions & 1 deletion homekit/model/characteristics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
'SaturationCharacteristicMixin', 'SerialNumberCharacteristic', 'TargetHeatingCoolingStateCharacteristic',
'TargetHeatingCoolingStateCharacteristicMixin', 'TargetTemperatureCharacteristic',
'TargetTemperatureCharacteristicMixin', 'TemperatureDisplayUnitCharacteristic', 'TemperatureDisplayUnitsMixin',
'VolumeCharacteristic', 'VolumeCharacteristicMixin'
'VolumeCharacteristic', 'VolumeCharacteristicMixin', 'MicrophoneMuteCharacteristicMixin',
'MicrophoneMuteCharacteristic'
]

from homekit.model.characteristics.characteristic_permissions import CharacteristicPermissions
Expand Down Expand Up @@ -59,3 +60,5 @@
from homekit.model.characteristics.temperature_display_unit import TemperatureDisplayUnitsMixin, \
TemperatureDisplayUnitCharacteristic
from homekit.model.characteristics.volume import VolumeCharacteristic, VolumeCharacteristicMixin
from homekit.model.characteristics.microphone_mute import MicrophoneMuteCharacteristicMixin, \
MicrophoneMuteCharacteristic
25 changes: 20 additions & 5 deletions homekit/model/characteristics/abstract_characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@
from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions
from homekit.protocol.statuscodes import HapStatusCodes
from homekit.exceptions import CharacteristicPermissionError, FormatError
from homekit.protocol.tlv import TLVItem


class AbstractCharacteristic(ToDictMixin):
def __init__(self, iid: int, characteristic_type: str, characteristic_format: str):
def __init__(self, iid: int, characteristic_type: str, characteristic_format: str, characteristic_tlv_type=None):
if type(self) is AbstractCharacteristic:
raise Exception('AbstractCharacteristic is an abstract class and cannot be instantiated directly')
self.type = CharacteristicsTypes.get_uuid(characteristic_type) # page 65, see ServicesTypes
Expand All @@ -47,6 +48,8 @@ def __init__(self, iid: int, characteristic_type: str, characteristic_format: st
self.valid_values = None # array, not required, see page 67, all numeric entries are allowed values
self.valid_values_range = None # 2 element array, not required, see page 67

self.tlv_type = characteristic_tlv_type

self._set_value_callback = None
self._get_value_callback = None

Expand Down Expand Up @@ -118,6 +121,9 @@ def set_value(self, new_val):
if len(new_val) > self.maxLen:
raise FormatError(HapStatusCodes.INVALID_VALUE)

if self.format == CharacteristicFormats.tlv8 and new_val is not None:
new_val = TLVItem.decode(self.tlv_type, base64.decodebytes(new_val.encode()))

self.value = new_val
if self._set_value_callback:
self._set_value_callback(new_val)
Expand Down Expand Up @@ -155,9 +161,15 @@ def get_value(self):
"""
if CharacteristicPermissions.paired_read not in self.perms:
raise CharacteristicPermissionError(HapStatusCodes.CANT_READ_WRITE_ONLY)
if self._get_value_callback:
return self._get_value_callback()
return self.value

value = self.value
if self._get_value_callback is not None:
value = self._get_value_callback()

if self.value is not None and self.format == CharacteristicFormats.tlv8:
return base64.b64encode(TLVItem.encode(value)).decode("ascii")
else:
return value

def get_value_for_ble(self):
value = self.get_value()
Expand Down Expand Up @@ -200,7 +212,10 @@ def to_accessory_and_service_list(self):
'format': self.format,
}
if CharacteristicPermissions.paired_read in self.perms:
d['value'] = self.value
if self.value is not None and self.format == CharacteristicFormats.tlv8:
d['value'] = base64.b64encode(TLVItem.encode(self.value)).decode("ascii")
else:
d['value'] = self.value
if self.ev:
d['ev'] = self.ev
if self.description:
Expand Down
43 changes: 43 additions & 0 deletions homekit/model/characteristics/microphone_mute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#
# Copyright 2018 Joachim Lusiardi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions, \
AbstractCharacteristic


class MicrophoneMuteCharacteristic(AbstractCharacteristic):
"""
Defined on page 157
"""

def __init__(self, iid):
AbstractCharacteristic.__init__(self, iid, CharacteristicsTypes.MUTE, CharacteristicFormats.bool)
self.description = 'Mute microphone (on/off)'
self.perms = [CharacteristicPermissions.paired_write, CharacteristicPermissions.paired_read,
CharacteristicPermissions.events]
self.value = False


class MicrophoneMuteCharacteristicMixin(object):
def __init__(self, iid):
self._muteCharacteristic = MicrophoneMuteCharacteristic(iid)
self.characteristics.append(self._muteCharacteristic)

def set_mute_set_callback(self, callback):
self._muteCharacteristic.set_set_value_callback(callback)

def set_mute_get_callback(self, callback):
self._muteCharacteristic.set_get_value_callback(callback)
36 changes: 36 additions & 0 deletions homekit/model/characteristics/rtp_stream/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#
# Copyright 2018 Joachim Lusiardi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

__all__ = [
'SelectedRTPStreamConfigurationCharacteristicMixin',
'SelectedRTPStreamConfigurationCharacteristic', 'SetupEndpointsCharacteristicMixin',
'SetupEndpointsCharacteristic', 'StreamingStatusCharacteristicMixin',
'StreamingStatusCharacteristic', 'SupportedRTPConfigurationCharacteristic',
'SupportedVideoStreamConfigurationCharacteristic', 'SupportedAudioStreamConfigurationCharacteristic'
]

from homekit.model.characteristics.rtp_stream.supported_video_stream_configuration import \
SupportedVideoStreamConfigurationCharacteristic
from homekit.model.characteristics.rtp_stream.supported_rtp_configuration import \
SupportedRTPConfigurationCharacteristic
from homekit.model.characteristics.rtp_stream.streaming_status import StreamingStatusCharacteristicMixin, \
StreamingStatusCharacteristic
from homekit.model.characteristics.rtp_stream.supported_audio_stream_configuration import \
SupportedAudioStreamConfigurationCharacteristic
from homekit.model.characteristics.rtp_stream.selected_rtp_stream_configuration import \
SelectedRTPStreamConfigurationCharacteristic, SelectedRTPStreamConfigurationCharacteristicMixin
from homekit.model.characteristics.rtp_stream.setup_endpoints import \
SetupEndpointsCharacteristic, SetupEndpointsCharacteristicMixin
Loading

0 comments on commit b5a862e

Please sign in to comment.