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`. + +Camera Stream + + ## 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. + +ELP 1080p + +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/ +Raspberry Pi Imager + +- Choose OS → Raspberry Pi OS (other) → Raspberry Pi OS Lite (32-bit) +Raspberry Pi Imager + +- Insert a sd card into your computer and choose it as the storage +Raspberry Pi Imager + +- Click on the settings icon and **enable SSH**, **Set username and password** and **configure wifi** +Raspberry Pi Imager + +- Write the operating system on the sd card by clicking on **Write** +Raspberry Pi Imager + +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...* + +More options + +- Select *Camera* and confirm that the accessory is uncertified + +Select Accessory + +- Enter the pin `001-02-003` and Continue + +Select Accessory + +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. + +Live streaming +Snapshots + +Taking snapshots in automations is also supported. + +Automation + +## 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 + +Desk mount +Wall mount + +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} }