diff --git a/README.md b/README.md
index 980d9be..cf9550e 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,20 @@
# hkcam
`hkcam` is an open-source implementation of an HomeKit IP camera.
-It uses `ffmpeg` to access the camera stream and publishes the stream to HomeKit using [hc](https://github.com/brutella/hc).
-The camera stream can be viewed in a HomeKit app. For example my [Home](https://hochgatterer.me/home) app works perfectly with `hkcam`.
+It uses `ffmpeg` to access the camera stream and publishes the stream to HomeKit using [hap](https://github.com/brutella/hap).
+The camera stream can be viewed in a HomeKit app. For example my [Home+](https://hochgatterer.me/home) app works perfectly with `hkcam`.
+
+
+
+
## Features
- Live streaming via HomeKit
-- Works with any HomeKit app
-- [3D-Printed Enclosure](#enclosure)
+- Works with any HomeKit app (ex. [Home+](https://hochgatterer.me/home))
+- [Multistream Support](#multistream)
- [Persistent Snapshots](#persistent-snapshots)
-- Completely written in Go
-- Runs on multiple platforms (Linux, macOS)
+- Runs on multiple platforms (Raspberry Pi OS, macOS)
## Get Started
@@ -25,7 +28,7 @@ The fastest way to get started is to
```sh
git clone https://github.com/brutella/hkcam && cd hkcam
```
-2. build and run `cmd/hkcam/main.go` by running `make run` in Terminal
+2. build and run `cmd/hkcam/main.go` by running `go run cmd/hkcam/main.go` in Terminal
3. open any HomeKit app and add the camera to HomeKit (pin for initial setup is `001 02 003`)
These steps require *git*, *go* and *ffmpeg* to be installed. On macOS you can install them via Homebrew.
@@ -38,13 +41,171 @@ brew install ffmpeg
### Raspberry Pi
-If you want to create your own surveillance camera, you can run `hkcam` on a Raspberry Pi with attached camera module.
+You can use a camera module or USB camera with a Raspberry Pi to create your own surveillance camera.
+
+
+
+For example the [ELP 1080P USB camera dome](https://de.aliexpress.com/item/4000562253329.html) is great for outdoor use. It is IP66 waterproof and has built-in IR LEDs for night vision. This camera gets you good quality and great performance when running `hkcam` on the latest Raspberry Pi OS.
+
+A cheaper alternative is a [camera module](https://www.raspberrypi.com/products/camera-module-v2/) attached via ribbon cable. You'll have to enable **[Legacy Camera Support](https://www.raspberrypi.com/documentation/accessories/camera.html#libcamera-and-the-legacy-raspicam-camera-stack)** when using a camera module on Raspberry Pi OS. That's why this option is not ideal in my opinion.
+
+---
+
+**How to Install on a Raspberry Pi?**
+
+Follow these steps to install `hkcam` and all the required libraries on a Raspberry Pi OS Lite (32-bit).
+
+1. Download and run the Raspberry Pi Imager from https://www.raspberrypi.com/software/
+
+
+- Choose OS → Raspberry Pi OS (other) → Raspberry Pi OS Lite (32-bit)
+
+
+- Insert a sd card into your computer and choose it as the storage
+
+
+- Click on the settings icon and **enable SSH**, **Set username and password** and **configure wifi**
+
+
+- Write the operating system on the sd card by clicking on **Write**
+
+
+2. Insert the sd card in your Raspberry Pi
+3. Connect your camera (in my case the ELP 1080P) and power supply
+4. Connect to your Raspberry Pi via SSH (the first boot may take a while, so be patient)
+`ssh pi@raspberrypi.local` (enter your previously configured password)
+
+5. Install ffmpeg
+`apt-get install ffmpeg`
+
+6. Install v4l2loopback
+`apt-get install v4l2loopback-dkms`
+
+- Enable v4l2loopback module at boot by creating a file `/etc/modules-load.d/v4l2loopback.conf` with the content
+
+```
+v4l2loopback
+```
+
+- Specify which loopback file should be created by the module (in our case /dev/video99) by creating the file `/etc/modprobe.d/v4l2loopback.conf` with the content
+```
+options v4l2loopback video_nr=99
+```
+
+- Restart the Raspberry Pi and verify that the file `/dev/video99` exists
+
+7. Install `hkcam`
+
+- Download the latest release from https://github.com/brutella/hkcam/releases
+```
+wget https://github.com/brutella/hkcam/releases/download/v0.1.0/hkcam-v0.1.0_linux_arm.tar.gz
+```
+
+- Extract the archive with `tar -xzf hkcam-v0.1.0_linux_arm.tar.gz`
+- Run `hkcam` by executing the following command
+```
+./hkcam -db=/var/lib/hkcam/data -multi_stream=true -verbose
+```
+
+8. Add the camera to HomeKit
+
+- Launch the Apple Home-app and tap *+* → Add Accessory
+
+- Tap *More Options...*
+
+
+
+- Select *Camera* and confirm that the accessory is uncertified
+
+
+
+- Enter the pin `001-02-003` and Continue
+
+
+
+If everything works as expected, you have to configure `hkcam` as a daemon – so that hkcam is automatically run after boot.
+This can be done in different way – [systemd](https://www.raspberrypi.com/documentation/computers/using_linux.html#the-systemd-daemon) is recommended,
+
+
+**How to install with Ansible?**
+
+I've made an [Ansible](http://docs.ansible.com/ansible/index.html) playbook which configures your Raspberry Pi and installs hkcam.
+The following steps require *ansible* to be installed. On macOS you can install it via Homebrew.
+```sh
+brew install ansible
+```
+
+---
+
+First install Raspberry Pi OS, as described above.
+Then create ssh key and copy them to the Raspberry Pi.
-#### Pre-configured Raspbian Image
+```sh
+ssh-keygen
+ssh-copy-id pi@raspberrypi.local
+```
+
+After that you can execute the playbook with the following command.
+
+```sh
+cd ansible && ansible-playbook rpi.yml -i hosts
+```
+
+Once the command finishes, your camera can be added to HomeKit.
+
+## Multistream
+
+Normally in HomeKit a camera stream can only be viewed by one device at a time.
+If a second device wants to to view the stream, the Apple Home app shows
+
+> **Camera Not Available**
+> Wait until someone else in this home stops viewing this camera and try again.
+
+`hkcam` allows multiple devices to view the same stream by setting the option `-multi_stream=true`. That's neat.
+
+## Persistent Snapshots
+
+In addition to video streaming, `hkcam` supports [Persistent Snapshots](/SNAPSHOTS.md).
+*Persistent Snapshots* is a way to take snapshots of the camera and store them on disk.
+You can then access them via HomeKit.
+
+*Persistent Snapshots* are currently supported by [Home+](https://hochgatterer.me/home),
+as you can see from the following screenshots.
+
+
+
+
+Taking snapshots in automations is also supported.
+
+
+
+## Raspberry Pi Zero W
+
+I do get kernel panics when running hkcam with a ELP 1080P USB camera.
+Updating `/boot/config.txt` with the following changes resolve those kernel panics.
+
+```
+arm_freq=800
+arm_freq_max=900
+arm_freq_min=700
+```
+
+## Raspberry Pi Zero W Enclosure
+
+
+
+
+I've also designed an enclsoure for the Raspberry Pi Zero W and standard camera module.
+You can use a stand to put the camera on a desk, or combine it with brackets of the [Articulating Raspberry Pi Camera Mount](https://www.prusaprinters.org/prints/3407-articulating-raspberry-pi-camera-mount-for-prusa-m) to mount it on a wall.
+The 3D-printed parts are available as STL files [here](https://github.com/brutella/hkcam/tree/master/enclosure).
+
+This enclosure is not waterproof and should not be used outside. Instead you should use an [ELP 1080P camera](https://de.aliexpress.com/item/4000562253329.html) and connect it via USB to a Raspberry Pi.
+
+
# Contact
diff --git a/SNAPSHOTS.md b/SNAPSHOTS.md
index 010dcad..4764aeb 100644
--- a/SNAPSHOTS.md
+++ b/SNAPSHOTS.md
@@ -4,7 +4,7 @@
You can then access them via HomeKit.
*Persistent Snapshots* is not defined in the HAP but instead implemented by `hkcam` with custom characteristics.
-*Persistent Snapshots* are supported by [Home 3](https://hochgatterer.me/home).
+*Persistent Snapshots* are supported by [Home+](https://hochgatterer.me/home+).
## Why?
diff --git a/_img/elp-1080p.jpg b/_img/elp-1080p.jpg
new file mode 100644
index 0000000..ece77a0
Binary files /dev/null and b/_img/elp-1080p.jpg differ
diff --git a/_img/home-app-camera.jpeg b/_img/home-app-camera.jpeg
new file mode 100644
index 0000000..085a80c
Binary files /dev/null and b/_img/home-app-camera.jpeg differ
diff --git a/_img/home-app-more-options.jpeg b/_img/home-app-more-options.jpeg
new file mode 100644
index 0000000..ab57e7a
Binary files /dev/null and b/_img/home-app-more-options.jpeg differ
diff --git a/_img/home-app-pin.jpeg b/_img/home-app-pin.jpeg
new file mode 100644
index 0000000..5a3a083
Binary files /dev/null and b/_img/home-app-pin.jpeg differ
diff --git a/_img/home-app-select-camera.jpeg b/_img/home-app-select-camera.jpeg
new file mode 100644
index 0000000..a9e4a4a
Binary files /dev/null and b/_img/home-app-select-camera.jpeg differ
diff --git a/_img/homeplus-automation.jpeg b/_img/homeplus-automation.jpeg
new file mode 100644
index 0000000..a2e78f1
Binary files /dev/null and b/_img/homeplus-automation.jpeg differ
diff --git a/_img/homeplus-snapshots.png b/_img/homeplus-snapshots.png
new file mode 100644
index 0000000..bad4d2e
Binary files /dev/null and b/_img/homeplus-snapshots.png differ
diff --git a/_img/homeplus-stream.png b/_img/homeplus-stream.png
new file mode 100644
index 0000000..fdb3c73
Binary files /dev/null and b/_img/homeplus-stream.png differ
diff --git a/_img/rpi-imager-os.png b/_img/rpi-imager-os.png
new file mode 100644
index 0000000..2b2b314
Binary files /dev/null and b/_img/rpi-imager-os.png differ
diff --git a/_img/rpi-imager-settings.png b/_img/rpi-imager-settings.png
new file mode 100644
index 0000000..bfb2006
Binary files /dev/null and b/_img/rpi-imager-settings.png differ
diff --git a/_img/rpi-imager-storage.png b/_img/rpi-imager-storage.png
new file mode 100644
index 0000000..658c806
Binary files /dev/null and b/_img/rpi-imager-storage.png differ
diff --git a/_img/rpi-imager-write.png b/_img/rpi-imager-write.png
new file mode 100644
index 0000000..0b51bd5
Binary files /dev/null and b/_img/rpi-imager-write.png differ
diff --git a/_img/rpi-imager.png b/_img/rpi-imager.png
new file mode 100644
index 0000000..1e29358
Binary files /dev/null and b/_img/rpi-imager.png differ
diff --git a/ansible/roles/hkcam/defaults/main.yml b/ansible/roles/hkcam/defaults/main.yml
index 34807b8..3c60e31 100644
--- a/ansible/roles/hkcam/defaults/main.yml
+++ b/ansible/roles/hkcam/defaults/main.yml
@@ -1,9 +1,7 @@
---
-hkcam_version: 'v0.0.10'
+hkcam_version: 'v0.1.0'
hkcam_download_file_name: hkcam-{{ hkcam_version }}_linux_arm.tar.gz
hkcam_download_url: https://github.com/brutella/hkcam/releases/download/{{ hkcam_version }}/{{ hkcam_download_file_name }}
hkcam_download_dir: /tmp
hkcam_download_dest: "{{ hkcam_download_dir }}/{{ hkcam_download_file_name }}"
-hkcam_data_dir: /var/lib/hkcam/data
-
-disable_camera_led: false
\ No newline at end of file
+hkcam_data_dir: /var/lib/hkcam/data
\ No newline at end of file
diff --git a/ansible/roles/hkcam/tasks/configure.yml b/ansible/roles/hkcam/tasks/configure.yml
index 393e159..aa5099a 100644
--- a/ansible/roles/hkcam/tasks/configure.yml
+++ b/ansible/roles/hkcam/tasks/configure.yml
@@ -1,66 +1,14 @@
---
-- name: Update camera config
- lineinfile:
- dest: /boot/config.txt
- regexp: "{{ item.regexp }}"
- line: "{{ item.line }}"
- with_items:
- - { regexp: '^start_x=', line: 'start_x=1' }
- - { regexp: '^gpu_mem=', line: 'gpu_mem=128' }
-
-- name: Disable camera led
- lineinfile:
- dest: /boot/config.txt
- regexp: "^disable_camera_led="
- line: "disable_camera_led=1"
- when: disable_camera_led
-
-- name: Enable camera led
- lineinfile:
- dest: /boot/config.txt
- regexp: "^disable_camera_led="
- line: "disable_camera_led=0"
- when: disable_camera_led == false
-
-- name: Install HKCam configuration file
- copy:
- dest: "/boot/hkcam.txt"
- content: |
- # Modify this file with the settings you want to have
-
- # PIN used while pairing the device
- export HKCAM_HOMEKIT_PIN=00102003
-
- # Force a minimum bitrate on all streams.
- # NOTE: specifying a high bit rate may cause some devices, such as the Apple Watch, to not function correctly
- export HKCAM_MIN_BITRATE=0
-
- # Rotate the camera view (e.g. set this to 180 if you mount your device upside down)
- export HKCAM_ROTATION=0
-
- # Enable multiple clients to watch the stream simultaneously
- export HKCAM_MULTI_STREAM=false
-
-# - name: Add bcm2835 module
-# modprobe:
-# name: bcm2835-v4l2
-# state: present
-
-- name: Add bcm2835 module
- lineinfile:
- dest: /etc/modules
- regexp: "^bcm2835-v4l2"
- line: "bcm2835-v4l2"
- name: Update packages
apt:
update_cache: yes
upgrade: yes
-- name: Reboot
- changed_when: false
- reboot:
- reboot_timeout: 200
+# - name: Reboot
+# changed_when: false
+# reboot:
+# reboot_timeout: 200
- name: Install packages
apt:
@@ -68,51 +16,15 @@
state: present
vars:
packages:
- - bc
- - libncurses5-dev
- ffmpeg
- - raspberrypi-kernel-headers
-
-# Old way of installing v4l2loopback
-# - name: Install rpi-source
-# changed_when: false
-# shell: sudo wget https://raw.githubusercontent.com/notro/rpi-source/master/rpi-source -O /usr/bin/rpi-source && sudo chmod +x /usr/bin/rpi-source && /usr/bin/rpi-source -q --tag-update
-#
-# - name: Install kernel source
-# changed_when: false
-# shell: rpi-source
-#
-# - name: Install v4l2loopback
-# apt:
-# name: "{{ packages }}"
-# state: present
-# vars:
-# packages:
-# - v4l2loopback-dkms
-
-- name: Download v4l2loopback
- get_url:
- url: https://github.com/umlaeute/v4l2loopback/archive/v0.12.5.tar.gz
- dest: /tmp
- register: v4l2_pkg_download
+ - v4l2loopback-dkms
-- name: Extract {{ v4l2_pkg_download }} to /tmp
- unarchive:
- src: "{{ v4l2_pkg_download.dest }}"
- dest: "/tmp"
- remote_src: true
- list_files: true
- register: v4l2_unarchived
-
-- name: Define extracted folder
- set_fact: v4l2_unarchived_dir="/tmp/{{ v4l2_unarchived.files[0] | dirname }}"
-
-- name: Install v4l2loopback from source
- changed_when: false
- shell: cd {{ v4l2_unarchived_dir }} && make && sudo make install && depmod -a
+- name: Enable v4l2loopback module
+ copy:
+ dest: "/etc/modules-load.d/v4l2loopback.conf"
+ content: "v4l2loopback"
-- name: Add v4l2loopback module
- lineinfile:
- dest: /etc/modules
- regexp: "^v4l2loopback"
- line: "v4l2loopback"
+- name: Set loopback file /dev/video99
+ copy:
+ dest: "/etc/modprobe.d/v4l2loopback.conf"
+ content: "options v4l2loopback video_nr=99"
diff --git a/ansible/roles/hkcam/tasks/main.yml b/ansible/roles/hkcam/tasks/main.yml
index 942b5fb..31ac1e9 100644
--- a/ansible/roles/hkcam/tasks/main.yml
+++ b/ansible/roles/hkcam/tasks/main.yml
@@ -13,15 +13,6 @@
#!/bin/sh -e
exec 2>&1
- # Load in config settings
- . /boot/hkcam.txt
-
- # Default to 720p for now
- v4l2-ctl --set-fmt-video=width=1280,height=720,pixelformat=YU12
- v4l2-ctl --set-ctrl=rotate=$HKCAM_ROTATION
-
- exec hkcam --data_dir={{ hkcam_data_dir }} --verbose=true \
- --min_video_bitrate=$HKCAM_MIN_BITRATE --multi_stream=$HKCAM_MULTI_STREAM \
- --pin=$HKCAM_HOMEKIT_PIN
+ exec hkcam --data_dir={{ hkcam_data_dir }} --verbose=true
log_dir: /var/log/hkcam
tags: [runit]
\ No newline at end of file
diff --git a/ansible/rpi.yml b/ansible/rpi.yml
index 260a65a..2c055c8 100644
--- a/ansible/rpi.yml
+++ b/ansible/rpi.yml
@@ -4,7 +4,6 @@
become: true
roles:
- role: hkcam
- disable_camera_led: false
enabled: true
tasks:
- name: Reboot
diff --git a/assets.go b/assets.go
index 90145c3..a2e4e05 100644
--- a/assets.go
+++ b/assets.go
@@ -1,7 +1,7 @@
package hkcam
import (
- "github.com/brutella/hc/characteristic"
+ "github.com/brutella/hap/characteristic"
)
// TypeAssets is the uuid of the Assets characteristic
@@ -16,9 +16,9 @@ type Assets struct {
func NewAssets() *Assets {
b := characteristic.NewBytes(TypeAssets)
- b.Perms = []string{characteristic.PermRead, characteristic.PermEvents}
+ b.Permissions = []string{characteristic.PermissionRead, characteristic.PermissionEvents}
- b.Value = []byte{}
+ b.SetValue([]byte{})
return &Assets{b}
}
diff --git a/camera_control.go b/camera_control.go
index d8cbdf1..08f95d7 100644
--- a/camera_control.go
+++ b/camera_control.go
@@ -1,7 +1,7 @@
package hkcam
import (
- "github.com/brutella/hc/log"
+ "github.com/brutella/hap/log"
"github.com/nfnt/resize"
"github.com/radovskyb/watcher"
diff --git a/cmd/hkcam/main.go b/cmd/hkcam/main.go
index acdd91f..188941b 100644
--- a/cmd/hkcam/main.go
+++ b/cmd/hkcam/main.go
@@ -1,17 +1,25 @@
package main
import (
- "flag"
-
- "github.com/brutella/hc"
- "github.com/brutella/hc/accessory"
- "github.com/brutella/hc/log"
+ "github.com/brutella/hap"
+ "github.com/brutella/hap/accessory"
+ "github.com/brutella/hap/log"
+ "github.com/brutella/hkcam"
+ "github.com/brutella/hkcam/ffmpeg"
+ "bytes"
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
"image"
+ "image/jpeg"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/signal"
"runtime"
-
- "github.com/brutella/hkcam"
- "github.com/brutella/hkcam/ffmpeg"
+ "syscall"
)
var (
@@ -34,24 +42,24 @@ func main() {
if runtime.GOOS == "linux" {
inputDevice = flag.String("input_device", "v4l2", "video input device")
inputFilename = flag.String("input_filename", "/dev/video0", "video input device filename")
- loopbackFilename = flag.String("loopback_filename", "/dev/video1", "video loopback device filename")
+ loopbackFilename = flag.String("loopback_filename", "/dev/video99", "video loopback device filename")
h264Decoder = flag.String("h264_decoder", "", "h264 video decoder")
- h264Encoder = flag.String("h264_encoder", "h264_omx", "h264 video encoder")
+ h264Encoder = flag.String("h264_encoder", "h264_v4l2m2m", "h264 video encoder")
} else if runtime.GOOS == "darwin" { // macOS
inputDevice = flag.String("input_device", "avfoundation", "video input device")
inputFilename = flag.String("input_filename", "default", "video input device filename")
// loopback is not needed on macOS because avfoundation provides multi-access to the camera
loopbackFilename = flag.String("loopback_filename", "", "video loopback device filename")
h264Decoder = flag.String("h264_decoder", "", "h264 video decoder")
- h264Encoder = flag.String("h264_encoder", "libx264", "h264 video encoder")
+ h264Encoder = flag.String("h264_encoder", "h264_videotoolbox", "h264 video encoder")
} else {
log.Info.Fatalf("%s platform is not supported", runtime.GOOS)
}
var minVideoBitrate *int = flag.Int("min_video_bitrate", 0, "minimum video bit rate in kbps")
var multiStream *bool = flag.Bool("multi_stream", false, "Allow mutliple clients to view the stream simultaneously")
- var dataDir *string = flag.String("data_dir", "Camera", "Path to data directory")
- var verbose *bool = flag.Bool("verbose", true, "Verbose logging")
+ var dataDir *string = flag.String("data_dir", "db", "Path to data directory")
+ var verbose *bool = flag.Bool("verbose", false, "Verbose logging")
var pin *string = flag.String("pin", "00102003", "PIN for HomeKit pairing")
var port *string = flag.String("port", "", "Port on which transport is reachable")
@@ -64,7 +72,7 @@ func main() {
log.Info.Printf("version %s (built at %s)\n", Version, Date)
- switchInfo := accessory.Info{Name: "Camera", FirmwareRevision: Version, Manufacturer: "Matthias Hochgatterer"}
+ switchInfo := accessory.Info{Name: "Camera", Firmware: Version, Manufacturer: "Matthias Hochgatterer"}
cam := accessory.NewCamera(switchInfo)
cfg := ffmpeg.Config{
@@ -81,28 +89,102 @@ func main() {
// Add a custom camera control service to record snapshots
cc := hkcam.NewCameraControl()
- cam.Control.AddCharacteristic(cc.Assets.Characteristic)
- cam.Control.AddCharacteristic(cc.GetAsset.Characteristic)
- cam.Control.AddCharacteristic(cc.DeleteAssets.Characteristic)
- cam.Control.AddCharacteristic(cc.TakeSnapshot.Characteristic)
+ cam.Control.AddC(cc.Assets.C)
+ cam.Control.AddC(cc.GetAsset.C)
+ cam.Control.AddC(cc.DeleteAssets.C)
+ cam.Control.AddC(cc.TakeSnapshot.C)
- t, err := hc.NewIPTransport(hc.Config{StoragePath: *dataDir, Pin: *pin, Port: *port}, cam.Accessory)
+ s, err := hap.NewServer(hap.NewFsStore(*dataDir), cam.A)
if err != nil {
log.Info.Panic(err)
}
- t.CameraSnapshotReq = func(width, height uint) (*image.Image, error) {
- return ffmpeg.Snapshot(width, height)
- }
+ s.Pin = *pin
+ s.Addr = fmt.Sprintf(":%s", *port)
+
+ s.ServeMux().HandleFunc("/resource", func(res http.ResponseWriter, req *http.Request) {
+ if !s.IsAuthorized(req) {
+ hap.JsonError(res, hap.JsonStatusInsufficientPrivileges)
+ return
+ }
+
+ if req.Method != http.MethodPost {
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ body, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ log.Info.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ r := struct {
+ Type string `json:"resource-type"`
+ Width uint `json:"image-width"`
+ Height uint `json:"image-height"`
+ }{}
+
+ err = json.Unmarshal(body, &r)
+ if err != nil {
+ log.Info.Println(err)
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ log.Debug.Printf("%+v\n", r)
+
+ switch r.Type {
+ case "image":
+ b, err := snapshot(r.Width, r.Height, ffmpeg)
+ if err != nil {
+ log.Info.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "image/jpeg")
+ wr := hap.NewChunkedWriter(res, 2048)
+ wr.Write(b)
+ default:
+ log.Info.Printf("unsupported resource request \"%s\"\n", r.Type)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ })
cc.SetupWithDir(*dataDir)
cc.CameraSnapshotReq = func(width, height uint) (*image.Image, error) {
return ffmpeg.Snapshot(width, height)
}
- hc.OnTermination(func() {
- <-t.Stop()
- })
+ c := make(chan os.Signal)
+ signal.Notify(c, os.Interrupt)
+ signal.Notify(c, syscall.SIGTERM)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ go func() {
+ <-c
+ signal.Stop(c) // stop delivering signals
+ cancel()
+ }()
+
+ s.ListenAndServe(ctx)
+}
+
+func snapshot(width, height uint, ffmpeg ffmpeg.FFMPEG) ([]byte, error) {
+ log.Debug.Printf("snapshot %dw x %dh\n", width, height)
+
+ img, err := ffmpeg.Snapshot(width, height)
+ if err != nil {
+ return nil, fmt.Errorf("snapshot: %v", err)
+ }
+
+ buf := new(bytes.Buffer)
+ if err := jpeg.Encode(buf, *img, nil); err != nil {
+ return nil, fmt.Errorf("encode: %v", err)
+ }
- t.Start()
+ return buf.Bytes(), nil
}
diff --git a/delete_assets.go b/delete_assets.go
index 05b7607..67949d1 100644
--- a/delete_assets.go
+++ b/delete_assets.go
@@ -1,7 +1,7 @@
package hkcam
import (
- "github.com/brutella/hc/characteristic"
+ "github.com/brutella/hap/characteristic"
)
// TypeDeleteAssets is the uuid of the DeleteAssets characteristic
@@ -16,8 +16,8 @@ type DeleteAssets struct {
func NewDeleteAssets() *DeleteAssets {
b := characteristic.NewBytes(TypeDeleteAssets)
- b.Perms = []string{characteristic.PermRead, characteristic.PermWrite}
- b.Value = []byte{}
+ b.Permissions = []string{characteristic.PermissionRead, characteristic.PermissionWrite}
+ b.SetValue([]byte{})
return &DeleteAssets{b}
}
diff --git a/ffmpeg/ffmpeg.go b/ffmpeg/ffmpeg.go
index 8608981..4cb70ea 100644
--- a/ffmpeg/ffmpeg.go
+++ b/ffmpeg/ffmpeg.go
@@ -2,8 +2,8 @@ package ffmpeg
import (
"fmt"
- "github.com/brutella/hc/log"
- "github.com/brutella/hc/rtp"
+ "github.com/brutella/hap/log"
+ "github.com/brutella/hap/rtp"
"image"
"io/ioutil"
"os"
diff --git a/ffmpeg/loopback.go b/ffmpeg/loopback.go
index 75ccc3b..655dd30 100644
--- a/ffmpeg/loopback.go
+++ b/ffmpeg/loopback.go
@@ -10,7 +10,7 @@ import (
"syscall"
"time"
- "github.com/brutella/hc/log"
+ "github.com/brutella/hap/log"
)
// loopback copies data from the inpute filename to the loopback filename.
diff --git a/ffmpeg/stream.go b/ffmpeg/stream.go
index e4a36cc..40097e4 100644
--- a/ffmpeg/stream.go
+++ b/ffmpeg/stream.go
@@ -2,8 +2,8 @@ package ffmpeg
import (
"fmt"
- "github.com/brutella/hc/log"
- "github.com/brutella/hc/rtp"
+ "github.com/brutella/hap/log"
+ "github.com/brutella/hap/rtp"
"os/exec"
"strings"
"syscall"
@@ -69,7 +69,7 @@ func (s *stream) start(video rtp.VideoParameters, audio rtp.AudioParameters) err
fmt.Sprintf(" -ssrc %d", s.resp.SsrcVideo) +
" -f rtp -srtp_out_suite AES_CM_128_HMAC_SHA1_80" +
fmt.Sprintf(" -srtp_out_params %s", s.req.Video.SrtpKey()) +
- fmt.Sprintf(" srtp://%s:%d?rtcpport=%d&localrtcpport=%d&pkt_size=%s&timeout=60", s.req.ControllerAddr.IPAddr, s.req.ControllerAddr.VideoRtpPort, s.req.ControllerAddr.VideoRtpPort, s.req.ControllerAddr.VideoRtpPort, videoMTU(s.req))
+ fmt.Sprintf(" srtp://%s:%d?rtcpport=%d&pkt_size=%s&timeout=60", s.req.ControllerAddr.IPAddr, s.req.ControllerAddr.VideoRtpPort, s.req.ControllerAddr.VideoRtpPort, videoMTU(s.req))
// FIXME (mah) Audio doesn't work yet
// ffmpegAudio := "-vn" +
@@ -113,7 +113,7 @@ func (s *stream) resume() {
// TODO (mah) implement
func (s *stream) reconfigure(video rtp.VideoParameters, audio rtp.AudioParameters) error {
if s.cmd != nil {
- log.Debug.Println("reconfigure() is not implemented")
+ log.Debug.Printf("reconfigure() is not implemented %+v %+v\n", video, audio)
}
return nil
diff --git a/get_asset.go b/get_asset.go
index 2cc4207..48c2a06 100644
--- a/get_asset.go
+++ b/get_asset.go
@@ -1,7 +1,7 @@
package hkcam
import (
- "github.com/brutella/hc/characteristic"
+ "github.com/brutella/hap/characteristic"
)
const TypeGetAsset = "6A6C39F5-67F0-4BE1-BA9D-E56BD27C9606"
@@ -16,8 +16,8 @@ type GetAsset struct {
func NewGetAsset() *GetAsset {
b := characteristic.NewBytes(TypeGetAsset)
- b.Perms = []string{characteristic.PermRead, characteristic.PermWrite}
- b.Value = []byte{}
+ b.Permissions = []string{characteristic.PermissionRead, characteristic.PermissionWrite}
+ b.SetValue([]byte{})
return &GetAsset{b}
}
diff --git a/go.mod b/go.mod
index 4baff82..e6efdb9 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/brutella/hkcam
go 1.12
require (
- github.com/brutella/hc v1.2.5-0.20210809073424-91c89ca209d9
+ github.com/brutella/hap v0.0.12
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/radovskyb/watcher v1.0.6
)
diff --git a/go.sum b/go.sum
index b33f357..e6617b5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,12 +1,17 @@
-github.com/brutella/dnssd v1.2.0 h1:bgrSycmZ2+u4BoJxRf1BzSlnViSAfeXWVdujqjLA004=
-github.com/brutella/dnssd v1.2.0/go.mod h1:FpJqlQ8+XU6w1vbnG1zJiQPTRE5fvQIRdrcBojMVuuQ=
-github.com/brutella/hc v1.2.5-0.20210809073424-91c89ca209d9 h1:Hy14RKhCSxlOiRTDaXBfwL8ibF5ZoGl+mS26q/tY1Ik=
-github.com/brutella/hc v1.2.5-0.20210809073424-91c89ca209d9/go.mod h1:TPPdombm3gA/2fsSON6ct2km7z7Vi8lQNqE+fzuDHQM=
+github.com/brutella/dnssd v1.2.1 h1:1xG+5itx/SDEP6ukYfAcBnox5WACTNvxZ+SMkAmSrFU=
+github.com/brutella/dnssd v1.2.1/go.mod h1:FpJqlQ8+XU6w1vbnG1zJiQPTRE5fvQIRdrcBojMVuuQ=
+github.com/brutella/hap v0.0.10 h1:jH8tsMNHMzqFSzJ0PrBhT5GwXTUN7xrTGNbMfNbjpuw=
+github.com/brutella/hap v0.0.10/go.mod h1:bpOEXdJ80ZI2lphDz+jdO0RoyQOn3tWeBDhts98sYF4=
+github.com/brutella/hap v0.0.11 h1:UPjGIy31NCHeR+oPWFG2qCylYFCa4zt71812dXH6k6o=
+github.com/brutella/hap v0.0.11/go.mod h1:bpOEXdJ80ZI2lphDz+jdO0RoyQOn3tWeBDhts98sYF4=
+github.com/brutella/hap v0.0.12 h1:Y9ZZIJwC8yvi+VC94j3hyWqjCu4/KIbkHWFYoB2Behc=
+github.com/brutella/hap v0.0.12/go.mod h1:bpOEXdJ80ZI2lphDz+jdO0RoyQOn3tWeBDhts98sYF4=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
+github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
+github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o=
github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
-github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -16,28 +21,34 @@ github.com/radovskyb/watcher v1.0.6/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/tadglines/go-pkgs v0.0.0-20140924210655-1f86682992f1 h1:ms/IQpkxq+t7hWpgKqCE5KjAUQWC24mqBrnL566SWgE=
-github.com/tadglines/go-pkgs v0.0.0-20140924210655-1f86682992f1/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
-github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed h1:Gjnw8buhv4V8qXaHtAWPnKXNpCNx62heQpjO8lOY0/M=
-github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
+github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
+github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
+github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
+github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
+golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/setup.go b/setup.go
index 9aa4137..fef9fe7 100644
--- a/setup.go
+++ b/setup.go
@@ -1,18 +1,20 @@
package hkcam
import (
+ "github.com/brutella/hap/accessory"
+ "github.com/brutella/hap/characteristic"
+ "github.com/brutella/hap/log"
+ "github.com/brutella/hap/rtp"
+ "github.com/brutella/hap/service"
+ "github.com/brutella/hap/tlv8"
+ "github.com/brutella/hkcam/ffmpeg"
+
"fmt"
- "github.com/brutella/hc/accessory"
- "github.com/brutella/hc/characteristic"
- "github.com/brutella/hc/log"
- "github.com/brutella/hc/rtp"
- "github.com/brutella/hc/service"
- "github.com/brutella/hc/tlv8"
+ "math/rand"
"net"
+ "net/http"
"reflect"
"strings"
-
- "github.com/brutella/hkcam/ffmpeg"
)
// SetupFFMPEGStreaming configures a camera to use ffmpeg to stream video.
@@ -37,8 +39,7 @@ func first(ips []net.IP, filter func(net.IP) bool) net.IP {
}
func setupStreamManagement(m *service.CameraRTPStreamManagement, ff ffmpeg.FFMPEG, multiStream bool) {
- status := rtp.StreamingStatus{rtp.StreamingStatusAvailable}
- setTLV8Payload(m.StreamingStatus.Bytes, status)
+ setTLV8Payload(m.StreamingStatus.Bytes, rtp.StreamingStatus{rtp.StreamingStatusAvailable})
setTLV8Payload(m.SupportedRTPConfiguration.Bytes, rtp.NewConfiguration(rtp.CryptoSuite_AES_CM_128_HMAC_SHA1_80))
setTLV8Payload(m.SupportedVideoStreamConfiguration.Bytes, rtp.DefaultVideoStreamConfiguration())
setTLV8Payload(m.SupportedAudioStreamConfiguration.Bytes, rtp.DefaultAudioStreamConfiguration())
@@ -52,18 +53,9 @@ func setupStreamManagement(m *service.CameraRTPStreamManagement, ff ffmpeg.FFMPE
id := ffmpeg.StreamID(cfg.Command.Identifier)
switch cfg.Command.Type {
- case rtp.SessionControlCommandTypeEnd:
- ff.Stop(id)
-
- if ff.ActiveStreams() == 0 {
- // Update stream status when no streams are currently active
- setTLV8Payload(m.StreamingStatus.Bytes, rtp.StreamingStatus{rtp.StreamingStatusAvailable})
- }
-
case rtp.SessionControlCommandTypeStart:
ff.Start(id, cfg.Video, cfg.Audio)
-
- if multiStream == false {
+ if !multiStream {
// If only one video stream is suppported, set the status to busy.
// This way HomeKit knows that nobody is allowed to connect anymore.
// If multiple streams are supported, the status is always availabe.
@@ -75,20 +67,26 @@ func setupStreamManagement(m *service.CameraRTPStreamManagement, ff ffmpeg.FFMPE
ff.Resume(id)
case rtp.SessionControlCommandTypeReconfigure:
ff.Reconfigure(id, cfg.Video, cfg.Audio)
+ case rtp.SessionControlCommandTypeEnd:
+ ff.Stop(id)
+ setTLV8Payload(m.StreamingStatus.Bytes, rtp.StreamingStatus{rtp.StreamingStatusAvailable})
default:
log.Debug.Printf("Unknown command type %d", cfg.Command.Type)
}
})
- m.SetupEndpoints.OnValueUpdateFromConn(func(conn net.Conn, c *characteristic.Characteristic, new, old interface{}) {
- buf := m.SetupEndpoints.GetValue()
+ m.SetupEndpoints.OnValueUpdate(func(new, old []byte, r *http.Request) {
+ if r == nil {
+ return
+ }
+
var req rtp.SetupEndpoints
- err := tlv8.Unmarshal(buf, &req)
+ err := tlv8.Unmarshal(new, &req)
if err != nil {
log.Debug.Fatalf("SetupEndpoints: Could not unmarshal tlv8 data: %s\n", err)
}
- iface, err := ifaceOfConnection(conn)
+ iface, err := ifaceOfRequest(r)
if err != nil {
log.Debug.Println(err)
return
@@ -100,8 +98,8 @@ func setupStreamManagement(m *service.CameraRTPStreamManagement, ff ffmpeg.FFMPE
}
// TODO ssrc is different for every stream
- ssrcVideo := int32(1)
- ssrcAudio := int32(2)
+ ssrcVideo := rand.Int31()
+ ssrcAudio := rand.Int31()
resp := rtp.SetupEndpointsResponse{
SessionId: req.SessionId,
@@ -158,9 +156,14 @@ func ipAtInterface(iface net.Interface, version uint8) (net.IP, error) {
return nil, fmt.Errorf("%s: No ip address found for version %d", iface.Name, version)
}
-// ifaceOfConnection returns the network interface at which the connection was established.
-func ifaceOfConnection(conn net.Conn) (*net.Interface, error) {
- host, _, err := net.SplitHostPort(conn.LocalAddr().String())
+// ifaceOfRequest returns the network interface at which the connection was established.
+func ifaceOfRequest(r *http.Request) (*net.Interface, error) {
+ v := r.Context().Value(http.LocalAddrContextKey)
+ if v == nil {
+ return nil, fmt.Errorf("no local address in context")
+ }
+
+ host, _, err := net.SplitHostPort(v.(net.Addr).String())
if err != nil {
return nil, err
}
diff --git a/take_snapshot.go b/take_snapshot.go
index 20c5cd1..0fd233d 100644
--- a/take_snapshot.go
+++ b/take_snapshot.go
@@ -1,7 +1,7 @@
package hkcam
import (
- "github.com/brutella/hc/characteristic"
+ "github.com/brutella/hap/characteristic"
)
const TypeTakeSnapshot = "E8AEE54F-6E4B-46D8-85B2-FECE188FDB08"
@@ -16,7 +16,7 @@ type TakeSnapshot struct {
func NewTakeSnapshot() *TakeSnapshot {
b := characteristic.NewBool(TypeTakeSnapshot)
b.Description = "Take Snapshot"
- b.Perms = []string{characteristic.PermWrite}
+ b.Permissions = []string{characteristic.PermissionWrite}
return &TakeSnapshot{b}
}