Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexxIT committed Apr 11, 2021
0 parents commit 436da5d
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

.homeassistant/

.idea/

custom_components/webrtc/__pycache__/
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# WebRTC Camera

[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs)
[![Donate](https://img.shields.io/badge/donate-Coffee-yellow.svg)](https://www.buymeacoffee.com/AlexxIT)
[![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://money.yandex.ru/to/41001428278477)

Home Assistant custom component for viewing IP cameras [RTSP](https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol) stream in real time using [WebRTC](https://en.wikipedia.org/wiki/WebRTC) technology.

Based on:
- [Pion](https://github.com/pion/webrtc) - pure Go implementation of WebRTC
- [RTSPtoWebRTC](https://github.com/deepch/RTSPtoWebRTC) - Go app by [@deepch](https://github.com/deepch) and [@vdalex25](https://github.com/vdalex25)

Why WebRTC:
- works in any modern browser, even on mobiles
- the only browser technology with minimal camera stream delays (0.5 seconds and below)
- works well with unstable channel
- does not use transcoding and does not load the CPU
- support camera stream with sound

## Install

You can install component with [HACS](https://hacs.xyz/) custom repo: . HACS > Integrations > 3 dots (upper top corner) > Custom repositories > URL: `AlexxIT/WebRTC` > Category: Integration

Or manually copy `webrtc` folder from [latest release](https://github.com/AlexxIT/WebRTC/releases/latest) to `custom_components` folder in your config folder.

## Config

With GUI. Configuration > Integration > Add Integration > WebRTC Camera.

If the integration is not in the list, you need to clear the browser cache.

Component **doesn't create devices/entities/services**. It creates only lovelace custom card:

```yaml
type: 'custom:webrtc-camera'
url: 'rtsp://rtsp:[email protected]:554/av_stream/ch0'
```
# About
Supported clients:
- macOS: Google Chrome, Safari
- Windows: Google Chrome
- Android: Google Chrome, Home Assistant Mobile App
- iOS: Home Assistant Mobile App
Limitations:
- works only with H.264 camaras
- for external access you need a white IP address (without provider NAT), dynamic IP is also supported
Known work cameras:
- Sonoff GK-200MP2-B (support sound)
- EZVIZ C3S
- Hikvision: DS-2CD2T47G1-L, DS-2CD1321-I, DS-2CD2143G0-IS
- Reolink: RLC-410, RLC-410W, E1 Pro, 4505MP
- TP-Link: Tapo C200
Support external camera access. You need to forward UDP ports 50000-50009 to Hass.io server on your router.
50000-50009 ports are used only during video streaming. At each start of the streaming, a random port is occupied. The port is released when the streaming ends. The data should theoretically be encrypted, but I haven't tested :)
111 changes: 111 additions & 0 deletions custom_components/webrtc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import logging
import os
import pathlib
import subprocess
from threading import Thread

import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType, ConfigType

from . import utils

_LOGGER = logging.getLogger(__name__)
DOMAIN = 'webrtc'

BINARY_VERSION = 'v1'


async def async_setup(hass: HomeAssistantType, config: ConfigType):
curdir = pathlib.Path(__file__).parent.absolute()

# check and download file if needed
filepath = hass.config.path(utils.get_binary_name(BINARY_VERSION))
if not os.path.isfile(filepath):
for file in os.listdir(hass.config.config_dir):
if file.startswith('rtsp2webrtc_'):
_LOGGER.debug(f"Remove old binary: {file}")
os.remove(file)

url = utils.get_binary_url(BINARY_VERSION)
_LOGGER.debug(f"Donwload new binary: {url}")

session = async_get_clientsession(hass)
r = await session.get(url)
raw = await r.read()
open(filepath, 'wb').write(raw)
os.chmod(filepath, 744)

# serve lovelace card
path = curdir / 'www/webrtc-camera.js'
url_path = '/webrtc/webrtc-camera.js'
hass.http.register_static_path(url_path, path, cache_headers=False)

# register lovelace card
if await utils.init_resource(hass, url_path):
_LOGGER.debug(f"Init new lovelace custom card: {url_path}")

websocket_api.async_register_command(hass, websocket_webrtc_stream)

hass.data[DOMAIN] = {
'filepath': filepath
}

return True


async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
filepath = hass.data[DOMAIN]['filepath']

# run communication webserver on localhost:8083
process = subprocess.Popen([filepath], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)

hass.data[DOMAIN][entry.entry_id] = process

def run():
# check alive
while process.poll() is None:
line = process.stdout.readline()
if line == b'':
break
_LOGGER.debug(line[:-1].decode())

def stop(*args):
process.terminate()

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop)

Thread(name=DOMAIN, target=run).start()

return True


async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
process = hass.data[DOMAIN][entry.entry_id]
process.terminate()
return True


@websocket_api.websocket_command({
vol.Required('type'): 'webrtc/stream',
vol.Required('url'): str,
vol.Required('sdp64'): str
})
@websocket_api.async_response
async def websocket_webrtc_stream(hass: HomeAssistantType, connection, msg):
try:
session = async_get_clientsession(hass)
r = await session.post('http://localhost:8083/stream', data={
'url': msg['url'], 'sdp64': msg['sdp64']
})
raw = await r.json()

_LOGGER.debug(f"New stream to url: {msg['url']}")
connection.send_result(msg['id'], raw)

except Exception as e:
_LOGGER.error(f"Can't start stream: {msg['url']}, because: {e}")
16 changes: 16 additions & 0 deletions custom_components/webrtc/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import os

from homeassistant.config_entries import ConfigFlow

from . import DOMAIN, utils


class FlowHandler(ConfigFlow, domain=DOMAIN):

async def async_step_user(self, user_input=None):
if utils.get_arch():
return self.async_create_entry(title="WebRTC Camera", data={})

return self.async_abort(reason='arch', description_placeholders={
'uname': os.uname() if os.name != 'nt' else os.name
})
11 changes: 11 additions & 0 deletions custom_components/webrtc/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "webrtc",
"name": "WebRTC Camera",
"config_flow": true,
"documentation": "https://github.com/AlexxIT/WebRTC",
"issue_tracker": "https://github.com/AlexxIT/WebRTC/issues",
"requirements": [],
"dependencies": ["lovelace"],
"codeowners": ["@AlexxIT"],
"version": "v1.0.0"
}
7 changes: 7 additions & 0 deletions custom_components/webrtc/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"config": {
"abort": {
"arch": "Unsupported OS architecture: {uname}"
}
}
}
43 changes: 43 additions & 0 deletions custom_components/webrtc/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import os
from typing import Optional

from homeassistant.components.lovelace.resources import \
ResourceStorageCollection
from homeassistant.helpers.typing import HomeAssistantType

ARCH = {
'armv7l': 'armv7',
'aarch64': 'aarch64',
'x86_64': 'amd64',
}


def get_arch() -> Optional[str]:
uname = ('Windows',) if os.name == 'nt' else os.uname()
if uname[0] == 'Windows':
return 'amd64.exe'
elif uname[0] == 'Linux' and uname[4] in ARCH:
return ARCH[uname[4]]
return None


def get_binary_name(version: str) -> str:
return f"rtsp2webrtc_{version}_{get_arch()}"


def get_binary_url(version: str) -> str:
return "https://github.com/AlexxIT/RTSPtoWebRTC/releases/download/" \
f"{version}/rtsp2webrtc_{get_arch()}"


async def init_resource(hass: HomeAssistantType, url: str) -> bool:
resources: ResourceStorageCollection = hass.data['lovelace']['resources']
# force load storage
await resources.async_get_info()

for item in resources.async_items():
if item['url'] == url:
return False

await resources.async_create_item({'res_type': 'module', 'url': url})
return True
95 changes: 95 additions & 0 deletions custom_components/webrtc/www/webrtc-camera.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
class WebRTCCamera extends HTMLElement {
async _init(hass) {
const pc = new RTCPeerConnection({
iceServers: [{
urls: ['stun:stun.l.google.com:19302']
}]
});

pc.onnegotiationneeded = async () => {
// console.log('onnegotiationneeded');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

const data = await hass.callWS({
type: 'webrtc/stream',
url: this.config.url,
sdp64: btoa(pc.localDescription.sdp)
});
// console.log(data);

try {
const remoteDesc = new RTCSessionDescription({
type: 'answer',
sdp: atob(data.sdp64)
});
await pc.setRemoteDescription(remoteDesc);
} catch (e) {
console.warn(e);
}
}

pc.ontrack = (event) => {
// console.log('ontrack', event);
const el = document.createElement(event.track.kind);
el.srcObject = event.streams[0];
el.muted = true;
el.autoplay = true;
el.controls = true;
el.style.width = '100%';
this.content.appendChild(el);
}

pc.addTransceiver('video', {'direction': 'recvonly'})
pc.addTransceiver('audio', {'direction': 'recvonly'})

const pingChannel = pc.createDataChannel('foo');
pingChannel.onopen = () => {
setInterval(() => {
try {
pingChannel.send('ping');
} catch (e) {
console.warn(e);
}
}, 1000);
}
}

set hass(hass) {
if (!this.content) {
this.content = document.createElement('div');

const card = document.createElement('ha-card');
// card.header = 'WebRTC Card';
card.appendChild(this.content);

this.appendChild(card);

this._init(hass);
}
}

setConfig(config) {
if (!config.url) {
throw new Error('Missing `url: "..."`');
}
this.config = config;
}

static getStubConfig() {
return {
url: 'rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov'
}
}
}

customElements.define('webrtc-camera', WebRTCCamera);


window.customCards = window.customCards || [];
window.customCards.push({
type: 'webrtc-camera',
name: 'WebRTC Camera',
preview: false,
description: 'WebRTC Camera allows you to watch RTSP-camera stream without any delay',
});
4 changes: 4 additions & 0 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "WebRTC Camera",
"render_readme": true
}

0 comments on commit 436da5d

Please sign in to comment.