From cc73f54abad61b3d8170b91ab56b5317c96e1e43 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Fri, 22 Nov 2024 20:18:30 +0100 Subject: [PATCH 01/51] First attempt at multi-camera setup. --- files/eufyp2pstream.py | 463 +++++++---------------------------------- 1 file changed, 78 insertions(+), 385 deletions(-) diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 1282030..d843be1 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -1,404 +1,97 @@ - -from websocket import EufySecurityWebSocket -import aiohttp import asyncio -import json import socket -import select -import threading -import time -import sys -import signal from http.server import BaseHTTPRequestHandler, HTTPServer -import os from queue import Queue +from threading import Thread +from websocket import EufySecurityWebSocket +import json +# Constants RECV_CHUNK_SIZE = 4096 -video_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -audio_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -backchannel_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - -EVENT_CONFIGURATION: dict = { - "livestream video data": { - "name": "video_data", - "value": "buffer", - "type": "event", - }, - "livestream audio data": { - "name": "audio_data", - "value": "buffer", - "type": "event", - }, -} - -START_P2P_LIVESTREAM_MESSAGE = { - "messageId": "start_livestream", - "command": "device.start_livestream", - "serialNumber": None, -} - -STOP_P2P_LIVESTREAM_MESSAGE = { - "messageId": "stop_livestream", - "command": "device.stop_livestream", - "serialNumber": None, -} - -START_TALKBACK = { - "messageId": "start_talkback", - "command": "device.start_talkback", - "serialNumber": None, -} - -SEND_TALKBACK_AUDIO_DATA = { - "messageId": "talkback_audio_data", - "command": "device.talkback_audio_data", - "serialNumber": None, - "buffer": None -} - -STOP_TALKBACK = { - "messageId": "stop_talkback", - "command": "device.stop_talkback", - "serialNumber": None, -} - -SET_API_SCHEMA = { - "messageId": "set_api_schema", - "command": "set_api_schema", - "schemaVersion": 13, -} - - -P2P_LIVESTREAMING_STATUS = "p2pLiveStreamingStatus" - -START_LISTENING_MESSAGE = {"messageId": "start_listening", "command": "start_listening"} - -TALKBACK_RESULT_MESSAGE = {"messageId": "talkback_audio_data", "errorCode": "device_talkback_not_running"} - -DRIVER_CONNECT_MESSAGE = {"messageId": "driver_connect", "command": "driver.connect"} - -run_event = threading.Event() - -def exit_handler(signum, frame): - print(f'Signal handler called with signal {signum}') - run_event.set() - -# Install signal handler -signal.signal(signal.SIGINT, exit_handler) - -class ClientAcceptThread(threading.Thread): - def __init__(self,socket,run_event,name,ws,serialno): - threading.Thread.__init__(self) - self.socket = socket - self.queues = [] - self.run_event = run_event - self.name = name - self.ws = ws - self.serialno = serialno - self.my_threads = [] - - def update_threads(self): - my_threads_before = len(self.my_threads) - for thread in self.my_threads: - if not thread.is_alive(): - self.queues.remove(thread.queue) - self.my_threads = [t for t in self.my_threads if t.is_alive()] - if self.ws and my_threads_before > 0 and len(self.my_threads) == 0: - if self.name == "BackChannel": - print("All clients died (BackChannel): ", self.name) - sys.stdout.flush() - else: - print("All clients died. Stopping Stream: ", self.name) - sys.stdout.flush() - - msg = STOP_P2P_LIVESTREAM_MESSAGE.copy() - msg["serialNumber"] = self.serialno - asyncio.run(self.ws.send_message(json.dumps(msg))) - - def run(self): - print("Accepting connection for ", self.name) - msg = STOP_TALKBACK.copy() - msg["serialNumber"] = self.serialno - asyncio.run(self.ws.send_message(json.dumps(msg))) - while not self.run_event.is_set(): - self.update_threads() - sys.stdout.flush() - try: - client_sock, client_addr = self.socket.accept() - print ("New connection added: ", client_addr, " for ", self.name) - sys.stdout.flush() - - if self.name == "BackChannel": - client_sock.setblocking(True) - print("Starting BackChannel") - thread = ClientRecvThread(client_sock, run_event, self.name, self.ws, self.serialno) - thread.start() - else: - client_sock.setblocking(False) - thread = ClientSendThread(client_sock, run_event, self.name, self.ws, self.serialno) - self.queues.append(thread.queue) - if self.ws: - msg = START_P2P_LIVESTREAM_MESSAGE.copy() - msg["serialNumber"] = self.serialno - asyncio.run(self.ws.send_message(json.dumps(msg))) - self.my_threads.append(thread) - thread.start() - except socket.timeout: - pass - -class ClientSendThread(threading.Thread): - def __init__(self,client_sock,run_event,name,ws,serialno): - threading.Thread.__init__(self) - self.client_sock = client_sock - self.queue = Queue(100) - self.run_event = run_event - self.name = name - self.ws = ws - self.serialno = serialno - - def run(self): - print ("Thread running: ", self.name) - sys.stdout.flush() - - try: - while not self.run_event.is_set(): - ready_to_read, ready_to_write, in_error = \ - select.select([], [self.client_sock], [self.client_sock], 2) - if len(in_error): - print("Exception in socket", self.name) - sys.stdout.flush() - break - if not len(ready_to_write): - print("Socket not ready to write ", self.name) - sys.stdout.flush() - break - if not self.queue.empty(): - self.client_sock.sendall( - bytearray(self.queue.get(True)["data"]) - ) - except socket.error as e: - print("Connection lost", self.name, e) - pass - except socket.timeout: - print("Timeout on socket for ", self.name) - pass - try: - self.client_sock.shutdown(socket.SHUT_RDWR) - except OSError: - print ("Error shutdown socket: ", self.name) - self.client_sock.close() - print ("Thread stopping: ", self.name) - sys.stdout.flush() - -class ClientRecvThread(threading.Thread): - def __init__(self,client_sock,run_event,name,ws,serialno): - threading.Thread.__init__(self) - self.client_sock = client_sock - self.run_event = run_event - self.name = name - self.ws = ws - self.serialno = serialno - - def run(self): - msg = START_TALKBACK.copy() - msg["serialNumber"] = self.serialno - asyncio.run(self.ws.send_message(json.dumps(msg))) - try: - curr_packet = bytearray() - no_data = 0 - while not self.run_event.is_set(): - try: - ready_to_read, ready_to_write, in_error = \ - select.select([self.client_sock,], [], [self.client_sock], 2) - if len(in_error): - print("Exception in socket", self.name) - sys.stdout.flush() - break - if len(ready_to_read): - data = self.client_sock.recv(RECV_CHUNK_SIZE) - curr_packet += bytearray(data) - if len(data) > 0: # and len(data) <= RECV_CHUNK_SIZE: - msg = SEND_TALKBACK_AUDIO_DATA.copy() - msg["serialNumber"] = self.serialno - msg["buffer"] = list(bytes(curr_packet)) - asyncio.run(self.ws.send_message(json.dumps(msg))) - curr_packet = bytearray() - no_data = 0 - else: - no_data += 1 - else: - no_data += 1 - if (no_data >= 15): - print("15x in a row no data in socket ", self.name) - sys.stdout.flush() - break - except BlockingIOError: - # Resource temporarily unavailable (errno EWOULDBLOCK) - pass - except socket.error as e: - print("Connection lost", self.name, e) - pass - except socket.timeout: - print("Timeout on socket for ", self.name) - pass - except select.error: - print("Select error on socket ", self.name) - pass - sys.stdout.flush() - try: - self.client_sock.shutdown(socket.SHUT_RDWR) - except OSError: - print ("Error shutdown socket: ", self.name) - sys.stdout.flush() - self.client_sock.close() - msg = STOP_TALKBACK.copy() - msg["serialNumber"] = self.serialno - asyncio.run(self.ws.send_message(json.dumps(msg))) - -class Connector: - def __init__( - self, - run_event, - ): - video_sock.bind(("0.0.0.0", 63336)) - video_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - video_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - video_sock.settimeout(1) # timeout for listening - video_sock.listen() - audio_sock.bind(("0.0.0.0", 63337)) - audio_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - audio_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - audio_sock.settimeout(1) # timeout for listening - audio_sock.listen() - backchannel_sock.bind(("0.0.0.0", 63338)) - backchannel_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - backchannel_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - backchannel_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - backchannel_sock.settimeout(1) # timeout for listening - backchannel_sock.listen() - self.ws = None - self.run_event = run_event - self.serialno = "" - - def stop(self): - try: - self.video_sock.shutdown(socket.SHUT_RDWR) - except OSError: - print ("Error shutdown socket") +# Camera Stream Handler +class CameraStreamHandler: + def __init__(self, serial_number, start_port): + self.serial_number = serial_number + self.start_port = start_port + self.video_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.audio_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.start_livestream_msg = { + "messageId": "start_livestream", + "command": "device.start_livestream", + "serialNumber": self.serial_number, + } + self.stop_livestream_msg = { + "messageId": "stop_livestream", + "command": "device.stop_livestream", + "serialNumber": self.serial_number, + } + + def setup_sockets(self): + self.video_sock.bind(("0.0.0.0", self.start_port)) + self.audio_sock.bind(("0.0.0.0", self.start_port + 1)) + self.video_sock.listen(1) + self.audio_sock.listen(1) + + def start_stream(self, websocket): + asyncio.run(self._start_stream_async(websocket)) + + async def _start_stream_async(self, websocket): + await websocket.send(json.dumps(self.start_livestream_msg)) + video_conn, _ = self.video_sock.accept() + audio_conn, _ = self.audio_sock.accept() + + while True: + video_data = await websocket.recv() + audio_data = await websocket.recv() + video_conn.sendall(video_data) + audio_conn.sendall(audio_data) + + def stop_stream(self, websocket): + asyncio.run(self._stop_stream_async(websocket)) + + async def _stop_stream_async(self, websocket): + await websocket.send(json.dumps(self.stop_livestream_msg)) self.video_sock.close() - try: - self.audio_sock.shutdown(socket.SHUT_RDWR) - except OSError: - print ("Error shutdown socket") self.audio_sock.close() - try: - self.backchannel_sock.shutdown(socket.SHUT_RDWR) - except OSError: - print ("Error shutdown socket") - self.backchannel_sock.close() - def setWs(self, ws : EufySecurityWebSocket): - self.ws = ws - async def on_open(self): - print(f" on_open - executed") +# Multi-Camera Manager +class MultiCameraManager: + def __init__(self, cameras, base_port): + self.cameras = cameras + self.base_port = base_port + self.stream_handlers = {} - async def on_close(self): - print(f" on_close - executed") - self.run_event.set() - self.ws = None - stop() - os._exit(-1) + def initialize_streams(self): + current_port = self.base_port + for serial in self.cameras: + handler = CameraStreamHandler(serial, current_port) + handler.setup_sockets() + self.stream_handlers[serial] = handler + current_port += 2 # Allocate two ports per camera - async def on_error(self, message): - print(f" on_error - executed - {message}") + def start_all_streams(self, websocket): + for handler in self.stream_handlers.values(): + Thread(target=handler.start_stream, args=(websocket,)).start() - async def on_message(self, message): - payload = message.json() - message_type: str = payload["type"] - if message_type == "result": - message_id = payload["messageId"] - if message_id != SEND_TALKBACK_AUDIO_DATA["messageId"]: - # Avoid spamming of TALKBACK_AUDIO_DATA logs - print(f"on_message result: {payload}") - sys.stdout.flush() - if message_id == START_LISTENING_MESSAGE["messageId"]: - message_result = payload[message_type] - states = message_result["state"] - for state in states["devices"]: - self.serialno = state["serialNumber"] - self.video_thread = ClientAcceptThread(video_sock, run_event, "Video", self.ws, self.serialno) - self.audio_thread = ClientAcceptThread(audio_sock, run_event, "Audio", self.ws, self.serialno) - self.backchannel_thread = ClientAcceptThread(backchannel_sock, run_event, "BackChannel", self.ws, self.serialno) - self.audio_thread.start() - self.video_thread.start() - self.backchannel_thread.start() - if message_id == TALKBACK_RESULT_MESSAGE["messageId"] and "errorCode" in payload: - error_code = payload["errorCode"] - if error_code == "device_talkback_not_running": - msg = START_TALKBACK.copy() - msg["serialNumber"] = self.serialno - await self.ws.send_message(json.dumps(msg)) + def stop_all_streams(self, websocket): + for handler in self.stream_handlers.values(): + handler.stop_stream(websocket) - if message_type == "event": - message = payload[message_type] - event_type = message["event"] - sys.stdout.flush() - if message["event"] == "livestream audio data": - #print(f"on_audio - {payload}") - event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - event_data_type = EVENT_CONFIGURATION[event_type]["type"] - if event_data_type == "event": - for queue in self.audio_thread.queues: - if queue.full(): - print("Audio queue full.") - queue.get(False) - queue.put(event_value) - if message["event"] == "livestream video data": - #print(f"on_video - {payload}") - event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - event_data_type = EVENT_CONFIGURATION[event_type]["type"] - if event_data_type == "event": - for queue in self.video_thread.queues: - if queue.full(): - print("Video queue full.") - queue.get(False) - queue.put(event_value) - if message["event"] == "livestream error": - print("Livestream Error!") - if self.ws and len(self.video_thread.queues) > 0: - msg = START_P2P_LIVESTREAM_MESSAGE.copy() - msg["serialNumber"] = self.serialno - await self.ws.send_message(json.dumps(msg)) -# Websocket connector -c = Connector(run_event) +if __name__ == "__main__": + # Example usage + CAMERA_SERIALS = ["CAM1_SERIAL", "CAM2_SERIAL", "CAM3_SERIAL"] + BASE_PORT = 8000 -async def init_websocket(): - ws: EufySecurityWebSocket = EufySecurityWebSocket( - "402f1039-eufy-security-ws", - sys.argv[1], - aiohttp.ClientSession(), - c.on_open, - c.on_message, - c.on_close, - c.on_error, - ) - c.setWs(ws) - try: - await ws.connect() - await ws.send_message(json.dumps(START_LISTENING_MESSAGE)) - await ws.send_message(json.dumps(SET_API_SCHEMA)) - await ws.send_message(json.dumps(DRIVER_CONNECT_MESSAGE)) - while not run_event.is_set(): - await asyncio.sleep(1000) - except Exception as ex: - print(ex) - print("init_websocket failed. Exiting.") - os._exit(-1) + manager = MultiCameraManager(CAMERA_SERIALS, BASE_PORT) + manager.initialize_streams() -loop = asyncio.get_event_loop() -loop.run_until_complete(init_websocket()) + # Assuming the WebSocket connection is established here + websocket = EufySecurityWebSocket() # Replace with actual WebSocket connection setup + try: + manager.start_all_streams(websocket) + except KeyboardInterrupt: + manager.stop_all_streams(websocket) From 4105f2f7ce8933eea781a8115de044c0b197b951 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Fri, 22 Nov 2024 20:31:44 +0100 Subject: [PATCH 02/51] Added backchannel. --- files/eufyp2pstream.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index d843be1..45dbd6e 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -1,3 +1,4 @@ + import asyncio import socket from http.server import BaseHTTPRequestHandler, HTTPServer @@ -12,10 +13,12 @@ # Camera Stream Handler class CameraStreamHandler: def __init__(self, serial_number, start_port): + print(f" - CameraStreamHandler - __init__ - serial_number: {serial_number} - start_port: {start_port}") self.serial_number = serial_number self.start_port = start_port self.video_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.audio_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.backchannel_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.start_livestream_msg = { "messageId": "start_livestream", "command": "device.start_livestream", @@ -30,8 +33,10 @@ def __init__(self, serial_number, start_port): def setup_sockets(self): self.video_sock.bind(("0.0.0.0", self.start_port)) self.audio_sock.bind(("0.0.0.0", self.start_port + 1)) + self.backchannel_sock.bind(("0.0.0.0", self.start_port + 2)) self.video_sock.listen(1) self.audio_sock.listen(1) + self.backchannel_sock.listen(1) def start_stream(self, websocket): asyncio.run(self._start_stream_async(websocket)) From 0005c032e4ff44252aa536b7ea60fbe8e7ccc7e6 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Fri, 22 Nov 2024 21:25:54 +0100 Subject: [PATCH 03/51] Added more ports, up to 5 cameras. --- config.yaml | 18 +++++++++++++++++- files/run.sh | 4 ++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index e7a5a92..e410d8d 100644 --- a/config.yaml +++ b/config.yaml @@ -16,4 +16,20 @@ options: eufy_security_ws_port: 3000 schema: eufy_security_ws_port: port -ports: { "63336/tcp": 63336, "63337/tcp": 63337, "63338/tcp": 63338 } +ports: { + "Camera-1-video": 63336, + "Camera-1-audio": 63337, + "Camera-1-backchannel": 63338, + "Camera-2-video": 63339, + "Camera-2-audio": 63340, + "Camera-2-backchannel": 63341, + "Camera-3-video": 63342, + "Camera-3-audio": 63343, + "Camera-3-backchannel": 63344, + "Camera-4-video": 63345, + "Camera-4-audio": 63346, + "Camera-4-backchannel": 63347, + "Camera-5-video": 63348, + "Camera-5-audio": 63349, + "Camera-5-backchannel": 63350, +} diff --git a/files/run.sh b/files/run.sh index 83cd891..47d6b29 100755 --- a/files/run.sh +++ b/files/run.sh @@ -2,6 +2,10 @@ set +u CONFIG_PATH=/data/options.json +echo "Starting EufyP2PStream" +echo "Config path is $CONFIG_PATH" +echo "Config content is $(cat $CONFIG_PATH)" + EUFY_WS_PORT=$(jq --raw-output ".eufy_security_ws_port" $CONFIG_PATH) echo "Starting EufyP2PStream. eufy_security_ws_port is $EUFY_WS_PORT" From 14ee4a09e1f54c44a5efd2225cd29cde6555e6d9 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Fri, 22 Nov 2024 22:03:45 +0100 Subject: [PATCH 04/51] Fixed port mapping. --- config.yaml | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/config.yaml b/config.yaml index e410d8d..6e9a5ef 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ url: http://192.168.178.252:8123/local_eufyp2pstream arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.1-beta +version: 0.2.2-beta slug: eufyp2pstream init: false startup: application @@ -17,19 +17,36 @@ options: schema: eufy_security_ws_port: port ports: { - "Camera-1-video": 63336, - "Camera-1-audio": 63337, - "Camera-1-backchannel": 63338, - "Camera-2-video": 63339, - "Camera-2-audio": 63340, - "Camera-2-backchannel": 63341, - "Camera-3-video": 63342, - "Camera-3-audio": 63343, - "Camera-3-backchannel": 63344, - "Camera-4-video": 63345, - "Camera-4-audio": 63346, - "Camera-4-backchannel": 63347, - "Camera-5-video": 63348, - "Camera-5-audio": 63349, - "Camera-5-backchannel": 63350, + "63336/tcp": 63336, + "63337/tcp": 63337, + "63338/tcp": 63338, + "63339/tcp": 63339, + "63340/tcp": 63340, + "63341/tcp": 63341, + "63342/tcp": 63342, + "63343/tcp": 63343, + "63344/tcp": 63344, + "63345/tcp": 63345, + "63346/tcp": 63346, + "63347/tcp": 63347, + "63348/tcp": 63348, + "63349/tcp": 63349, + "63350/tcp": 63350 +} +ports_description: { + "63336/tcp": "Camera-1 Video Stream", + "63337/tcp": "Camera-1 Audio Stream", + "63338/tcp": "Camera-1 Backchannel", + "63339/tcp": "Camera-2 Video Stream", + "63340/tcp": "Camera-2 Audio Stream", + "63341/tcp": "Camera-2 Backchannel", + "63342/tcp": "Camera-3 Video Stream", + "63343/tcp": "Camera-3 Audio Stream", + "63344/tcp": "Camera-3 Backchannel", + "63345/tcp": "Camera-4 Video Stream", + "63346/tcp": "Camera-4 Audio Stream", + "63347/tcp": "Camera-4 Backchannel", + "63348/tcp": "Camera-5 Video Stream", + "63349/tcp": "Camera-5 Audio Stream", + "63350/tcp": "Camera-5 Backchannel" } From efd85c2f0f49c6669c15a824363505bc7ad9f4ba Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Fri, 22 Nov 2024 22:38:51 +0100 Subject: [PATCH 05/51] Changed repo name --- config.yaml | 2 +- repository.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.yaml b/config.yaml index 6e9a5ef..84f44ed 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ # https://github.com/AlexxIT/builder/blob/master/builder.sh name: eufyp2pstream description: Eufy P2P camera streaming application -url: http://192.168.178.252:8123/local_eufyp2pstream +#url: http://192.168.178.252:8123/local_eufyp2pstream arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration diff --git a/repository.yaml b/repository.yaml index 9837e34..2a16802 100644 --- a/repository.yaml +++ b/repository.yaml @@ -1,3 +1,3 @@ name: Eufy P2P Stream -url: https://github.com/oischinger/eufyp2pstream +url: https://github.com/neerdoc/eufyp2pstream maintainer: Hans Oischinger From 1064a7b1429ff48b97c935c07fa950034950462c Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sat, 23 Nov 2024 00:03:09 +0100 Subject: [PATCH 06/51] Added serial numbers to camera options. --- config.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config.yaml b/config.yaml index 84f44ed..e4b5917 100644 --- a/config.yaml +++ b/config.yaml @@ -14,8 +14,18 @@ map: [config, media] host_network: false options: eufy_security_ws_port: 3000 + camera_1_serial_number: "" + camera_2_serial_number: "" + camera_3_serial_number: "" + camera_4_serial_number: "" + camera_5_serial_number: "" schema: eufy_security_ws_port: port + camera_1_serial_number: string + camera_2_serial_number: string + camera_3_serial_number: string + camera_4_serial_number: string + camera_5_serial_number: string ports: { "63336/tcp": 63336, "63337/tcp": 63337, From 4c53b38c4c4c2d75b4576be06668c6228501fd23 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sat, 23 Nov 2024 00:03:25 +0100 Subject: [PATCH 07/51] Re-added lot's of handling. --- files/eufyp2pstream.py | 434 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 401 insertions(+), 33 deletions(-) diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 45dbd6e..d351cda 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -1,21 +1,278 @@ +import os +import signal +import sys +import threading +from aiohttp import ClientSession import asyncio import socket from http.server import BaseHTTPRequestHandler, HTTPServer from queue import Queue from threading import Thread +from typing import get_args from websocket import EufySecurityWebSocket import json +# Variables +camera_handlers = {} +run_event = threading.Event() + # Constants RECV_CHUNK_SIZE = 4096 +EVENT_CONFIGURATION: dict = { + "livestream video data": { + "name": "video_data", + "value": "buffer", + "type": "event", + }, + "livestream audio data": { + "name": "audio_data", + "value": "buffer", + "type": "event", + }, +} + +START_P2P_LIVESTREAM_MESSAGE = { + "messageId": "start_livestream", + "command": "device.start_livestream", + "serialNumber": None, +} + +STOP_P2P_LIVESTREAM_MESSAGE = { + "messageId": "stop_livestream", + "command": "device.stop_livestream", + "serialNumber": None, +} + +START_TALKBACK = { + "messageId": "start_talkback", + "command": "device.start_talkback", + "serialNumber": None, +} + +SEND_TALKBACK_AUDIO_DATA = { + "messageId": "talkback_audio_data", + "command": "device.talkback_audio_data", + "serialNumber": None, + "buffer": None +} + +STOP_TALKBACK = { + "messageId": "stop_talkback", + "command": "device.stop_talkback", + "serialNumber": None, +} + +SET_API_SCHEMA = { + "messageId": "set_api_schema", + "command": "set_api_schema", + "schemaVersion": 13, +} + + +P2P_LIVESTREAMING_STATUS = "p2pLiveStreamingStatus" + +START_LISTENING_MESSAGE = {"messageId": "start_listening", "command": "start_listening"} + +TALKBACK_RESULT_MESSAGE = {"messageId": "talkback_audio_data", "errorCode": "device_talkback_not_running"} + +DRIVER_CONNECT_MESSAGE = {"messageId": "driver_connect", "command": "driver.connect"} + + + +def exit_handler(signum, frame): + print(f'Signal handler called with signal {signum}') + run_event.set() + +# Install signal handler +signal.signal(signal.SIGINT, exit_handler) + + +def exit_handler(signum, frame): + print(f'Signal handler called with signal {signum}') + run_event.set() + +# Install signal handler +signal.signal(signal.SIGINT, exit_handler) + +class ClientAcceptThread(threading.Thread): + def __init__(self,socket,run_event,name,ws,serialno): + threading.Thread.__init__(self) + self.socket = socket + self.queues = [] + self.run_event = run_event + self.name = name + self.ws = ws + self.serialno = serialno + self.my_threads = [] + + def update_threads(self): + my_threads_before = len(self.my_threads) + for thread in self.my_threads: + if not thread.is_alive(): + self.queues.remove(thread.queue) + self.my_threads = [t for t in self.my_threads if t.is_alive()] + if self.ws and my_threads_before > 0 and len(self.my_threads) == 0: + if self.name == "BackChannel": + print("All clients died (BackChannel): ", self.name) + sys.stdout.flush() + else: + print("All clients died. Stopping Stream: ", self.name) + sys.stdout.flush() + + msg = STOP_P2P_LIVESTREAM_MESSAGE.copy() + msg["serialNumber"] = self.serialno + asyncio.run(self.ws.send_message(json.dumps(msg))) + + def run(self): + print("Accepting connection for ", self.name) + msg = STOP_TALKBACK.copy() + msg["serialNumber"] = self.serialno + asyncio.run(self.ws.send_message(json.dumps(msg))) + while not self.run_event.is_set(): + self.update_threads() + sys.stdout.flush() + try: + client_sock, client_addr = self.socket.accept() + print ("New connection added: ", client_addr, " for ", self.name) + sys.stdout.flush() + + if self.name == "BackChannel": + client_sock.setblocking(True) + print("Starting BackChannel") + thread = ClientRecvThread(client_sock, run_event, self.name, self.ws, self.serialno) + thread.start() + else: + client_sock.setblocking(False) + thread = ClientSendThread(client_sock, run_event, self.name, self.ws, self.serialno) + self.queues.append(thread.queue) + if self.ws: + msg = START_P2P_LIVESTREAM_MESSAGE.copy() + msg["serialNumber"] = self.serialno + asyncio.run(self.ws.send_message(json.dumps(msg))) + self.my_threads.append(thread) + thread.start() + except socket.timeout: + pass + +class ClientSendThread(threading.Thread): + def __init__(self,client_sock,run_event,name,ws,serialno): + threading.Thread.__init__(self) + self.client_sock = client_sock + self.queue = Queue(100) + self.run_event = run_event + self.name = name + self.ws = ws + self.serialno = serialno + + def run(self): + print ("Thread running: ", self.name) + sys.stdout.flush() + + try: + while not self.run_event.is_set(): + ready_to_read, ready_to_write, in_error = \ + select.select([], [self.client_sock], [self.client_sock], 2) + if len(in_error): + print("Exception in socket", self.name) + sys.stdout.flush() + break + if not len(ready_to_write): + print("Socket not ready to write ", self.name) + sys.stdout.flush() + break + if not self.queue.empty(): + self.client_sock.sendall( + bytearray(self.queue.get(True)["data"]) + ) + except socket.error as e: + print("Connection lost", self.name, e) + pass + except socket.timeout: + print("Timeout on socket for ", self.name) + pass + try: + self.client_sock.shutdown(socket.SHUT_RDWR) + except OSError: + print ("Error shutdown socket: ", self.name) + self.client_sock.close() + print ("Thread stopping: ", self.name) + sys.stdout.flush() + +class ClientRecvThread(threading.Thread): + def __init__(self,client_sock,run_event,name,ws,serialno): + threading.Thread.__init__(self) + self.client_sock = client_sock + self.run_event = run_event + self.name = name + self.ws = ws + self.serialno = serialno + + def run(self): + msg = START_TALKBACK.copy() + msg["serialNumber"] = self.serialno + asyncio.run(self.ws.send_message(json.dumps(msg))) + try: + curr_packet = bytearray() + no_data = 0 + while not self.run_event.is_set(): + try: + ready_to_read, ready_to_write, in_error = \ + select.select([self.client_sock,], [], [self.client_sock], 2) + if len(in_error): + print("Exception in socket", self.name) + sys.stdout.flush() + break + if len(ready_to_read): + data = self.client_sock.recv(RECV_CHUNK_SIZE) + curr_packet += bytearray(data) + if len(data) > 0: # and len(data) <= RECV_CHUNK_SIZE: + msg = SEND_TALKBACK_AUDIO_DATA.copy() + msg["serialNumber"] = self.serialno + msg["buffer"] = list(bytes(curr_packet)) + asyncio.run(self.ws.send_message(json.dumps(msg))) + curr_packet = bytearray() + no_data = 0 + else: + no_data += 1 + else: + no_data += 1 + if (no_data >= 15): + print("15x in a row no data in socket ", self.name) + sys.stdout.flush() + break + except BlockingIOError: + # Resource temporarily unavailable (errno EWOULDBLOCK) + pass + except socket.error as e: + print("Connection lost", self.name, e) + pass + except socket.timeout: + print("Timeout on socket for ", self.name) + pass + except select.error: + print("Select error on socket ", self.name) + pass + sys.stdout.flush() + try: + self.client_sock.shutdown(socket.SHUT_RDWR) + except OSError: + print ("Error shutdown socket: ", self.name) + sys.stdout.flush() + self.client_sock.close() + msg = STOP_TALKBACK.copy() + msg["serialNumber"] = self.serialno + asyncio.run(self.ws.send_message(json.dumps(msg))) + # Camera Stream Handler class CameraStreamHandler: - def __init__(self, serial_number, start_port): - print(f" - CameraStreamHandler - __init__ - serial_number: {serial_number} - start_port: {start_port}") + def __init__(self, serial_number, start_port, run_event): + print(f" - CameraStreamHandler - __init__ - serial_number: {serial_number} - video_port: {start_port} - audio_port: {start_port + 1} - backchannel_port: {start_port + 2}") self.serial_number = serial_number self.start_port = start_port + self.run_event = run_event + self.ws = None self.video_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.audio_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.backchannel_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -32,25 +289,44 @@ def __init__(self, serial_number, start_port): def setup_sockets(self): self.video_sock.bind(("0.0.0.0", self.start_port)) + self.video_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.video_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) self.audio_sock.bind(("0.0.0.0", self.start_port + 1)) + self.audio_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.audio_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) self.backchannel_sock.bind(("0.0.0.0", self.start_port + 2)) + self.backchannel_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.backchannel_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + self.backchannel_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) self.video_sock.listen(1) self.audio_sock.listen(1) self.backchannel_sock.listen(1) def start_stream(self, websocket): - asyncio.run(self._start_stream_async(websocket)) + self.video_thread = ClientAcceptThread(self.video_sock, self.run_event, "Video", self.ws, self.serial_number) + self.audio_thread = ClientAcceptThread(self.audio_sock, self.run_event, "Audio", self.ws, self.serial_number) + self.backchannel_thread = ClientAcceptThread(self.backchannel_sock, self.run_event, "BackChannel", self.ws, self.serial_number) + self.audio_thread.start() + self.video_thread.start() + self.backchannel_thread.start() + + def setWs(self, ws : EufySecurityWebSocket): + self.ws = ws + + # asyncio.run(self._start_stream_async(websocket)) async def _start_stream_async(self, websocket): await websocket.send(json.dumps(self.start_livestream_msg)) video_conn, _ = self.video_sock.accept() audio_conn, _ = self.audio_sock.accept() + backchannel_conn, _ = self.backchannel_sock.accept() while True: video_data = await websocket.recv() audio_data = await websocket.recv() video_conn.sendall(video_data) audio_conn.sendall(audio_data) + backchannel_conn.sendall(b"") # Send empty data to keep the backchannel alive def stop_stream(self, websocket): asyncio.run(self._stop_stream_async(websocket)) @@ -59,44 +335,136 @@ async def _stop_stream_async(self, websocket): await websocket.send(json.dumps(self.stop_livestream_msg)) self.video_sock.close() self.audio_sock.close() + self.backchannel_sock.close() -# Multi-Camera Manager -class MultiCameraManager: - def __init__(self, cameras, base_port): - self.cameras = cameras - self.base_port = base_port - self.stream_handlers = {} - def initialize_streams(self): - current_port = self.base_port - for serial in self.cameras: - handler = CameraStreamHandler(serial, current_port) - handler.setup_sockets() - self.stream_handlers[serial] = handler - current_port += 2 # Allocate two ports per camera - def start_all_streams(self, websocket): - for handler in self.stream_handlers.values(): - Thread(target=handler.start_stream, args=(websocket,)).start() +# On Open Callback +async def on_open(self): + print(f" on_open - executed") - def stop_all_streams(self, websocket): - for handler in self.stream_handlers.values(): - handler.stop_stream(websocket) +# On Close Callback +async def on_close(self): + print(f" on_close - executed") +# self.run_event.set() +# self.ws = None +# stop() +# os._exit(-1) +# On Error Callback +async def on_error(self, message): + print(f" on_error - executed - {message}") -if __name__ == "__main__": - # Example usage - CAMERA_SERIALS = ["CAM1_SERIAL", "CAM2_SERIAL", "CAM3_SERIAL"] - BASE_PORT = 8000 +# On Message Callback +async def on_message(self, message): + payload = message.json() + message_type: str = payload["type"] + if message_type == "result": + message_id = payload["messageId"] + if message_id != SEND_TALKBACK_AUDIO_DATA["messageId"]: + # Avoid spamming of TALKBACK_AUDIO_DATA logs + print(f"on_message result: {payload}") + sys.stdout.flush() + if message_id == START_LISTENING_MESSAGE["messageId"]: + # message_result = payload[message_type] + # states = message_result["state"] + # for state in states["devices"]: + # self.serialno = state["serialNumber"] + camera_handlers[self.serialno].start_stream(self.ws) + # self.video_thread = ClientAcceptThread(video_sock, run_event, "Video", self.ws, self.serialno) + # self.audio_thread = ClientAcceptThread(audio_sock, run_event, "Audio", self.ws, self.serialno) + # self.backchannel_thread = ClientAcceptThread(backchannel_sock, run_event, "BackChannel", self.ws, self.serialno) + # self.audio_thread.start() + # self.video_thread.start() + # self.backchannel_thread.start() + if message_id == TALKBACK_RESULT_MESSAGE["messageId"] and "errorCode" in payload: + error_code = payload["errorCode"] + print(f"Talkback error: {error_code}") + # if error_code == "device_talkback_not_running": + # msg = START_TALKBACK.copy() + # msg["serialNumber"] = self.serialno + # await self.ws.send_message(json.dumps(msg)) - manager = MultiCameraManager(CAMERA_SERIALS, BASE_PORT) - manager.initialize_streams() + if message_type == "event": + message = payload[message_type] + event_type = message["event"] + sys.stdout.flush() + if message["event"] == "livestream audio data": + print(f"on_audio - {payload}") + event_value = message[EVENT_CONFIGURATION[event_type]["value"]] + event_data_type = EVENT_CONFIGURATION[event_type]["type"] + if event_data_type == "event": + for queue in self.audio_thread.queues: + if queue.full(): + print("Audio queue full.") + queue.get(False) + queue.put(event_value) + if message["event"] == "livestream video data": + print(f"on_video - {payload}") + event_value = message[EVENT_CONFIGURATION[event_type]["value"]] + event_data_type = EVENT_CONFIGURATION[event_type]["type"] + if event_data_type == "event": + for queue in self.video_thread.queues: + if queue.full(): + print("Video queue full.") + queue.get(False) + queue.put(event_value) + if message["event"] == "livestream error": + print(f"Livestream Error! - {payload}") + if self.ws and len(self.video_thread.queues) > 0: + msg = START_P2P_LIVESTREAM_MESSAGE.copy() + msg["serialNumber"] = self.serialno + await self.ws.send_message(json.dumps(msg)) - # Assuming the WebSocket connection is established here - websocket = EufySecurityWebSocket() # Replace with actual WebSocket connection setup + +async def init_websocket(): + websocket = EufySecurityWebSocket( + "402f1039-eufy-security-ws", + ws_security_port, + ClientSession(), + on_open, + on_message, + on_close, + on_error, + ) + # Set the websocket for all camera handlers. + for handler in camera_handlers.values(): + handler.setWs(websocket) try: - manager.start_all_streams(websocket) - except KeyboardInterrupt: - manager.stop_all_streams(websocket) + await websocket.connect() + await websocket.send_message(json.dumps(START_LISTENING_MESSAGE)) + await websocket.send_message(json.dumps(SET_API_SCHEMA)) + await websocket.send_message(json.dumps(DRIVER_CONNECT_MESSAGE)) + while not run_event.is_set(): + await asyncio.sleep(1000) + except Exception as ex: + print(ex) + print("init_websocket failed. Exiting.") + os._exit(-1) + +if __name__ == "__main__": + # Get all arguments. + args = get_args() + print(f" - args: {args}") + # First argument is the WS Security port. + ws_security_port = args[1] + print(f" - ws_security_port: {ws_security_port}") + # The rest of the arguments are the camera serials. + camera_serials = args[2:] + + BASE_PORT = 63336 + + + # Create one Camera Stream Handler per camera. + for i, serial in enumerate(camera_serials): + handler = CameraStreamHandler(serial, BASE_PORT + i * 3) + handler.setup_sockets() + camera_handlers[serial] = handler + + # Loop forever. + loop = asyncio.get_event_loop() + loop.run_until_complete(init_websocket()) + + From b13848169a0ff7332d2bb98b4d29a10bba30cd38 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sat, 23 Nov 2024 00:03:46 +0100 Subject: [PATCH 08/51] Added config serial numbers to code. --- files/run.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/files/run.sh b/files/run.sh index 47d6b29..209edfe 100755 --- a/files/run.sh +++ b/files/run.sh @@ -7,7 +7,12 @@ echo "Config path is $CONFIG_PATH" echo "Config content is $(cat $CONFIG_PATH)" EUFY_WS_PORT=$(jq --raw-output ".eufy_security_ws_port" $CONFIG_PATH) +CAM1_SN=$(jq --raw-output ".camera_1_serial_number" $CONFIG_PATH) +CAM2_SN=$(jq --raw-output ".camera_2_serial_number" $CONFIG_PATH) +CAM3_SN=$(jq --raw-output ".camera_3_serial_number" $CONFIG_PATH) +CAM4_SN=$(jq --raw-output ".camera_4_serial_number" $CONFIG_PATH) +CAM5_SN=$(jq --raw-output ".camera_5_serial_number" $CONFIG_PATH) echo "Starting EufyP2PStream. eufy_security_ws_port is $EUFY_WS_PORT" -python3 -u /eufyp2pstream.py $EUFY_WS_PORT +python3 -u /eufyp2pstream.py $EUFY_WS_PORT $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN echo "Exited with code $?" \ No newline at end of file From 11d83241c3632b2ab40700d63b853c4ddf2a50be Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sat, 23 Nov 2024 00:04:08 +0100 Subject: [PATCH 09/51] Updated version. --- config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index e4b5917..fdff62a 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.2-beta +version: 0.2.3-beta slug: eufyp2pstream init: false startup: application From d364388acdccdd5ed69d08544bff26061b714f10 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sat, 23 Nov 2024 17:28:53 +0100 Subject: [PATCH 10/51] Corrected options. --- config.yaml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/config.yaml b/config.yaml index fdff62a..0947a00 100644 --- a/config.yaml +++ b/config.yaml @@ -14,18 +14,13 @@ map: [config, media] host_network: false options: eufy_security_ws_port: 3000 - camera_1_serial_number: "" - camera_2_serial_number: "" - camera_3_serial_number: "" - camera_4_serial_number: "" - camera_5_serial_number: "" schema: eufy_security_ws_port: port - camera_1_serial_number: string - camera_2_serial_number: string - camera_3_serial_number: string - camera_4_serial_number: string - camera_5_serial_number: string + camera_1_serial_number: "str?" + camera_2_serial_number: "str?" + camera_3_serial_number: "str?" + camera_4_serial_number: "str?" + camera_5_serial_number: "str?" ports: { "63336/tcp": 63336, "63337/tcp": 63337, From 0b00af50964a236f1955565143ab8876a4394192 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sat, 23 Nov 2024 17:53:32 +0100 Subject: [PATCH 11/51] Fixed command line argument parsing. --- config.yaml | 2 +- files/eufyp2pstream.py | 34 +++++++++++++++++++++------------- files/run.sh | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/config.yaml b/config.yaml index 0947a00..254f1fe 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.3-beta +version: 0.2.4-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index d351cda..22c8704 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -9,7 +9,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from queue import Queue from threading import Thread -from typing import get_args +import argparse from websocket import EufySecurityWebSocket import json @@ -445,18 +445,26 @@ async def init_websocket(): os._exit(-1) if __name__ == "__main__": - # Get all arguments. - args = get_args() - print(f" - args: {args}") - # First argument is the WS Security port. - ws_security_port = args[1] - print(f" - ws_security_port: {ws_security_port}") - # The rest of the arguments are the camera serials. - camera_serials = args[2:] - - BASE_PORT = 63336 - - + # Parse command-line arguments + parser = argparse.ArgumentParser(description="Stream video and audio from multiple Eufy cameras.") + parser.add_argument( + "--serials", + nargs="+", + required=True, + help="List of camera serial numbers (e.g., --serials CAM1_SERIAL CAM2_SERIAL).", + ) + parser.add_argument( + "--ws_security_port", + type=int, + default=3000, + help="Base port number for streaming (default: 3000).", + ) + args = parser.parse_args() + print(f"WS Security Port: {args.ws_security_port}") + print(f"Camera Serial Numbers: {args.serials}") + + # Define constants. + BASE_PORT = 63336 # Create one Camera Stream Handler per camera. for i, serial in enumerate(camera_serials): handler = CameraStreamHandler(serial, BASE_PORT + i * 3) diff --git a/files/run.sh b/files/run.sh index 209edfe..ebb199a 100755 --- a/files/run.sh +++ b/files/run.sh @@ -14,5 +14,5 @@ CAM4_SN=$(jq --raw-output ".camera_4_serial_number" $CONFIG_PATH) CAM5_SN=$(jq --raw-output ".camera_5_serial_number" $CONFIG_PATH) echo "Starting EufyP2PStream. eufy_security_ws_port is $EUFY_WS_PORT" -python3 -u /eufyp2pstream.py $EUFY_WS_PORT $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN +python3 -u /eufyp2pstream.py --ws_security_port $EUFY_WS_PORT --serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN echo "Exited with code $?" \ No newline at end of file From 0b2c72ee8b28b55403290d3a101e6b398b71e3fc Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sat, 23 Nov 2024 19:25:21 +0100 Subject: [PATCH 12/51] Corrected arguments --- config.yaml | 2 +- files/eufyp2pstream.py | 10 +++++----- files/run.sh | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config.yaml b/config.yaml index 254f1fe..0f954db 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.4-beta +version: 0.2.5-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 22c8704..5042711 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -418,7 +418,7 @@ async def on_message(self, message): await self.ws.send_message(json.dumps(msg)) -async def init_websocket(): +async def init_websocket(ws_security_port): websocket = EufySecurityWebSocket( "402f1039-eufy-security-ws", ws_security_port, @@ -448,7 +448,7 @@ async def init_websocket(): # Parse command-line arguments parser = argparse.ArgumentParser(description="Stream video and audio from multiple Eufy cameras.") parser.add_argument( - "--serials", + "--camera_serials", nargs="+", required=True, help="List of camera serial numbers (e.g., --serials CAM1_SERIAL CAM2_SERIAL).", @@ -461,18 +461,18 @@ async def init_websocket(): ) args = parser.parse_args() print(f"WS Security Port: {args.ws_security_port}") - print(f"Camera Serial Numbers: {args.serials}") + print(f"Camera Serial Numbers: {args.camera_serials}") # Define constants. BASE_PORT = 63336 # Create one Camera Stream Handler per camera. - for i, serial in enumerate(camera_serials): + for i, serial in enumerate(args.camera_serials): handler = CameraStreamHandler(serial, BASE_PORT + i * 3) handler.setup_sockets() camera_handlers[serial] = handler # Loop forever. loop = asyncio.get_event_loop() - loop.run_until_complete(init_websocket()) + loop.run_until_complete(init_websocket(args.ws_security_port)) diff --git a/files/run.sh b/files/run.sh index ebb199a..5eb6497 100755 --- a/files/run.sh +++ b/files/run.sh @@ -14,5 +14,5 @@ CAM4_SN=$(jq --raw-output ".camera_4_serial_number" $CONFIG_PATH) CAM5_SN=$(jq --raw-output ".camera_5_serial_number" $CONFIG_PATH) echo "Starting EufyP2PStream. eufy_security_ws_port is $EUFY_WS_PORT" -python3 -u /eufyp2pstream.py --ws_security_port $EUFY_WS_PORT --serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN +python3 -u /eufyp2pstream.py --ws_security_port $EUFY_WS_PORT --camera_serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN echo "Exited with code $?" \ No newline at end of file From 4698677c7659ca91b2380097763c346a30526cb2 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sat, 23 Nov 2024 23:43:49 +0100 Subject: [PATCH 13/51] Added missing run_event --- config.yaml | 2 +- files/eufyp2pstream.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.yaml b/config.yaml index 0f954db..12c12fa 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.5-beta +version: 0.2.6-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 5042711..7ec5d10 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -467,7 +467,7 @@ async def init_websocket(ws_security_port): BASE_PORT = 63336 # Create one Camera Stream Handler per camera. for i, serial in enumerate(args.camera_serials): - handler = CameraStreamHandler(serial, BASE_PORT + i * 3) + handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) handler.setup_sockets() camera_handlers[serial] = handler From 427b17e500d9e2902b98525b9c706655ff0e1b6f Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 00:02:56 +0100 Subject: [PATCH 14/51] Cleaned up init of cameras. --- config.yaml | 2 +- files/eufyp2pstream.py | 58 ++++++++++++++++++++++-------------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/config.yaml b/config.yaml index 12c12fa..9ded411 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.6-beta +version: 0.2.7-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 7ec5d10..b78e7a0 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -341,11 +341,11 @@ async def _stop_stream_async(self, websocket): # On Open Callback -async def on_open(self): +async def on_open(): print(f" on_open - executed") # On Close Callback -async def on_close(self): +async def on_close(): print(f" on_close - executed") # self.run_event.set() # self.ws = None @@ -353,11 +353,11 @@ async def on_close(self): # os._exit(-1) # On Error Callback -async def on_error(self, message): +async def on_error(message): print(f" on_error - executed - {message}") # On Message Callback -async def on_message(self, message): +async def on_message(message): payload = message.json() message_type: str = payload["type"] if message_type == "result": @@ -367,11 +367,12 @@ async def on_message(self, message): print(f"on_message result: {payload}") sys.stdout.flush() if message_id == START_LISTENING_MESSAGE["messageId"]: + print(f"Listening started: {payload}") # message_result = payload[message_type] # states = message_result["state"] # for state in states["devices"]: # self.serialno = state["serialNumber"] - camera_handlers[self.serialno].start_stream(self.ws) +# camera_handlers[self.serialno].start_stream(self.ws) # self.video_thread = ClientAcceptThread(video_sock, run_event, "Video", self.ws, self.serialno) # self.audio_thread = ClientAcceptThread(audio_sock, run_event, "Audio", self.ws, self.serialno) # self.backchannel_thread = ClientAcceptThread(backchannel_sock, run_event, "BackChannel", self.ws, self.serialno) @@ -392,30 +393,30 @@ async def on_message(self, message): sys.stdout.flush() if message["event"] == "livestream audio data": print(f"on_audio - {payload}") - event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - event_data_type = EVENT_CONFIGURATION[event_type]["type"] - if event_data_type == "event": - for queue in self.audio_thread.queues: - if queue.full(): - print("Audio queue full.") - queue.get(False) - queue.put(event_value) + # event_value = message[EVENT_CONFIGURATION[event_type]["value"]] + # event_data_type = EVENT_CONFIGURATION[event_type]["type"] + # if event_data_type == "event": + # for queue in self.audio_thread.queues: + # if queue.full(): + # print("Audio queue full.") + # queue.get(False) + # queue.put(event_value) if message["event"] == "livestream video data": print(f"on_video - {payload}") - event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - event_data_type = EVENT_CONFIGURATION[event_type]["type"] - if event_data_type == "event": - for queue in self.video_thread.queues: - if queue.full(): - print("Video queue full.") - queue.get(False) - queue.put(event_value) + # event_value = message[EVENT_CONFIGURATION[event_type]["value"]] + # event_data_type = EVENT_CONFIGURATION[event_type]["type"] + # if event_data_type == "event": + # for queue in self.video_thread.queues: + # if queue.full(): + # print("Video queue full.") + # queue.get(False) + # queue.put(event_value) if message["event"] == "livestream error": print(f"Livestream Error! - {payload}") - if self.ws and len(self.video_thread.queues) > 0: - msg = START_P2P_LIVESTREAM_MESSAGE.copy() - msg["serialNumber"] = self.serialno - await self.ws.send_message(json.dumps(msg)) + # if self.ws and len(self.video_thread.queues) > 0: + # msg = START_P2P_LIVESTREAM_MESSAGE.copy() + # msg["serialNumber"] = self.serialno + # await self.ws.send_message(json.dumps(msg)) async def init_websocket(ws_security_port): @@ -467,9 +468,10 @@ async def init_websocket(ws_security_port): BASE_PORT = 63336 # Create one Camera Stream Handler per camera. for i, serial in enumerate(args.camera_serials): - handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) - handler.setup_sockets() - camera_handlers[serial] = handler + if serial != None: + handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) + handler.setup_sockets() + camera_handlers[serial] = handler # Loop forever. loop = asyncio.get_event_loop() From 04737bdcbda04bc45d18b58bd5f1221c263eee9d Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 00:15:18 +0100 Subject: [PATCH 15/51] Started streams --- config.yaml | 2 +- files/eufyp2pstream.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config.yaml b/config.yaml index 9ded411..54a609e 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.7-beta +version: 0.2.8-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index b78e7a0..7fb5d3c 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -302,7 +302,7 @@ def setup_sockets(self): self.audio_sock.listen(1) self.backchannel_sock.listen(1) - def start_stream(self, websocket): + def start_stream(self): self.video_thread = ClientAcceptThread(self.video_sock, self.run_event, "Video", self.ws, self.serial_number) self.audio_thread = ClientAcceptThread(self.audio_sock, self.run_event, "Audio", self.ws, self.serial_number) self.backchannel_thread = ClientAcceptThread(self.backchannel_sock, self.run_event, "BackChannel", self.ws, self.serial_number) @@ -368,11 +368,11 @@ async def on_message(message): sys.stdout.flush() if message_id == START_LISTENING_MESSAGE["messageId"]: print(f"Listening started: {payload}") - # message_result = payload[message_type] - # states = message_result["state"] - # for state in states["devices"]: - # self.serialno = state["serialNumber"] -# camera_handlers[self.serialno].start_stream(self.ws) + message_result = payload[message_type] + states = message_result["state"] + for state in states["devices"]: + # self.serialno = state["serialNumber"] + camera_handlers[state["serialNumber"]].start_stream() # self.video_thread = ClientAcceptThread(video_sock, run_event, "Video", self.ws, self.serialno) # self.audio_thread = ClientAcceptThread(audio_sock, run_event, "Audio", self.ws, self.serialno) # self.backchannel_thread = ClientAcceptThread(backchannel_sock, run_event, "BackChannel", self.ws, self.serialno) From 150ff0ea2b9b9ce46f9a027a2e5ec084e3d10d3d Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 00:57:31 +0100 Subject: [PATCH 16/51] Don't start cameras that are not defined. --- config.yaml | 2 +- files/eufyp2pstream.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/config.yaml b/config.yaml index 54a609e..d56f631 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.8-beta +version: 0.2.9-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 7fb5d3c..839e8ee 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -371,8 +371,12 @@ async def on_message(message): message_result = payload[message_type] states = message_result["state"] for state in states["devices"]: - # self.serialno = state["serialNumber"] - camera_handlers[state["serialNumber"]].start_stream() + serialno = state["serialNumber"] + if serialno in camera_handlers: + camera_handlers[serialno].start_stream() + else: + print(f"Found unknown Eufy camera with serial number {serialno}.") + # self.video_thread = ClientAcceptThread(video_sock, run_event, "Video", self.ws, self.serialno) # self.audio_thread = ClientAcceptThread(audio_sock, run_event, "Audio", self.ws, self.serialno) # self.backchannel_thread = ClientAcceptThread(backchannel_sock, run_event, "BackChannel", self.ws, self.serialno) @@ -468,7 +472,7 @@ async def init_websocket(ws_security_port): BASE_PORT = 63336 # Create one Camera Stream Handler per camera. for i, serial in enumerate(args.camera_serials): - if serial != None: + if serial != 'null': handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) handler.setup_sockets() camera_handlers[serial] = handler From c1dd7d19279ccf2b94a9c20b9d59b9b3531594e5 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 10:20:02 +0100 Subject: [PATCH 17/51] Blacked --- config.yaml | 2 +- files/websocket.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/config.yaml b/config.yaml index d56f631..02f226b 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.9-beta +version: 0.2.10-beta slug: eufyp2pstream init: false startup: application diff --git a/files/websocket.py b/files/websocket.py index 9341a4e..1dc2790 100644 --- a/files/websocket.py +++ b/files/websocket.py @@ -73,13 +73,11 @@ def on_error(self, error: Text = "Unspecified") -> None: def on_close(self, future="") -> None: print(f" - WebSocket Connection Closed. %s", future) - print( - f" - WebSocket Connection Closed. %s", self.close_callback - ) + print(f" - WebSocket Connection Closed. %s", self.close_callback) if self.close_callback is not None: self.ws = None asyncio.run_coroutine_threadsafe(self.close_callback(), self.loop) async def send_message(self, message): - #print(f" - WebSocket message sent. %s", message) + # print(f" - WebSocket message sent. %s", message) await self.ws.send_str(message) From 9f9f19b33a6841b595808aa668226a31e37b9669 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 10:20:10 +0100 Subject: [PATCH 18/51] Added missing select import. --- files/eufyp2pstream.py | 124 +++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 43 deletions(-) diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 839e8ee..c7397ea 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -1,10 +1,10 @@ - import os import signal import sys import threading from aiohttp import ClientSession import asyncio +import select import socket from http.server import BaseHTTPRequestHandler, HTTPServer from queue import Queue @@ -55,7 +55,7 @@ "messageId": "talkback_audio_data", "command": "device.talkback_audio_data", "serialNumber": None, - "buffer": None + "buffer": None, } STOP_TALKBACK = { @@ -75,29 +75,34 @@ START_LISTENING_MESSAGE = {"messageId": "start_listening", "command": "start_listening"} -TALKBACK_RESULT_MESSAGE = {"messageId": "talkback_audio_data", "errorCode": "device_talkback_not_running"} +TALKBACK_RESULT_MESSAGE = { + "messageId": "talkback_audio_data", + "errorCode": "device_talkback_not_running", +} DRIVER_CONNECT_MESSAGE = {"messageId": "driver_connect", "command": "driver.connect"} - def exit_handler(signum, frame): - print(f'Signal handler called with signal {signum}') + print(f"Signal handler called with signal {signum}") run_event.set() + # Install signal handler signal.signal(signal.SIGINT, exit_handler) def exit_handler(signum, frame): - print(f'Signal handler called with signal {signum}') + print(f"Signal handler called with signal {signum}") run_event.set() + # Install signal handler signal.signal(signal.SIGINT, exit_handler) + class ClientAcceptThread(threading.Thread): - def __init__(self,socket,run_event,name,ws,serialno): + def __init__(self, socket, run_event, name, ws, serialno): threading.Thread.__init__(self) self.socket = socket self.queues = [] @@ -135,17 +140,21 @@ def run(self): sys.stdout.flush() try: client_sock, client_addr = self.socket.accept() - print ("New connection added: ", client_addr, " for ", self.name) + print("New connection added: ", client_addr, " for ", self.name) sys.stdout.flush() if self.name == "BackChannel": client_sock.setblocking(True) print("Starting BackChannel") - thread = ClientRecvThread(client_sock, run_event, self.name, self.ws, self.serialno) + thread = ClientRecvThread( + client_sock, run_event, self.name, self.ws, self.serialno + ) thread.start() else: client_sock.setblocking(False) - thread = ClientSendThread(client_sock, run_event, self.name, self.ws, self.serialno) + thread = ClientSendThread( + client_sock, run_event, self.name, self.ws, self.serialno + ) self.queues.append(thread.queue) if self.ws: msg = START_P2P_LIVESTREAM_MESSAGE.copy() @@ -156,8 +165,9 @@ def run(self): except socket.timeout: pass + class ClientSendThread(threading.Thread): - def __init__(self,client_sock,run_event,name,ws,serialno): + def __init__(self, client_sock, run_event, name, ws, serialno): threading.Thread.__init__(self) self.client_sock = client_sock self.queue = Queue(100) @@ -167,13 +177,14 @@ def __init__(self,client_sock,run_event,name,ws,serialno): self.serialno = serialno def run(self): - print ("Thread running: ", self.name) + print("Thread running: ", self.name) sys.stdout.flush() try: while not self.run_event.is_set(): - ready_to_read, ready_to_write, in_error = \ - select.select([], [self.client_sock], [self.client_sock], 2) + ready_to_read, ready_to_write, in_error = select.select( + [], [self.client_sock], [self.client_sock], 2 + ) if len(in_error): print("Exception in socket", self.name) sys.stdout.flush() @@ -183,9 +194,7 @@ def run(self): sys.stdout.flush() break if not self.queue.empty(): - self.client_sock.sendall( - bytearray(self.queue.get(True)["data"]) - ) + self.client_sock.sendall(bytearray(self.queue.get(True)["data"])) except socket.error as e: print("Connection lost", self.name, e) pass @@ -195,13 +204,14 @@ def run(self): try: self.client_sock.shutdown(socket.SHUT_RDWR) except OSError: - print ("Error shutdown socket: ", self.name) + print("Error shutdown socket: ", self.name) self.client_sock.close() - print ("Thread stopping: ", self.name) + print("Thread stopping: ", self.name) sys.stdout.flush() + class ClientRecvThread(threading.Thread): - def __init__(self,client_sock,run_event,name,ws,serialno): + def __init__(self, client_sock, run_event, name, ws, serialno): threading.Thread.__init__(self) self.client_sock = client_sock self.run_event = run_event @@ -214,12 +224,18 @@ def run(self): msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) try: - curr_packet = bytearray() + curr_packet = bytearray() no_data = 0 while not self.run_event.is_set(): try: - ready_to_read, ready_to_write, in_error = \ - select.select([self.client_sock,], [], [self.client_sock], 2) + ready_to_read, ready_to_write, in_error = select.select( + [ + self.client_sock, + ], + [], + [self.client_sock], + 2, + ) if len(in_error): print("Exception in socket", self.name) sys.stdout.flush() @@ -227,18 +243,18 @@ def run(self): if len(ready_to_read): data = self.client_sock.recv(RECV_CHUNK_SIZE) curr_packet += bytearray(data) - if len(data) > 0: # and len(data) <= RECV_CHUNK_SIZE: + if len(data) > 0: # and len(data) <= RECV_CHUNK_SIZE: msg = SEND_TALKBACK_AUDIO_DATA.copy() msg["serialNumber"] = self.serialno - msg["buffer"] = list(bytes(curr_packet)) + msg["buffer"] = list(bytes(curr_packet)) asyncio.run(self.ws.send_message(json.dumps(msg))) - curr_packet = bytearray() + curr_packet = bytearray() no_data = 0 else: no_data += 1 else: no_data += 1 - if (no_data >= 15): + if no_data >= 15: print("15x in a row no data in socket ", self.name) sys.stdout.flush() break @@ -258,17 +274,20 @@ def run(self): try: self.client_sock.shutdown(socket.SHUT_RDWR) except OSError: - print ("Error shutdown socket: ", self.name) + print("Error shutdown socket: ", self.name) sys.stdout.flush() self.client_sock.close() msg = STOP_TALKBACK.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) + # Camera Stream Handler class CameraStreamHandler: def __init__(self, serial_number, start_port, run_event): - print(f" - CameraStreamHandler - __init__ - serial_number: {serial_number} - video_port: {start_port} - audio_port: {start_port + 1} - backchannel_port: {start_port + 2}") + print( + f" - CameraStreamHandler - __init__ - serial_number: {serial_number} - video_port: {start_port} - audio_port: {start_port + 1} - backchannel_port: {start_port + 2}" + ) self.serial_number = serial_number self.start_port = start_port self.run_event = run_event @@ -303,14 +322,24 @@ def setup_sockets(self): self.backchannel_sock.listen(1) def start_stream(self): - self.video_thread = ClientAcceptThread(self.video_sock, self.run_event, "Video", self.ws, self.serial_number) - self.audio_thread = ClientAcceptThread(self.audio_sock, self.run_event, "Audio", self.ws, self.serial_number) - self.backchannel_thread = ClientAcceptThread(self.backchannel_sock, self.run_event, "BackChannel", self.ws, self.serial_number) + self.video_thread = ClientAcceptThread( + self.video_sock, self.run_event, "Video", self.ws, self.serial_number + ) + self.audio_thread = ClientAcceptThread( + self.audio_sock, self.run_event, "Audio", self.ws, self.serial_number + ) + self.backchannel_thread = ClientAcceptThread( + self.backchannel_sock, + self.run_event, + "BackChannel", + self.ws, + self.serial_number, + ) self.audio_thread.start() self.video_thread.start() self.backchannel_thread.start() - def setWs(self, ws : EufySecurityWebSocket): + def setWs(self, ws: EufySecurityWebSocket): self.ws = ws # asyncio.run(self._start_stream_async(websocket)) @@ -326,7 +355,9 @@ async def _start_stream_async(self, websocket): audio_data = await websocket.recv() video_conn.sendall(video_data) audio_conn.sendall(audio_data) - backchannel_conn.sendall(b"") # Send empty data to keep the backchannel alive + backchannel_conn.sendall( + b"" + ) # Send empty data to keep the backchannel alive def stop_stream(self, websocket): asyncio.run(self._stop_stream_async(websocket)) @@ -338,24 +369,27 @@ async def _stop_stream_async(self, websocket): self.backchannel_sock.close() - - # On Open Callback async def on_open(): print(f" on_open - executed") + # On Close Callback async def on_close(): print(f" on_close - executed") + + # self.run_event.set() # self.ws = None # stop() # os._exit(-1) + # On Error Callback async def on_error(message): print(f" on_error - executed - {message}") + # On Message Callback async def on_message(message): payload = message.json() @@ -383,7 +417,10 @@ async def on_message(message): # self.audio_thread.start() # self.video_thread.start() # self.backchannel_thread.start() - if message_id == TALKBACK_RESULT_MESSAGE["messageId"] and "errorCode" in payload: + if ( + message_id == TALKBACK_RESULT_MESSAGE["messageId"] + and "errorCode" in payload + ): error_code = payload["errorCode"] print(f"Talkback error: {error_code}") # if error_code == "device_talkback_not_running": @@ -405,7 +442,7 @@ async def on_message(message): # print("Audio queue full.") # queue.get(False) # queue.put(event_value) - if message["event"] == "livestream video data": + if message["event"] == "livestream video data": print(f"on_video - {payload}") # event_value = message[EVENT_CONFIGURATION[event_type]["value"]] # event_data_type = EVENT_CONFIGURATION[event_type]["type"] @@ -449,9 +486,12 @@ async def init_websocket(ws_security_port): print("init_websocket failed. Exiting.") os._exit(-1) + if __name__ == "__main__": # Parse command-line arguments - parser = argparse.ArgumentParser(description="Stream video and audio from multiple Eufy cameras.") + parser = argparse.ArgumentParser( + description="Stream video and audio from multiple Eufy cameras." + ) parser.add_argument( "--camera_serials", nargs="+", @@ -469,10 +509,10 @@ async def init_websocket(ws_security_port): print(f"Camera Serial Numbers: {args.camera_serials}") # Define constants. - BASE_PORT = 63336 + BASE_PORT = 63336 # Create one Camera Stream Handler per camera. for i, serial in enumerate(args.camera_serials): - if serial != 'null': + if serial != "null": handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) handler.setup_sockets() camera_handlers[serial] = handler @@ -480,5 +520,3 @@ async def init_websocket(ws_security_port): # Loop forever. loop = asyncio.get_event_loop() loop.run_until_complete(init_websocket(args.ws_security_port)) - - From cabe813247a7bc673dcc27bd4bdb24aff70b547e Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 10:25:40 +0100 Subject: [PATCH 19/51] Moved messages a bit to clear up. --- files/eufyp2pstream.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index c7397ea..5bc836a 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -424,35 +424,40 @@ async def on_message(message): error_code = payload["errorCode"] print(f"Talkback error: {error_code}") # if error_code == "device_talkback_not_running": - # msg = START_TALKBACK.copy() - # msg["serialNumber"] = self.serialno - # await self.ws.send_message(json.dumps(msg)) + # msg = START_TALKBACK.copy() + # msg["serialNumber"] = self.serialno + # await self.ws.send_message(json.dumps(msg)) if message_type == "event": message = payload[message_type] event_type = message["event"] sys.stdout.flush() if message["event"] == "livestream audio data": - print(f"on_audio - {payload}") - # event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - # event_data_type = EVENT_CONFIGURATION[event_type]["type"] - # if event_data_type == "event": + # print(f"on_audio - {payload}") + event_value = message[EVENT_CONFIGURATION[event_type]["value"]] + event_data_type = EVENT_CONFIGURATION[event_type]["type"] + if event_data_type == "event": + print(f"##################################################################") + print(f"on_audio - {payload}") + # for queue in self.audio_thread.queues: # if queue.full(): # print("Audio queue full.") # queue.get(False) # queue.put(event_value) if message["event"] == "livestream video data": - print(f"on_video - {payload}") - # event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - # event_data_type = EVENT_CONFIGURATION[event_type]["type"] - # if event_data_type == "event": - # for queue in self.video_thread.queues: + event_value = message[EVENT_CONFIGURATION[event_type]["value"]] + event_data_type = EVENT_CONFIGURATION[event_type]["type"] + if event_data_type == "event": + print(f"##################################################################") + print(f"on_video - {payload}") + # for queue in self.video_thread.queues: # if queue.full(): # print("Video queue full.") # queue.get(False) # queue.put(event_value) if message["event"] == "livestream error": + print(f"##################################################################") print(f"Livestream Error! - {payload}") # if self.ws and len(self.video_thread.queues) > 0: # msg = START_P2P_LIVESTREAM_MESSAGE.copy() From 4b2c2f5015df0e8261b95212f2430c186c57aa41 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 10:38:55 +0100 Subject: [PATCH 20/51] Added queing of audio/video data messages. --- config.yaml | 2 +- files/eufyp2pstream.py | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/config.yaml b/config.yaml index 02f226b..3884e4f 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.10-beta +version: 0.2.11-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 5bc836a..f2f08be 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -438,8 +438,14 @@ async def on_message(message): event_data_type = EVENT_CONFIGURATION[event_type]["type"] if event_data_type == "event": print(f"##################################################################") - print(f"on_audio - {payload}") - + print(f"on_audio - {payload['source']['serialNumber']}") + serialno = payload["source"]["serialNumber"] + if serialno in camera_handlers: + for queue in camera_handlers[serialno].audio_thread.queues: + if queue.full(): + print("Audio queue full.") + queue.get(False) + queue.put(event_value) # for queue in self.audio_thread.queues: # if queue.full(): # print("Audio queue full.") @@ -450,7 +456,14 @@ async def on_message(message): event_data_type = EVENT_CONFIGURATION[event_type]["type"] if event_data_type == "event": print(f"##################################################################") - print(f"on_video - {payload}") + print(f"on_video - {payload['source']['serialNumber']}") + serialno = payload["source"]["serialNumber"] + if serialno in camera_handlers: + for queue in camera_handlers[serialno].video_thread.queues: + if queue.full(): + print("Video queue full.") + queue.get(False) + queue.put(event_value) # for queue in self.video_thread.queues: # if queue.full(): # print("Video queue full.") From fb3dbd78563281b2db10e94d6e0bbf4f37d68f85 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 10:42:22 +0100 Subject: [PATCH 21/51] Added async to start_stream. --- files/eufyp2pstream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index f2f08be..228950f 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -321,7 +321,7 @@ def setup_sockets(self): self.audio_sock.listen(1) self.backchannel_sock.listen(1) - def start_stream(self): + async def start_stream(self): self.video_thread = ClientAcceptThread( self.video_sock, self.run_event, "Video", self.ws, self.serial_number ) From 32453e808a4a0561128e3f05a7711beb28a18299 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 10:59:19 +0100 Subject: [PATCH 22/51] Added debug printout. start_stream seems to be blocking. --- config.yaml | 2 +- files/eufyp2pstream.py | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/config.yaml b/config.yaml index 3884e4f..c82eef6 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.11-beta +version: 0.2.12-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 228950f..7ffa709 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -283,8 +283,9 @@ def run(self): # Camera Stream Handler -class CameraStreamHandler: +class CameraStreamHandler(): def __init__(self, serial_number, start_port, run_event): + # threading.Thread.__init__(self) print( f" - CameraStreamHandler - __init__ - serial_number: {serial_number} - video_port: {start_port} - audio_port: {start_port + 1} - backchannel_port: {start_port + 2}" ) @@ -305,6 +306,26 @@ def __init__(self, serial_number, start_port, run_event): "command": "device.stop_livestream", "serialNumber": self.serial_number, } + # def run(self): + # # Main logic of the handler goes here + # while self.run_event.is_set(): + # # Example of handling sockets + # try: + # # Your socket handling code here + # pass + # except select.error: + # print("Select error on socket ", self.name) + # pass + # sys.stdout.flush() + # try: + # self.client_sock.shutdown(socket.SHUT_RDWR) + # except OSError: + # print("Error shutdown socket: ", self.name) + # sys.stdout.flush() + # self.client_sock.close() + # msg = STOP_TALKBACK.copy() + # msg["serialNumber"] = self.serialno + # asyncio.run(self.ws.send_message(json.dumps(msg))) def setup_sockets(self): self.video_sock.bind(("0.0.0.0", self.start_port)) @@ -321,13 +342,16 @@ def setup_sockets(self): self.audio_sock.listen(1) self.backchannel_sock.listen(1) - async def start_stream(self): + def start_stream(self): + print(f"Starting stream for camera {self.serial_number}.") self.video_thread = ClientAcceptThread( self.video_sock, self.run_event, "Video", self.ws, self.serial_number ) + print(f"Video thread setup.") self.audio_thread = ClientAcceptThread( self.audio_sock, self.run_event, "Audio", self.ws, self.serial_number ) + print(f"Audio thread setup.") self.backchannel_thread = ClientAcceptThread( self.backchannel_sock, self.run_event, @@ -335,9 +359,13 @@ async def start_stream(self): self.ws, self.serial_number, ) + print(f"Backchannel thread setup.") self.audio_thread.start() + print("Audio thread started.") self.video_thread.start() + print("Video thread started.") self.backchannel_thread.start() + print("Backchannel thread started.") def setWs(self, ws: EufySecurityWebSocket): self.ws = ws @@ -401,13 +429,14 @@ async def on_message(message): print(f"on_message result: {payload}") sys.stdout.flush() if message_id == START_LISTENING_MESSAGE["messageId"]: - print(f"Listening started: {payload}") + # print(f"Listening started: {payload}") message_result = payload[message_type] states = message_result["state"] for state in states["devices"]: serialno = state["serialNumber"] if serialno in camera_handlers: camera_handlers[serialno].start_stream() + print(f"Started stream for camera {serialno}.") else: print(f"Found unknown Eufy camera with serial number {serialno}.") From a401e47567e0194effa9905e781f9a1e6ef1ca47 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 11:20:35 +0100 Subject: [PATCH 23/51] Added more debugging. --- config.yaml | 2 +- files/eufyp2pstream.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.yaml b/config.yaml index c82eef6..4959965 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.12-beta +version: 0.2.13-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 7ffa709..02cdcc4 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -135,6 +135,7 @@ def run(self): msg = STOP_TALKBACK.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) + print("stop talkback sent for ", self.serialno) while not self.run_event.is_set(): self.update_threads() sys.stdout.flush() @@ -164,6 +165,7 @@ def run(self): thread.start() except socket.timeout: pass + print("ClientAcceptThread ended for ", self.serialno) class ClientSendThread(threading.Thread): @@ -370,8 +372,6 @@ def start_stream(self): def setWs(self, ws: EufySecurityWebSocket): self.ws = ws - # asyncio.run(self._start_stream_async(websocket)) - async def _start_stream_async(self, websocket): await websocket.send(json.dumps(self.start_livestream_msg)) video_conn, _ = self.video_sock.accept() From 7702ae44fd079d996ceb7333c635c527b7b0899c Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 11:32:14 +0100 Subject: [PATCH 24/51] Added more debugging. --- config.yaml | 2 +- files/eufyp2pstream.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/config.yaml b/config.yaml index 4959965..099666f 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.13-beta +version: 0.2.14-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 02cdcc4..3d1f12a 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -113,6 +113,7 @@ def __init__(self, socket, run_event, name, ws, serialno): self.my_threads = [] def update_threads(self): + print("Updating ", self.name, " threads for ", self.serialno) my_threads_before = len(self.my_threads) for thread in self.my_threads: if not thread.is_alive(): @@ -129,15 +130,18 @@ def update_threads(self): msg = STOP_P2P_LIVESTREAM_MESSAGE.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) + print("Done updating ", self.name, " threads for ", self.serialno) def run(self): - print("Accepting connection for ", self.name) + print("Accepting ", self.name, " connection for ", self.serialno) msg = STOP_TALKBACK.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) print("stop talkback sent for ", self.serialno) while not self.run_event.is_set(): + print("Updaing threads for ", self.name) self.update_threads() + print("Waiting for connection for ", self.name) sys.stdout.flush() try: client_sock, client_addr = self.socket.accept() @@ -164,8 +168,9 @@ def run(self): self.my_threads.append(thread) thread.start() except socket.timeout: + print("socket timeout for ", self.serialno) pass - print("ClientAcceptThread ended for ", self.serialno) + print("ClientAcceptThread ", self.name, " ended for ", self.serialno) class ClientSendThread(threading.Thread): From 9f06811fdd78fe192d7a6266764cf7e6b0c17902 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 11:41:53 +0100 Subject: [PATCH 25/51] Added missing socket timeout. --- config.yaml | 2 +- files/eufyp2pstream.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/config.yaml b/config.yaml index 099666f..830e67d 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.14-beta +version: 0.2.15-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 3d1f12a..975ea86 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -141,8 +141,9 @@ def run(self): while not self.run_event.is_set(): print("Updaing threads for ", self.name) self.update_threads() - print("Waiting for connection for ", self.name) + print("Waiting for ", self.name, " connection for ", self.serialno) sys.stdout.flush() + print("Flush done for ", self.serialno) try: client_sock, client_addr = self.socket.accept() print("New connection added: ", client_addr, " for ", self.name) @@ -338,16 +339,19 @@ def setup_sockets(self): self.video_sock.bind(("0.0.0.0", self.start_port)) self.video_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.video_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.video_sock.settimeout(1) # timeout for listening self.audio_sock.bind(("0.0.0.0", self.start_port + 1)) self.audio_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.audio_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.audio_sock.settimeout(1) # timeout for listening self.backchannel_sock.bind(("0.0.0.0", self.start_port + 2)) self.backchannel_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.backchannel_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) self.backchannel_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - self.video_sock.listen(1) - self.audio_sock.listen(1) - self.backchannel_sock.listen(1) + self.backchannel_sock.settimeout(1) # timeout for listening + self.video_sock.listen() + self.audio_sock.listen() + self.backchannel_sock.listen() def start_stream(self): print(f"Starting stream for camera {self.serial_number}.") From df7a7bd3feb82d0f8ae5c6f05e4c457ab663bcaa Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 11:54:18 +0100 Subject: [PATCH 26/51] Added debugging to find serialnumber for events. --- config.yaml | 2 +- files/eufyp2pstream.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config.yaml b/config.yaml index 830e67d..42af663 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.15-beta +version: 0.2.16-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 975ea86..ad477dd 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -471,12 +471,12 @@ async def on_message(message): event_type = message["event"] sys.stdout.flush() if message["event"] == "livestream audio data": - # print(f"on_audio - {payload}") + print(f"on_audio - {message}") event_value = message[EVENT_CONFIGURATION[event_type]["value"]] event_data_type = EVENT_CONFIGURATION[event_type]["type"] if event_data_type == "event": print(f"##################################################################") - print(f"on_audio - {payload['source']['serialNumber']}") + print(f"on_audio - {payload['event']['serialNumber']}") serialno = payload["source"]["serialNumber"] if serialno in camera_handlers: for queue in camera_handlers[serialno].audio_thread.queues: @@ -495,7 +495,7 @@ async def on_message(message): if event_data_type == "event": print(f"##################################################################") print(f"on_video - {payload['source']['serialNumber']}") - serialno = payload["source"]["serialNumber"] + serialno = payload['event']["source"]["serialNumber"] if serialno in camera_handlers: for queue in camera_handlers[serialno].video_thread.queues: if queue.full(): From d856033e1f72568ffb1da682b0748aedd0e3ea4f Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 12:04:22 +0100 Subject: [PATCH 27/51] Corrected serialNumber extraction. --- config.yaml | 2 +- files/eufyp2pstream.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config.yaml b/config.yaml index 42af663..3dfffdb 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.16-beta +version: 0.2.17-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index ad477dd..7d2a6ed 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -471,13 +471,13 @@ async def on_message(message): event_type = message["event"] sys.stdout.flush() if message["event"] == "livestream audio data": - print(f"on_audio - {message}") + # print(f"on_audio - {message}") event_value = message[EVENT_CONFIGURATION[event_type]["value"]] event_data_type = EVENT_CONFIGURATION[event_type]["type"] if event_data_type == "event": print(f"##################################################################") - print(f"on_audio - {payload['event']['serialNumber']}") - serialno = payload["source"]["serialNumber"] + print(f"on_audio - {message['serialNumber']}") + serialno = message['serialNumber'] if serialno in camera_handlers: for queue in camera_handlers[serialno].audio_thread.queues: if queue.full(): @@ -494,8 +494,8 @@ async def on_message(message): event_data_type = EVENT_CONFIGURATION[event_type]["type"] if event_data_type == "event": print(f"##################################################################") - print(f"on_video - {payload['source']['serialNumber']}") - serialno = payload['event']["source"]["serialNumber"] + print(f"on_video - {message['serialNumber']}") + serialno = message['serialNumber'] if serialno in camera_handlers: for queue in camera_handlers[serialno].video_thread.queues: if queue.full(): From 2fd0fb6513dc71b3d9e236ced25554834faf250d Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 12:25:52 +0100 Subject: [PATCH 28/51] More debugging. --- config.yaml | 2 +- files/eufyp2pstream.py | 26 +++----------------------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/config.yaml b/config.yaml index 3dfffdb..ddefe78 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.17-beta +version: 0.2.18-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 7d2a6ed..ca88e49 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -169,7 +169,6 @@ def run(self): self.my_threads.append(thread) thread.start() except socket.timeout: - print("socket timeout for ", self.serialno) pass print("ClientAcceptThread ", self.name, " ended for ", self.serialno) @@ -293,7 +292,6 @@ def run(self): # Camera Stream Handler class CameraStreamHandler(): def __init__(self, serial_number, start_port, run_event): - # threading.Thread.__init__(self) print( f" - CameraStreamHandler - __init__ - serial_number: {serial_number} - video_port: {start_port} - audio_port: {start_port + 1} - backchannel_port: {start_port + 2}" ) @@ -314,26 +312,6 @@ def __init__(self, serial_number, start_port, run_event): "command": "device.stop_livestream", "serialNumber": self.serial_number, } - # def run(self): - # # Main logic of the handler goes here - # while self.run_event.is_set(): - # # Example of handling sockets - # try: - # # Your socket handling code here - # pass - # except select.error: - # print("Select error on socket ", self.name) - # pass - # sys.stdout.flush() - # try: - # self.client_sock.shutdown(socket.SHUT_RDWR) - # except OSError: - # print("Error shutdown socket: ", self.name) - # sys.stdout.flush() - # self.client_sock.close() - # msg = STOP_TALKBACK.copy() - # msg["serialNumber"] = self.serialno - # asyncio.run(self.ws.send_message(json.dumps(msg))) def setup_sockets(self): self.video_sock.bind(("0.0.0.0", self.start_port)) @@ -471,9 +449,11 @@ async def on_message(message): event_type = message["event"] sys.stdout.flush() if message["event"] == "livestream audio data": - # print(f"on_audio - {message}") + print(f"on_audio") event_value = message[EVENT_CONFIGURATION[event_type]["value"]] + print("event_value: ", event_value) event_data_type = EVENT_CONFIGURATION[event_type]["type"] + print("event_data_type: ", event_data_type) if event_data_type == "event": print(f"##################################################################") print(f"on_audio - {message['serialNumber']}") From ae58968b403697d1979e468ba989951ea93ae3c8 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 15:09:41 +0100 Subject: [PATCH 29/51] More debug. --- config.yaml | 2 +- files/eufyp2pstream.py | 68 +++++++++++++++++++++--------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/config.yaml b/config.yaml index ddefe78..12a5e18 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.18-beta +version: 0.2.19-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index ca88e49..eaa50af 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -302,18 +302,18 @@ def __init__(self, serial_number, start_port, run_event): self.video_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.audio_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.backchannel_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.start_livestream_msg = { - "messageId": "start_livestream", - "command": "device.start_livestream", - "serialNumber": self.serial_number, - } - self.stop_livestream_msg = { - "messageId": "stop_livestream", - "command": "device.stop_livestream", - "serialNumber": self.serial_number, - } - - def setup_sockets(self): + # self.start_livestream_msg = { + # "messageId": "start_livestream", + # "command": "device.start_livestream", + # "serialNumber": self.serial_number, + # } + # self.stop_livestream_msg = { + # "messageId": "stop_livestream", + # "command": "device.stop_livestream", + # "serialNumber": self.serial_number, + # } + + # def setup_sockets(self): self.video_sock.bind(("0.0.0.0", self.start_port)) self.video_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.video_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) @@ -359,29 +359,29 @@ def start_stream(self): def setWs(self, ws: EufySecurityWebSocket): self.ws = ws - async def _start_stream_async(self, websocket): - await websocket.send(json.dumps(self.start_livestream_msg)) - video_conn, _ = self.video_sock.accept() - audio_conn, _ = self.audio_sock.accept() - backchannel_conn, _ = self.backchannel_sock.accept() + # async def _start_stream_async(self, websocket): + # await websocket.send(json.dumps(self.start_livestream_msg)) + # video_conn, _ = self.video_sock.accept() + # audio_conn, _ = self.audio_sock.accept() + # backchannel_conn, _ = self.backchannel_sock.accept() - while True: - video_data = await websocket.recv() - audio_data = await websocket.recv() - video_conn.sendall(video_data) - audio_conn.sendall(audio_data) - backchannel_conn.sendall( - b"" - ) # Send empty data to keep the backchannel alive + # while True: + # video_data = await websocket.recv() + # audio_data = await websocket.recv() + # video_conn.sendall(video_data) + # audio_conn.sendall(audio_data) + # backchannel_conn.sendall( + # b"" + # ) # Send empty data to keep the backchannel alive - def stop_stream(self, websocket): - asyncio.run(self._stop_stream_async(websocket)) + # def stop_stream(self, websocket): + # asyncio.run(self._stop_stream_async(websocket)) - async def _stop_stream_async(self, websocket): - await websocket.send(json.dumps(self.stop_livestream_msg)) - self.video_sock.close() - self.audio_sock.close() - self.backchannel_sock.close() + # async def _stop_stream_async(self, websocket): + # await websocket.send(json.dumps(self.stop_livestream_msg)) + # self.video_sock.close() + # self.audio_sock.close() + # self.backchannel_sock.close() # On Open Callback @@ -494,7 +494,7 @@ async def on_message(message): # msg = START_P2P_LIVESTREAM_MESSAGE.copy() # msg["serialNumber"] = self.serialno # await self.ws.send_message(json.dumps(msg)) - + print("on_message done") async def init_websocket(ws_security_port): websocket = EufySecurityWebSocket( @@ -550,7 +550,7 @@ async def init_websocket(ws_security_port): for i, serial in enumerate(args.camera_serials): if serial != "null": handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) - handler.setup_sockets() + # handler.setup_sockets() camera_handlers[serial] = handler # Loop forever. From bad7c5e7b50ad2179d03e2cec364daae7851c8f5 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 15:18:02 +0100 Subject: [PATCH 30/51] Added more debugging. --- config.yaml | 2 +- files/eufyp2pstream.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index 12a5e18..1917e0f 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.19-beta +version: 0.2.20-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index eaa50af..a08fb16 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -548,11 +548,13 @@ async def init_websocket(ws_security_port): BASE_PORT = 63336 # Create one Camera Stream Handler per camera. for i, serial in enumerate(args.camera_serials): + print("Creating CameraStreamHandler for camera: ", serial) if serial != "null": handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) # handler.setup_sockets() camera_handlers[serial] = handler + print("Starting websocket.") # Loop forever. loop = asyncio.get_event_loop() loop.run_until_complete(init_websocket(args.ws_security_port)) From 8575120044ccd7d0c3e94c2ba97cc25969100d92 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 15:26:05 +0100 Subject: [PATCH 31/51] Changed print to logMessage to ensure it's sent in order to the log. --- config.yaml | 2 +- files/eufyp2pstream.py | 184 +++++++++++++++++++++-------------------- 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/config.yaml b/config.yaml index 1917e0f..effb89e 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.20-beta +version: 0.2.21-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index a08fb16..d66fc82 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -84,7 +84,8 @@ def exit_handler(signum, frame): - print(f"Signal handler called with signal {signum}") + """Signal handler to stop the script.""" + logMessage(f"Signal handler called with signal {signum}") run_event.set() @@ -92,16 +93,14 @@ def exit_handler(signum, frame): signal.signal(signal.SIGINT, exit_handler) -def exit_handler(signum, frame): - print(f"Signal handler called with signal {signum}") - run_event.set() - - -# Install signal handler -signal.signal(signal.SIGINT, exit_handler) +def logMessage(message): + """Log a message to the console.""" + print(message) + sys.stdout.flush() class ClientAcceptThread(threading.Thread): + # ClientAcceptThread def __init__(self, socket, run_event, name, ws, serialno): threading.Thread.__init__(self) self.socket = socket @@ -113,7 +112,7 @@ def __init__(self, socket, run_event, name, ws, serialno): self.my_threads = [] def update_threads(self): - print("Updating ", self.name, " threads for ", self.serialno) + logMessage("Updating ", self.name, " threads for ", self.serialno) my_threads_before = len(self.my_threads) for thread in self.my_threads: if not thread.is_alive(): @@ -121,37 +120,35 @@ def update_threads(self): self.my_threads = [t for t in self.my_threads if t.is_alive()] if self.ws and my_threads_before > 0 and len(self.my_threads) == 0: if self.name == "BackChannel": - print("All clients died (BackChannel): ", self.name) - sys.stdout.flush() + logMessage("All clients died (BackChannel): ", self.name) + else: - print("All clients died. Stopping Stream: ", self.name) - sys.stdout.flush() + logMessage("All clients died. Stopping Stream: ", self.name) msg = STOP_P2P_LIVESTREAM_MESSAGE.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) - print("Done updating ", self.name, " threads for ", self.serialno) + logMessage("Done updating ", self.name, " threads for ", self.serialno) def run(self): - print("Accepting ", self.name, " connection for ", self.serialno) + logMessage("Accepting ", self.name, " connection for ", self.serialno) msg = STOP_TALKBACK.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) - print("stop talkback sent for ", self.serialno) + logMessage("stop talkback sent for ", self.serialno) while not self.run_event.is_set(): - print("Updaing threads for ", self.name) + logMessage("Updaing threads for ", self.name) self.update_threads() - print("Waiting for ", self.name, " connection for ", self.serialno) - sys.stdout.flush() - print("Flush done for ", self.serialno) + logMessage("Waiting for ", self.name, " connection for ", self.serialno) + + logMessage("Flush done for ", self.serialno) try: client_sock, client_addr = self.socket.accept() - print("New connection added: ", client_addr, " for ", self.name) - sys.stdout.flush() + logMessage("New connection added: ", client_addr, " for ", self.name) if self.name == "BackChannel": client_sock.setblocking(True) - print("Starting BackChannel") + logMessage("Starting BackChannel") thread = ClientRecvThread( client_sock, run_event, self.name, self.ws, self.serialno ) @@ -170,7 +167,7 @@ def run(self): thread.start() except socket.timeout: pass - print("ClientAcceptThread ", self.name, " ended for ", self.serialno) + logMessage("ClientAcceptThread ", self.name, " ended for ", self.serialno) class ClientSendThread(threading.Thread): @@ -184,8 +181,7 @@ def __init__(self, client_sock, run_event, name, ws, serialno): self.serialno = serialno def run(self): - print("Thread running: ", self.name) - sys.stdout.flush() + logMessage("Thread running: ", self.name) try: while not self.run_event.is_set(): @@ -193,28 +189,27 @@ def run(self): [], [self.client_sock], [self.client_sock], 2 ) if len(in_error): - print("Exception in socket", self.name) - sys.stdout.flush() + logMessage("Exception in socket", self.name) + break if not len(ready_to_write): - print("Socket not ready to write ", self.name) - sys.stdout.flush() + logMessage("Socket not ready to write ", self.name) + break if not self.queue.empty(): self.client_sock.sendall(bytearray(self.queue.get(True)["data"])) except socket.error as e: - print("Connection lost", self.name, e) + logMessage("Connection lost", self.name, e) pass except socket.timeout: - print("Timeout on socket for ", self.name) + logMessage("Timeout on socket for ", self.name) pass try: self.client_sock.shutdown(socket.SHUT_RDWR) except OSError: - print("Error shutdown socket: ", self.name) + logMessage("Error shutdown socket: ", self.name) self.client_sock.close() - print("Thread stopping: ", self.name) - sys.stdout.flush() + logMessage("Thread stopping: ", self.name) class ClientRecvThread(threading.Thread): @@ -244,8 +239,8 @@ def run(self): 2, ) if len(in_error): - print("Exception in socket", self.name) - sys.stdout.flush() + logMessage("Exception in socket", self.name) + break if len(ready_to_read): data = self.client_sock.recv(RECV_CHUNK_SIZE) @@ -262,27 +257,27 @@ def run(self): else: no_data += 1 if no_data >= 15: - print("15x in a row no data in socket ", self.name) - sys.stdout.flush() + logMessage("15x in a row no data in socket ", self.name) + break except BlockingIOError: # Resource temporarily unavailable (errno EWOULDBLOCK) pass except socket.error as e: - print("Connection lost", self.name, e) + logMessage("Connection lost", self.name, e) pass except socket.timeout: - print("Timeout on socket for ", self.name) + logMessage("Timeout on socket for ", self.name) pass except select.error: - print("Select error on socket ", self.name) + logMessage("Select error on socket ", self.name) pass - sys.stdout.flush() + try: self.client_sock.shutdown(socket.SHUT_RDWR) except OSError: - print("Error shutdown socket: ", self.name) - sys.stdout.flush() + logMessage("Error shutdown socket: ", self.name) + self.client_sock.close() msg = STOP_TALKBACK.copy() msg["serialNumber"] = self.serialno @@ -290,9 +285,9 @@ def run(self): # Camera Stream Handler -class CameraStreamHandler(): +class CameraStreamHandler: def __init__(self, serial_number, start_port, run_event): - print( + logMessage( f" - CameraStreamHandler - __init__ - serial_number: {serial_number} - video_port: {start_port} - audio_port: {start_port + 1} - backchannel_port: {start_port + 2}" ) self.serial_number = serial_number @@ -313,7 +308,7 @@ def __init__(self, serial_number, start_port, run_event): # "serialNumber": self.serial_number, # } - # def setup_sockets(self): + # def setup_sockets(self): self.video_sock.bind(("0.0.0.0", self.start_port)) self.video_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.video_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) @@ -332,15 +327,15 @@ def __init__(self, serial_number, start_port, run_event): self.backchannel_sock.listen() def start_stream(self): - print(f"Starting stream for camera {self.serial_number}.") + logMessage(f"Starting stream for camera {self.serial_number}.") self.video_thread = ClientAcceptThread( self.video_sock, self.run_event, "Video", self.ws, self.serial_number ) - print(f"Video thread setup.") + logMessage(f"Video thread setup.") self.audio_thread = ClientAcceptThread( self.audio_sock, self.run_event, "Audio", self.ws, self.serial_number ) - print(f"Audio thread setup.") + logMessage(f"Audio thread setup.") self.backchannel_thread = ClientAcceptThread( self.backchannel_sock, self.run_event, @@ -348,13 +343,13 @@ def start_stream(self): self.ws, self.serial_number, ) - print(f"Backchannel thread setup.") + logMessage(f"Backchannel thread setup.") self.audio_thread.start() - print("Audio thread started.") + logMessage("Audio thread started.") self.video_thread.start() - print("Video thread started.") + logMessage("Video thread started.") self.backchannel_thread.start() - print("Backchannel thread started.") + logMessage("Backchannel thread started.") def setWs(self, ws: EufySecurityWebSocket): self.ws = ws @@ -386,12 +381,12 @@ def setWs(self, ws: EufySecurityWebSocket): # On Open Callback async def on_open(): - print(f" on_open - executed") + logMessage(f" on_open - executed") # On Close Callback async def on_close(): - print(f" on_close - executed") + logMessage(f" on_close - executed") # self.run_event.set() @@ -402,7 +397,7 @@ async def on_close(): # On Error Callback async def on_error(message): - print(f" on_error - executed - {message}") + logMessage(f" on_error - executed - {message}") # On Message Callback @@ -413,19 +408,21 @@ async def on_message(message): message_id = payload["messageId"] if message_id != SEND_TALKBACK_AUDIO_DATA["messageId"]: # Avoid spamming of TALKBACK_AUDIO_DATA logs - print(f"on_message result: {payload}") - sys.stdout.flush() + logMessage(f"on_message result: {payload}") + if message_id == START_LISTENING_MESSAGE["messageId"]: - # print(f"Listening started: {payload}") + # logMessage(f"Listening started: {payload}") message_result = payload[message_type] states = message_result["state"] for state in states["devices"]: serialno = state["serialNumber"] if serialno in camera_handlers: camera_handlers[serialno].start_stream() - print(f"Started stream for camera {serialno}.") + logMessage(f"Started stream for camera {serialno}.") else: - print(f"Found unknown Eufy camera with serial number {serialno}.") + logMessage( + f"Found unknown Eufy camera with serial number {serialno}." + ) # self.video_thread = ClientAcceptThread(video_sock, run_event, "Video", self.ws, self.serialno) # self.audio_thread = ClientAcceptThread(audio_sock, run_event, "Audio", self.ws, self.serialno) @@ -438,63 +435,70 @@ async def on_message(message): and "errorCode" in payload ): error_code = payload["errorCode"] - print(f"Talkback error: {error_code}") + logMessage(f"Talkback error: {error_code}") # if error_code == "device_talkback_not_running": - # msg = START_TALKBACK.copy() - # msg["serialNumber"] = self.serialno - # await self.ws.send_message(json.dumps(msg)) + # msg = START_TALKBACK.copy() + # msg["serialNumber"] = self.serialno + # await self.ws.send_message(json.dumps(msg)) if message_type == "event": message = payload[message_type] event_type = message["event"] - sys.stdout.flush() + if message["event"] == "livestream audio data": - print(f"on_audio") + logMessage(f"on_audio") event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - print("event_value: ", event_value) + logMessage("event_value: ", event_value) event_data_type = EVENT_CONFIGURATION[event_type]["type"] - print("event_data_type: ", event_data_type) + logMessage("event_data_type: ", event_data_type) if event_data_type == "event": - print(f"##################################################################") - print(f"on_audio - {message['serialNumber']}") - serialno = message['serialNumber'] + logMessage( + f"##################################################################" + ) + logMessage(f"on_audio - {message['serialNumber']}") + serialno = message["serialNumber"] if serialno in camera_handlers: for queue in camera_handlers[serialno].audio_thread.queues: if queue.full(): - print("Audio queue full.") + logMessage("Audio queue full.") queue.get(False) queue.put(event_value) # for queue in self.audio_thread.queues: # if queue.full(): - # print("Audio queue full.") + # logMessage("Audio queue full.") # queue.get(False) # queue.put(event_value) if message["event"] == "livestream video data": event_value = message[EVENT_CONFIGURATION[event_type]["value"]] event_data_type = EVENT_CONFIGURATION[event_type]["type"] if event_data_type == "event": - print(f"##################################################################") - print(f"on_video - {message['serialNumber']}") - serialno = message['serialNumber'] + logMessage( + f"##################################################################" + ) + logMessage(f"on_video - {message['serialNumber']}") + serialno = message["serialNumber"] if serialno in camera_handlers: for queue in camera_handlers[serialno].video_thread.queues: if queue.full(): - print("Video queue full.") + logMessage("Video queue full.") queue.get(False) queue.put(event_value) # for queue in self.video_thread.queues: # if queue.full(): - # print("Video queue full.") + # logMessage("Video queue full.") # queue.get(False) # queue.put(event_value) if message["event"] == "livestream error": - print(f"##################################################################") - print(f"Livestream Error! - {payload}") + logMessage( + f"##################################################################" + ) + logMessage(f"Livestream Error! - {payload}") # if self.ws and len(self.video_thread.queues) > 0: # msg = START_P2P_LIVESTREAM_MESSAGE.copy() # msg["serialNumber"] = self.serialno # await self.ws.send_message(json.dumps(msg)) - print("on_message done") + logMessage("on_message done") + async def init_websocket(ws_security_port): websocket = EufySecurityWebSocket( @@ -518,8 +522,8 @@ async def init_websocket(ws_security_port): while not run_event.is_set(): await asyncio.sleep(1000) except Exception as ex: - print(ex) - print("init_websocket failed. Exiting.") + logMessage(ex) + logMessage("init_websocket failed. Exiting.") os._exit(-1) @@ -541,20 +545,20 @@ async def init_websocket(ws_security_port): help="Base port number for streaming (default: 3000).", ) args = parser.parse_args() - print(f"WS Security Port: {args.ws_security_port}") - print(f"Camera Serial Numbers: {args.camera_serials}") + logMessage(f"WS Security Port: {args.ws_security_port}") + logMessage(f"Camera Serial Numbers: {args.camera_serials}") # Define constants. BASE_PORT = 63336 # Create one Camera Stream Handler per camera. for i, serial in enumerate(args.camera_serials): - print("Creating CameraStreamHandler for camera: ", serial) + logMessage("Creating CameraStreamHandler for camera: ", serial) if serial != "null": handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) # handler.setup_sockets() camera_handlers[serial] = handler - print("Starting websocket.") + logMessage("Starting websocket.") # Loop forever. loop = asyncio.get_event_loop() loop.run_until_complete(init_websocket(args.ws_security_port)) From f82679ad98a094fa8227875cf49ea92118e54df7 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 15:38:36 +0100 Subject: [PATCH 32/51] Corrected logMessage calls. --- config.yaml | 2 +- files/eufyp2pstream.py | 76 +++++++++++++++++++++--------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/config.yaml b/config.yaml index effb89e..e707c69 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.21-beta +version: 0.2.22-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index d66fc82..733a166 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -112,7 +112,7 @@ def __init__(self, socket, run_event, name, ws, serialno): self.my_threads = [] def update_threads(self): - logMessage("Updating ", self.name, " threads for ", self.serialno) + logMessage(f"Updating {self.name} threads for {self.serialno}") my_threads_before = len(self.my_threads) for thread in self.my_threads: if not thread.is_alive(): @@ -120,35 +120,35 @@ def update_threads(self): self.my_threads = [t for t in self.my_threads if t.is_alive()] if self.ws and my_threads_before > 0 and len(self.my_threads) == 0: if self.name == "BackChannel": - logMessage("All clients died (BackChannel): ", self.name) + logMessage(f"All clients died (BackChannel): {self.name}") else: - logMessage("All clients died. Stopping Stream: ", self.name) + logMessage(f"All clients died. Stopping Stream: {self.name}") msg = STOP_P2P_LIVESTREAM_MESSAGE.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) - logMessage("Done updating ", self.name, " threads for ", self.serialno) + logMessage(f"Done updating {self.name} threads for {self.serialno}") def run(self): - logMessage("Accepting ", self.name, " connection for ", self.serialno) + logMessage(f"Accepting {self.name} connection for {self.serialno}") msg = STOP_TALKBACK.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) - logMessage("stop talkback sent for ", self.serialno) + logMessage(f"stop talkback sent for {self.serialno}") while not self.run_event.is_set(): - logMessage("Updaing threads for ", self.name) + logMessage(f"Updaing threads for {self.name}") self.update_threads() - logMessage("Waiting for ", self.name, " connection for ", self.serialno) + logMessage(f"Waiting for {self.name} connection for {self.serialno}") - logMessage("Flush done for ", self.serialno) + logMessage(f"Flush done for {self.serialno}") try: client_sock, client_addr = self.socket.accept() - logMessage("New connection added: ", client_addr, " for ", self.name) + logMessage(f"New connection added: {client_addr} for {self.name}") if self.name == "BackChannel": client_sock.setblocking(True) - logMessage("Starting BackChannel") + logMessage(f"Starting BackChannel") thread = ClientRecvThread( client_sock, run_event, self.name, self.ws, self.serialno ) @@ -167,7 +167,7 @@ def run(self): thread.start() except socket.timeout: pass - logMessage("ClientAcceptThread ", self.name, " ended for ", self.serialno) + logMessage(f"ClientAcceptThread {self.name} ended for {self.serialno}") class ClientSendThread(threading.Thread): @@ -181,7 +181,7 @@ def __init__(self, client_sock, run_event, name, ws, serialno): self.serialno = serialno def run(self): - logMessage("Thread running: ", self.name) + logMessage(f"Thread running: {self.name}") try: while not self.run_event.is_set(): @@ -189,27 +189,27 @@ def run(self): [], [self.client_sock], [self.client_sock], 2 ) if len(in_error): - logMessage("Exception in socket", self.name) + logMessage(f"Exception in socket {self.name}") break if not len(ready_to_write): - logMessage("Socket not ready to write ", self.name) + logMessage(f"Socket not ready to write {self.name}") break if not self.queue.empty(): self.client_sock.sendall(bytearray(self.queue.get(True)["data"])) except socket.error as e: - logMessage("Connection lost", self.name, e) + logMessage(f"Connection lost {self.name}: {e}") pass except socket.timeout: - logMessage("Timeout on socket for ", self.name) + logMessage(f"Timeout on socket for {self.name}") pass try: self.client_sock.shutdown(socket.SHUT_RDWR) except OSError: - logMessage("Error shutdown socket: ", self.name) + logMessage(f"Error shutdown socket: {self.name}") self.client_sock.close() - logMessage("Thread stopping: ", self.name) + logMessage(f"Thread stopping: {self.name}") class ClientRecvThread(threading.Thread): @@ -239,7 +239,7 @@ def run(self): 2, ) if len(in_error): - logMessage("Exception in socket", self.name) + logMessage(f"Exception in socket {self.name}") break if len(ready_to_read): @@ -257,26 +257,26 @@ def run(self): else: no_data += 1 if no_data >= 15: - logMessage("15x in a row no data in socket ", self.name) + logMessage(f"15x in a row no data in socket {self.name}") break except BlockingIOError: # Resource temporarily unavailable (errno EWOULDBLOCK) pass except socket.error as e: - logMessage("Connection lost", self.name, e) + logMessage(f"Connection lost {self.name}: {e}") pass except socket.timeout: - logMessage("Timeout on socket for ", self.name) + logMessage(f"Timeout on socket for {self.name}") pass except select.error: - logMessage("Select error on socket ", self.name) + logMessage(f"Select error on socket {self.name}") pass try: self.client_sock.shutdown(socket.SHUT_RDWR) except OSError: - logMessage("Error shutdown socket: ", self.name) + logMessage(f"Error shutdown socket: {self.name}") self.client_sock.close() msg = STOP_TALKBACK.copy() @@ -345,11 +345,11 @@ def start_stream(self): ) logMessage(f"Backchannel thread setup.") self.audio_thread.start() - logMessage("Audio thread started.") + logMessage(f"Audio thread started.") self.video_thread.start() - logMessage("Video thread started.") + logMessage(f"Video thread started.") self.backchannel_thread.start() - logMessage("Backchannel thread started.") + logMessage(f"Backchannel thread started.") def setWs(self, ws: EufySecurityWebSocket): self.ws = ws @@ -448,9 +448,9 @@ async def on_message(message): if message["event"] == "livestream audio data": logMessage(f"on_audio") event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - logMessage("event_value: ", event_value) + logMessage(f"event_value: {event_value}") event_data_type = EVENT_CONFIGURATION[event_type]["type"] - logMessage("event_data_type: ", event_data_type) + logMessage(f"event_data_type: {event_data_type}") if event_data_type == "event": logMessage( f"##################################################################" @@ -460,12 +460,12 @@ async def on_message(message): if serialno in camera_handlers: for queue in camera_handlers[serialno].audio_thread.queues: if queue.full(): - logMessage("Audio queue full.") + logMessage(f"Audio queue full.") queue.get(False) queue.put(event_value) # for queue in self.audio_thread.queues: # if queue.full(): - # logMessage("Audio queue full.") + # logMessage(f"Audio queue full.") # queue.get(False) # queue.put(event_value) if message["event"] == "livestream video data": @@ -480,12 +480,12 @@ async def on_message(message): if serialno in camera_handlers: for queue in camera_handlers[serialno].video_thread.queues: if queue.full(): - logMessage("Video queue full.") + logMessage(f"Video queue full.") queue.get(False) queue.put(event_value) # for queue in self.video_thread.queues: # if queue.full(): - # logMessage("Video queue full.") + # logMessage(f"Video queue full.") # queue.get(False) # queue.put(event_value) if message["event"] == "livestream error": @@ -497,7 +497,7 @@ async def on_message(message): # msg = START_P2P_LIVESTREAM_MESSAGE.copy() # msg["serialNumber"] = self.serialno # await self.ws.send_message(json.dumps(msg)) - logMessage("on_message done") + logMessage(f"on_message done") async def init_websocket(ws_security_port): @@ -523,7 +523,7 @@ async def init_websocket(ws_security_port): await asyncio.sleep(1000) except Exception as ex: logMessage(ex) - logMessage("init_websocket failed. Exiting.") + logMessage(f"init_websocket failed. Exiting.") os._exit(-1) @@ -552,13 +552,13 @@ async def init_websocket(ws_security_port): BASE_PORT = 63336 # Create one Camera Stream Handler per camera. for i, serial in enumerate(args.camera_serials): - logMessage("Creating CameraStreamHandler for camera: ", serial) + logMessage(f"Creating CameraStreamHandler for camera: {serial}") if serial != "null": handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) # handler.setup_sockets() camera_handlers[serial] = handler - logMessage("Starting websocket.") + logMessage(f"Starting websocket.") # Loop forever. loop = asyncio.get_event_loop() loop.run_until_complete(init_websocket(args.ws_security_port)) From d93b196e2a70c474312cd2e845d2e440e8c08e1e Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 15:49:14 +0100 Subject: [PATCH 33/51] Cleaned up debugging. --- config.yaml | 2 +- files/eufyp2pstream.py | 41 +++++++++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/config.yaml b/config.yaml index e707c69..f51fd67 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.22-beta +version: 0.2.23-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 733a166..03b5217 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -100,8 +100,9 @@ def logMessage(message): class ClientAcceptThread(threading.Thread): - # ClientAcceptThread + """Thread to accept incoming connections from clients.""" def __init__(self, socket, run_event, name, ws, serialno): + """Initialize the thread.""" threading.Thread.__init__(self) self.socket = socket self.queues = [] @@ -112,7 +113,8 @@ def __init__(self, socket, run_event, name, ws, serialno): self.my_threads = [] def update_threads(self): - logMessage(f"Updating {self.name} threads for {self.serialno}") + """Update the list of active threads.""" +# logMessage(f"Updating {self.name} threads for {self.serialno}") my_threads_before = len(self.my_threads) for thread in self.my_threads: if not thread.is_alive(): @@ -121,34 +123,33 @@ def update_threads(self): if self.ws and my_threads_before > 0 and len(self.my_threads) == 0: if self.name == "BackChannel": logMessage(f"All clients died (BackChannel): {self.name}") - else: logMessage(f"All clients died. Stopping Stream: {self.name}") - msg = STOP_P2P_LIVESTREAM_MESSAGE.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) - logMessage(f"Done updating {self.name} threads for {self.serialno}") +# logMessage(f"Done updating {self.name} threads for {self.serialno}") def run(self): + """Run the thread to accept incoming connections.""" logMessage(f"Accepting {self.name} connection for {self.serialno}") msg = STOP_TALKBACK.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) logMessage(f"stop talkback sent for {self.serialno}") while not self.run_event.is_set(): - logMessage(f"Updaing threads for {self.name}") +# logMessage(f"Updating threads for {self.name}") self.update_threads() - logMessage(f"Waiting for {self.name} connection for {self.serialno}") +# logMessage(f"Waiting for {self.name} connection for {self.serialno}") - logMessage(f"Flush done for {self.serialno}") +# logMessage(f"Flush done for {self.serialno}") try: client_sock, client_addr = self.socket.accept() - logMessage(f"New connection added: {client_addr} for {self.name}") +# logMessage(f"New connection added: {client_addr} for {self.name}") if self.name == "BackChannel": client_sock.setblocking(True) - logMessage(f"Starting BackChannel") +# logMessage(f"Starting BackChannel") thread = ClientRecvThread( client_sock, run_event, self.name, self.ws, self.serialno ) @@ -171,7 +172,9 @@ def run(self): class ClientSendThread(threading.Thread): + """Thread to send data to clients.""" def __init__(self, client_sock, run_event, name, ws, serialno): + """Initialize the thread.""" threading.Thread.__init__(self) self.client_sock = client_sock self.queue = Queue(100) @@ -181,7 +184,8 @@ def __init__(self, client_sock, run_event, name, ws, serialno): self.serialno = serialno def run(self): - logMessage(f"Thread running: {self.name}") + """Run the thread to send data to clients.""" + logMessage(f"Thread {self.name} running for {self.serialno}") try: while not self.run_event.is_set(): @@ -209,11 +213,13 @@ def run(self): except OSError: logMessage(f"Error shutdown socket: {self.name}") self.client_sock.close() - logMessage(f"Thread stopping: {self.name}") + logMessage(f"Thread {self.name} stopping for {self.serialno}") class ClientRecvThread(threading.Thread): + """Thread to receive data from clients.""" def __init__(self, client_sock, run_event, name, ws, serialno): + """Initialize the thread.""" threading.Thread.__init__(self) self.client_sock = client_sock self.run_event = run_event @@ -222,6 +228,7 @@ def __init__(self, client_sock, run_event, name, ws, serialno): self.serialno = serialno def run(self): + """Run the thread to receive data from clients.""" msg = START_TALKBACK.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) @@ -286,7 +293,9 @@ def run(self): # Camera Stream Handler class CameraStreamHandler: + """Handler for camera streams.""" def __init__(self, serial_number, start_port, run_event): + """Initialize the handler.""" logMessage( f" - CameraStreamHandler - __init__ - serial_number: {serial_number} - video_port: {start_port} - audio_port: {start_port + 1} - backchannel_port: {start_port + 2}" ) @@ -327,6 +336,7 @@ def __init__(self, serial_number, start_port, run_event): self.backchannel_sock.listen() def start_stream(self): + """Start the stream.""" logMessage(f"Starting stream for camera {self.serial_number}.") self.video_thread = ClientAcceptThread( self.video_sock, self.run_event, "Video", self.ws, self.serial_number @@ -352,6 +362,7 @@ def start_stream(self): logMessage(f"Backchannel thread started.") def setWs(self, ws: EufySecurityWebSocket): + """Set the websocket for the camera handler.""" self.ws = ws # async def _start_stream_async(self, websocket): @@ -381,11 +392,13 @@ def setWs(self, ws: EufySecurityWebSocket): # On Open Callback async def on_open(): + """Callback when the websocket is opened.""" logMessage(f" on_open - executed") # On Close Callback async def on_close(): + """Callback when the websocket is closed.""" logMessage(f" on_close - executed") @@ -397,11 +410,13 @@ async def on_close(): # On Error Callback async def on_error(message): + """Callback when an error occurs.""" logMessage(f" on_error - executed - {message}") # On Message Callback async def on_message(message): + """Callback when a message is received.""" payload = message.json() message_type: str = payload["type"] if message_type == "result": @@ -501,6 +516,7 @@ async def on_message(message): async def init_websocket(ws_security_port): + """Initialize the websocket.""" websocket = EufySecurityWebSocket( "402f1039-eufy-security-ws", ws_security_port, @@ -528,6 +544,7 @@ async def init_websocket(ws_security_port): if __name__ == "__main__": + """Main entry point.""" # Parse command-line arguments parser = argparse.ArgumentParser( description="Stream video and audio from multiple Eufy cameras." From 82bf8aa321ec9f8a2cdb40927e8c45eb923a27fa Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 15:57:18 +0100 Subject: [PATCH 34/51] Debug messages. --- config.yaml | 2 +- files/eufyp2pstream.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config.yaml b/config.yaml index f51fd67..62e112b 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.23-beta +version: 0.2.24-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 03b5217..13db8de 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -419,7 +419,9 @@ async def on_message(message): """Callback when a message is received.""" payload = message.json() message_type: str = payload["type"] + logMessage(f"on_message - {message_type}") if message_type == "result": + logMessage(f"message is of type result") message_id = payload["messageId"] if message_id != SEND_TALKBACK_AUDIO_DATA["messageId"]: # Avoid spamming of TALKBACK_AUDIO_DATA logs @@ -456,7 +458,8 @@ async def on_message(message): # msg["serialNumber"] = self.serialno # await self.ws.send_message(json.dumps(msg)) - if message_type == "event": + elif message_type == "event": + logMessage(f"message is of type event") message = payload[message_type] event_type = message["event"] @@ -512,6 +515,8 @@ async def on_message(message): # msg = START_P2P_LIVESTREAM_MESSAGE.copy() # msg["serialNumber"] = self.serialno # await self.ws.send_message(json.dumps(msg)) + else: + logMessage(f"Unknown message type: {message_type}") logMessage(f"on_message done") From 7488c6099cf3801a91938bcbba56133ddec9383f Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 16:04:42 +0100 Subject: [PATCH 35/51] More debugging. --- config.yaml | 2 +- files/eufyp2pstream.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/config.yaml b/config.yaml index 62e112b..e493c9c 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.24-beta +version: 0.2.25-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 13db8de..ef1018c 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -462,7 +462,7 @@ async def on_message(message): logMessage(f"message is of type event") message = payload[message_type] event_type = message["event"] - + logMessage(f"event_type: {event_type}") if message["event"] == "livestream audio data": logMessage(f"on_audio") event_value = message[EVENT_CONFIGURATION[event_type]["value"]] @@ -486,9 +486,12 @@ async def on_message(message): # logMessage(f"Audio queue full.") # queue.get(False) # queue.put(event_value) - if message["event"] == "livestream video data": + elif message["event"] == "livestream video data": + logMessage(f"on_video") event_value = message[EVENT_CONFIGURATION[event_type]["value"]] + logMessage(f"event_value: {event_value}") event_data_type = EVENT_CONFIGURATION[event_type]["type"] + logMessage(f"event_data_type: {event_data_type}") if event_data_type == "event": logMessage( f"##################################################################" @@ -506,7 +509,7 @@ async def on_message(message): # logMessage(f"Video queue full.") # queue.get(False) # queue.put(event_value) - if message["event"] == "livestream error": + elif message["event"] == "livestream error": logMessage( f"##################################################################" ) @@ -515,6 +518,8 @@ async def on_message(message): # msg = START_P2P_LIVESTREAM_MESSAGE.copy() # msg["serialNumber"] = self.serialno # await self.ws.send_message(json.dumps(msg)) + else: + logMessage(f"Unknown event type: {message['event']}") else: logMessage(f"Unknown message type: {message_type}") logMessage(f"on_message done") From 4462ec0d54b28397846629b3cca38f1ba54efffd Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Sun, 24 Nov 2024 16:22:03 +0100 Subject: [PATCH 36/51] More debugging. --- config.yaml | 2 +- files/eufyp2pstream.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index e493c9c..1db66bb 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.25-beta +version: 0.2.26-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index ef1018c..5f836fc 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -520,6 +520,7 @@ async def on_message(message): # await self.ws.send_message(json.dumps(msg)) else: logMessage(f"Unknown event type: {message['event']}") + logMessage(f"{message}") else: logMessage(f"Unknown message type: {message_type}") logMessage(f"on_message done") From 88598e83d2dc47b51aa81dad45d5a04c2e92dee8 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 09:34:59 +0100 Subject: [PATCH 37/51] Works! Cleaned up a lot and added capability to stream up to 15 cameras. --- config.yaml | 77 ++++++++++++++++++++- files/eufyp2pstream.py | 154 +++++++++++------------------------------ files/run.sh | 15 +++- 3 files changed, 128 insertions(+), 118 deletions(-) diff --git a/config.yaml b/config.yaml index 1db66bb..409357a 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.26-beta +version: 0.2.27-beta slug: eufyp2pstream init: false startup: application @@ -14,6 +14,7 @@ map: [config, media] host_network: false options: eufy_security_ws_port: 3000 + debug: false schema: eufy_security_ws_port: port camera_1_serial_number: "str?" @@ -21,6 +22,16 @@ schema: camera_3_serial_number: "str?" camera_4_serial_number: "str?" camera_5_serial_number: "str?" + camera_6_serial_number: "str?" + camera_7_serial_number: "str?" + camera_8_serial_number: "str?" + camera_9_serial_number: "str?" + camera_10_serial_number: "str?" + camera_11_serial_number: "str?" + camera_12_serial_number: "str?" + camera_13_serial_number: "str?" + camera_14_serial_number: "str?" + camera_15_serial_number: "str?" ports: { "63336/tcp": 63336, "63337/tcp": 63337, @@ -36,7 +47,37 @@ ports: { "63347/tcp": 63347, "63348/tcp": 63348, "63349/tcp": 63349, - "63350/tcp": 63350 + "63350/tcp": 63350, + "63351/tcp": 63351, + "63352/tcp": 63352, + "63353/tcp": 63353, + "63354/tcp": 63354, + "63355/tcp": 63355, + "63356/tcp": 63356, + "63357/tcp": 63357, + "63358/tcp": 63358, + "63359/tcp": 63359, + "63360/tcp": 63360, + "63361/tcp": 63361, + "63362/tcp": 63362, + "63363/tcp": 63363, + "63364/tcp": 63364, + "63365/tcp": 63365, + "63366/tcp": 63366, + "63367/tcp": 63367, + "63368/tcp": 63368, + "63369/tcp": 63369, + "63370/tcp": 63370, + "63371/tcp": 63371, + "63372/tcp": 63372, + "63373/tcp": 63373, + "63374/tcp": 63374, + "63375/tcp": 63375, + "63376/tcp": 63376, + "63377/tcp": 63377, + "63378/tcp": 63378, + "63379/tcp": 63379, + "63380/tcp": 63380, } ports_description: { "63336/tcp": "Camera-1 Video Stream", @@ -53,5 +94,35 @@ ports_description: { "63347/tcp": "Camera-4 Backchannel", "63348/tcp": "Camera-5 Video Stream", "63349/tcp": "Camera-5 Audio Stream", - "63350/tcp": "Camera-5 Backchannel" + "63350/tcp": "Camera-5 Backchannel", + "63351/tcp": "Camera-6 Video Stream", + "63352/tcp": "Camera-6 Audio Stream", + "63353/tcp": "Camera-6 Backchannel", + "63354/tcp": "Camera-7 Video Stream", + "63355/tcp": "Camera-7 Audio Stream", + "63356/tcp": "Camera-7 Backchannel", + "63357/tcp": "Camera-8 Video Stream", + "63358/tcp": "Camera-8 Audio Stream", + "63359/tcp": "Camera-8 Backchannel", + "63360/tcp": "Camera-9 Video Stream", + "63361/tcp": "Camera-9 Audio Stream", + "63362/tcp": "Camera-9 Backchannel", + "63363/tcp": "Camera-10 Video Stream", + "63364/tcp": "Camera-10 Audio Stream", + "63365/tcp": "Camera-10 Backchannel", + "63366/tcp": "Camera-11 Video Stream", + "63367/tcp": "Camera-11 Audio Stream", + "63368/tcp": "Camera-11 Backchannel", + "63369/tcp": "Camera-12 Video Stream", + "63370/tcp": "Camera-12 Audio Stream", + "63371/tcp": "Camera-12 Backchannel", + "63372/tcp": "Camera-13 Video Stream", + "63373/tcp": "Camera-13 Audio Stream", + "63374/tcp": "Camera-13 Backchannel", + "63375/tcp": "Camera-14 Video Stream", + "63376/tcp": "Camera-14 Audio Stream", + "63377/tcp": "Camera-14 Backchannel", + "63378/tcp": "Camera-15 Video Stream", + "63379/tcp": "Camera-15 Audio Stream", + "63380/tcp": "Camera-15 Backchannel", } diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 5f836fc..dc6c64e 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -16,6 +16,7 @@ # Variables camera_handlers = {} run_event = threading.Event() +debug = False # Constants RECV_CHUNK_SIZE = 4096 @@ -93,10 +94,11 @@ def exit_handler(signum, frame): signal.signal(signal.SIGINT, exit_handler) -def logMessage(message): +def logMessage(message, force=False): """Log a message to the console.""" - print(message) - sys.stdout.flush() + if debug or force: + print(message) + sys.stdout.flush() class ClientAcceptThread(threading.Thread): @@ -114,7 +116,6 @@ def __init__(self, socket, run_event, name, ws, serialno): def update_threads(self): """Update the list of active threads.""" -# logMessage(f"Updating {self.name} threads for {self.serialno}") my_threads_before = len(self.my_threads) for thread in self.my_threads: if not thread.is_alive(): @@ -128,7 +129,6 @@ def update_threads(self): msg = STOP_P2P_LIVESTREAM_MESSAGE.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) -# logMessage(f"Done updating {self.name} threads for {self.serialno}") def run(self): """Run the thread to accept incoming connections.""" @@ -138,18 +138,11 @@ def run(self): asyncio.run(self.ws.send_message(json.dumps(msg))) logMessage(f"stop talkback sent for {self.serialno}") while not self.run_event.is_set(): -# logMessage(f"Updating threads for {self.name}") self.update_threads() -# logMessage(f"Waiting for {self.name} connection for {self.serialno}") - -# logMessage(f"Flush done for {self.serialno}") try: client_sock, client_addr = self.socket.accept() -# logMessage(f"New connection added: {client_addr} for {self.name}") - if self.name == "BackChannel": client_sock.setblocking(True) -# logMessage(f"Starting BackChannel") thread = ClientRecvThread( client_sock, run_event, self.name, self.ws, self.serialno ) @@ -186,7 +179,6 @@ def __init__(self, client_sock, run_event, name, ws, serialno): def run(self): """Run the thread to send data to clients.""" logMessage(f"Thread {self.name} running for {self.serialno}") - try: while not self.run_event.is_set(): ready_to_read, ready_to_write, in_error = select.select( @@ -265,7 +257,6 @@ def run(self): no_data += 1 if no_data >= 15: logMessage(f"15x in a row no data in socket {self.name}") - break except BlockingIOError: # Resource temporarily unavailable (errno EWOULDBLOCK) @@ -289,9 +280,8 @@ def run(self): msg = STOP_TALKBACK.copy() msg["serialNumber"] = self.serialno asyncio.run(self.ws.send_message(json.dumps(msg))) + logMessage(f"Thread {self.name} stopping for {self.serialno}") - -# Camera Stream Handler class CameraStreamHandler: """Handler for camera streams.""" def __init__(self, serial_number, start_port, run_event): @@ -306,18 +296,6 @@ def __init__(self, serial_number, start_port, run_event): self.video_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.audio_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.backchannel_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # self.start_livestream_msg = { - # "messageId": "start_livestream", - # "command": "device.start_livestream", - # "serialNumber": self.serial_number, - # } - # self.stop_livestream_msg = { - # "messageId": "stop_livestream", - # "command": "device.stop_livestream", - # "serialNumber": self.serial_number, - # } - - # def setup_sockets(self): self.video_sock.bind(("0.0.0.0", self.start_port)) self.video_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.video_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) @@ -341,11 +319,9 @@ def start_stream(self): self.video_thread = ClientAcceptThread( self.video_sock, self.run_event, "Video", self.ws, self.serial_number ) - logMessage(f"Video thread setup.") self.audio_thread = ClientAcceptThread( self.audio_sock, self.run_event, "Audio", self.ws, self.serial_number ) - logMessage(f"Audio thread setup.") self.backchannel_thread = ClientAcceptThread( self.backchannel_sock, self.run_event, @@ -353,127 +329,95 @@ def start_stream(self): self.ws, self.serial_number, ) - logMessage(f"Backchannel thread setup.") self.audio_thread.start() - logMessage(f"Audio thread started.") self.video_thread.start() - logMessage(f"Video thread started.") self.backchannel_thread.start() - logMessage(f"Backchannel thread started.") def setWs(self, ws: EufySecurityWebSocket): """Set the websocket for the camera handler.""" self.ws = ws - # async def _start_stream_async(self, websocket): - # await websocket.send(json.dumps(self.start_livestream_msg)) - # video_conn, _ = self.video_sock.accept() - # audio_conn, _ = self.audio_sock.accept() - # backchannel_conn, _ = self.backchannel_sock.accept() - - # while True: - # video_data = await websocket.recv() - # audio_data = await websocket.recv() - # video_conn.sendall(video_data) - # audio_conn.sendall(audio_data) - # backchannel_conn.sendall( - # b"" - # ) # Send empty data to keep the backchannel alive - - # def stop_stream(self, websocket): - # asyncio.run(self._stop_stream_async(websocket)) - - # async def _stop_stream_async(self, websocket): - # await websocket.send(json.dumps(self.stop_livestream_msg)) - # self.video_sock.close() - # self.audio_sock.close() - # self.backchannel_sock.close() + def stop(self): + """Stop the stream.""" + try: + self.video_sock.shutdown(socket.SHUT_RDWR) + except OSError: + print("Error shutdown socket") + self.video_sock.close() + try: + self.audio_sock.shutdown(socket.SHUT_RDWR) + except OSError: + print("Error shutdown socket") + self.audio_sock.close() + try: + self.backchannel_sock.shutdown(socket.SHUT_RDWR) + except OSError: + print("Error shutdown socket") + self.backchannel_sock.close() -# On Open Callback async def on_open(): """Callback when the websocket is opened.""" logMessage(f" on_open - executed") -# On Close Callback async def on_close(): """Callback when the websocket is closed.""" logMessage(f" on_close - executed") + run_event.set() + # Close all camera handlers. + for handler in camera_handlers.values(): + handler.stop() + os._exit(-1) -# self.run_event.set() -# self.ws = None -# stop() -# os._exit(-1) - - -# On Error Callback async def on_error(message): """Callback when an error occurs.""" logMessage(f" on_error - executed - {message}") -# On Message Callback async def on_message(message): """Callback when a message is received.""" payload = message.json() message_type: str = payload["type"] - logMessage(f"on_message - {message_type}") if message_type == "result": - logMessage(f"message is of type result") message_id = payload["messageId"] if message_id != SEND_TALKBACK_AUDIO_DATA["messageId"]: # Avoid spamming of TALKBACK_AUDIO_DATA logs logMessage(f"on_message result: {payload}") if message_id == START_LISTENING_MESSAGE["messageId"]: - # logMessage(f"Listening started: {payload}") message_result = payload[message_type] states = message_result["state"] for state in states["devices"]: serialno = state["serialNumber"] if serialno in camera_handlers: camera_handlers[serialno].start_stream() - logMessage(f"Started stream for camera {serialno}.") + logMessage(f"Started stream for camera {serialno}.", True) else: logMessage( - f"Found unknown Eufy camera with serial number {serialno}." + f"Found unknown Eufy camera with serial number {serialno}.", True ) - - # self.video_thread = ClientAcceptThread(video_sock, run_event, "Video", self.ws, self.serialno) - # self.audio_thread = ClientAcceptThread(audio_sock, run_event, "Audio", self.ws, self.serialno) - # self.backchannel_thread = ClientAcceptThread(backchannel_sock, run_event, "BackChannel", self.ws, self.serialno) - # self.audio_thread.start() - # self.video_thread.start() - # self.backchannel_thread.start() - if ( + elif ( message_id == TALKBACK_RESULT_MESSAGE["messageId"] and "errorCode" in payload ): error_code = payload["errorCode"] logMessage(f"Talkback error: {error_code}") + logMessage(f"{payload}", True) + # TODO: Handle error codes with muliple cameras. # if error_code == "device_talkback_not_running": # msg = START_TALKBACK.copy() # msg["serialNumber"] = self.serialno # await self.ws.send_message(json.dumps(msg)) elif message_type == "event": - logMessage(f"message is of type event") message = payload[message_type] event_type = message["event"] - logMessage(f"event_type: {event_type}") if message["event"] == "livestream audio data": - logMessage(f"on_audio") event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - logMessage(f"event_value: {event_value}") event_data_type = EVENT_CONFIGURATION[event_type]["type"] - logMessage(f"event_data_type: {event_data_type}") if event_data_type == "event": - logMessage( - f"##################################################################" - ) - logMessage(f"on_audio - {message['serialNumber']}") serialno = message["serialNumber"] if serialno in camera_handlers: for queue in camera_handlers[serialno].audio_thread.queues: @@ -481,22 +425,10 @@ async def on_message(message): logMessage(f"Audio queue full.") queue.get(False) queue.put(event_value) - # for queue in self.audio_thread.queues: - # if queue.full(): - # logMessage(f"Audio queue full.") - # queue.get(False) - # queue.put(event_value) elif message["event"] == "livestream video data": - logMessage(f"on_video") event_value = message[EVENT_CONFIGURATION[event_type]["value"]] - logMessage(f"event_value: {event_value}") event_data_type = EVENT_CONFIGURATION[event_type]["type"] - logMessage(f"event_data_type: {event_data_type}") if event_data_type == "event": - logMessage( - f"##################################################################" - ) - logMessage(f"on_video - {message['serialNumber']}") serialno = message["serialNumber"] if serialno in camera_handlers: for queue in camera_handlers[serialno].video_thread.queues: @@ -504,16 +436,9 @@ async def on_message(message): logMessage(f"Video queue full.") queue.get(False) queue.put(event_value) - # for queue in self.video_thread.queues: - # if queue.full(): - # logMessage(f"Video queue full.") - # queue.get(False) - # queue.put(event_value) elif message["event"] == "livestream error": - logMessage( - f"##################################################################" - ) logMessage(f"Livestream Error! - {payload}") + # TODO: Handle error codes with muliple cameras. # if self.ws and len(self.video_thread.queues) > 0: # msg = START_P2P_LIVESTREAM_MESSAGE.copy() # msg["serialNumber"] = self.serialno @@ -523,7 +448,7 @@ async def on_message(message): logMessage(f"{message}") else: logMessage(f"Unknown message type: {message_type}") - logMessage(f"on_message done") + logMessage(f"{message}") async def init_websocket(ws_security_port): @@ -560,6 +485,11 @@ async def init_websocket(ws_security_port): parser = argparse.ArgumentParser( description="Stream video and audio from multiple Eufy cameras." ) + parser.add_argument( + "--debug", + action="store_true", + help="Enable debug mode (default: disabled).", + ) parser.add_argument( "--camera_serials", nargs="+", @@ -573,15 +503,15 @@ async def init_websocket(ws_security_port): help="Base port number for streaming (default: 3000).", ) args = parser.parse_args() + debug = args.debug logMessage(f"WS Security Port: {args.ws_security_port}") - logMessage(f"Camera Serial Numbers: {args.camera_serials}") # Define constants. BASE_PORT = 63336 # Create one Camera Stream Handler per camera. for i, serial in enumerate(args.camera_serials): - logMessage(f"Creating CameraStreamHandler for camera: {serial}") if serial != "null": + logMessage(f"Creating CameraStreamHandler for camera: {serial}") handler = CameraStreamHandler(serial, BASE_PORT + i * 3, run_event) # handler.setup_sockets() camera_handlers[serial] = handler diff --git a/files/run.sh b/files/run.sh index 5eb6497..f1a8545 100755 --- a/files/run.sh +++ b/files/run.sh @@ -3,8 +3,6 @@ set +u CONFIG_PATH=/data/options.json echo "Starting EufyP2PStream" -echo "Config path is $CONFIG_PATH" -echo "Config content is $(cat $CONFIG_PATH)" EUFY_WS_PORT=$(jq --raw-output ".eufy_security_ws_port" $CONFIG_PATH) CAM1_SN=$(jq --raw-output ".camera_1_serial_number" $CONFIG_PATH) @@ -12,7 +10,18 @@ CAM2_SN=$(jq --raw-output ".camera_2_serial_number" $CONFIG_PATH) CAM3_SN=$(jq --raw-output ".camera_3_serial_number" $CONFIG_PATH) CAM4_SN=$(jq --raw-output ".camera_4_serial_number" $CONFIG_PATH) CAM5_SN=$(jq --raw-output ".camera_5_serial_number" $CONFIG_PATH) +CAM6_SN=$(jq --raw-output ".camera_6_serial_number" $CONFIG_PATH) +CAM7_SN=$(jq --raw-output ".camera_7_serial_number" $CONFIG_PATH) +CAM8_SN=$(jq --raw-output ".camera_8_serial_number" $CONFIG_PATH) +CAM9_SN=$(jq --raw-output ".camera_9_serial_number" $CONFIG_PATH) +CAM10_SN=$(jq --raw-output ".camera_10_serial_number" $CONFIG_PATH) +CAM11_SN=$(jq --raw-output ".camera_11_serial_number" $CONFIG_PATH) +CAM12_SN=$(jq --raw-output ".camera_12_serial_number" $CONFIG_PATH) +CAM13_SN=$(jq --raw-output ".camera_13_serial_number" $CONFIG_PATH) +CAM14_SN=$(jq --raw-output ".camera_14_serial_number" $CONFIG_PATH) +CAM15_SN=$(jq --raw-output ".camera_15_serial_number" $CONFIG_PATH) +DEBUG=$(jq --raw-output ".debug" $CONFIG_PATH) echo "Starting EufyP2PStream. eufy_security_ws_port is $EUFY_WS_PORT" -python3 -u /eufyp2pstream.py --ws_security_port $EUFY_WS_PORT --camera_serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN +python3 -u /eufyp2pstream.py --debug $DEBUG --ws_security_port $EUFY_WS_PORT --camera_serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN $CAM6_SN $CAM7_SN $CAM8_SN $CAM9_SN $CAM10_SN $CAM11_SN $CAM12_SN $CAM13_SN $CAM14_SN $CAM15_SN echo "Exited with code $?" \ No newline at end of file From 2a44f229a1bd39c2ba823de9736e0dfe38429e6b Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 09:35:18 +0100 Subject: [PATCH 38/51] Blacked. --- files/eufyp2pstream.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index dc6c64e..daf0031 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -103,6 +103,7 @@ def logMessage(message, force=False): class ClientAcceptThread(threading.Thread): """Thread to accept incoming connections from clients.""" + def __init__(self, socket, run_event, name, ws, serialno): """Initialize the thread.""" threading.Thread.__init__(self) @@ -166,6 +167,7 @@ def run(self): class ClientSendThread(threading.Thread): """Thread to send data to clients.""" + def __init__(self, client_sock, run_event, name, ws, serialno): """Initialize the thread.""" threading.Thread.__init__(self) @@ -210,6 +212,7 @@ def run(self): class ClientRecvThread(threading.Thread): """Thread to receive data from clients.""" + def __init__(self, client_sock, run_event, name, ws, serialno): """Initialize the thread.""" threading.Thread.__init__(self) @@ -282,8 +285,10 @@ def run(self): asyncio.run(self.ws.send_message(json.dumps(msg))) logMessage(f"Thread {self.name} stopping for {self.serialno}") + class CameraStreamHandler: """Handler for camera streams.""" + def __init__(self, serial_number, start_port, run_event): """Initialize the handler.""" logMessage( @@ -396,7 +401,8 @@ async def on_message(message): logMessage(f"Started stream for camera {serialno}.", True) else: logMessage( - f"Found unknown Eufy camera with serial number {serialno}.", True + f"Found unknown Eufy camera with serial number {serialno}.", + True, ) elif ( message_id == TALKBACK_RESULT_MESSAGE["messageId"] From 4775e70a2ead6561493c6a123b0bac6318e841bb Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 09:45:10 +0100 Subject: [PATCH 39/51] Added missing schema for debug. --- config.yaml | 5 +++-- files/run.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config.yaml b/config.yaml index 409357a..e889288 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.27-beta +version: 0.2.28-beta slug: eufyp2pstream init: false startup: application @@ -14,9 +14,10 @@ map: [config, media] host_network: false options: eufy_security_ws_port: 3000 - debug: false + debug_log: false schema: eufy_security_ws_port: port + debug_log: bool camera_1_serial_number: "str?" camera_2_serial_number: "str?" camera_3_serial_number: "str?" diff --git a/files/run.sh b/files/run.sh index f1a8545..47320a2 100755 --- a/files/run.sh +++ b/files/run.sh @@ -20,7 +20,7 @@ CAM12_SN=$(jq --raw-output ".camera_12_serial_number" $CONFIG_PATH) CAM13_SN=$(jq --raw-output ".camera_13_serial_number" $CONFIG_PATH) CAM14_SN=$(jq --raw-output ".camera_14_serial_number" $CONFIG_PATH) CAM15_SN=$(jq --raw-output ".camera_15_serial_number" $CONFIG_PATH) -DEBUG=$(jq --raw-output ".debug" $CONFIG_PATH) +DEBUG=$(jq --raw-output ".debug_log" $CONFIG_PATH) echo "Starting EufyP2PStream. eufy_security_ws_port is $EUFY_WS_PORT" python3 -u /eufyp2pstream.py --debug $DEBUG --ws_security_port $EUFY_WS_PORT --camera_serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN $CAM6_SN $CAM7_SN $CAM8_SN $CAM9_SN $CAM10_SN $CAM11_SN $CAM12_SN $CAM13_SN $CAM14_SN $CAM15_SN From a098de78702fc1c3f2d0c40149b42e1606476815 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 09:54:10 +0100 Subject: [PATCH 40/51] Corrected debug argument. --- config.yaml | 2 +- files/run.sh | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config.yaml b/config.yaml index e889288..1fb6bd8 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.28-beta +version: 0.2.29-beta slug: eufyp2pstream init: false startup: application diff --git a/files/run.sh b/files/run.sh index 47320a2..001e681 100755 --- a/files/run.sh +++ b/files/run.sh @@ -21,7 +21,12 @@ CAM13_SN=$(jq --raw-output ".camera_13_serial_number" $CONFIG_PATH) CAM14_SN=$(jq --raw-output ".camera_14_serial_number" $CONFIG_PATH) CAM15_SN=$(jq --raw-output ".camera_15_serial_number" $CONFIG_PATH) DEBUG=$(jq --raw-output ".debug_log" $CONFIG_PATH) +if [ "$DEBUG" == "true" ]; then + DEBUG="--debug" +else + DEBUG="" +fi echo "Starting EufyP2PStream. eufy_security_ws_port is $EUFY_WS_PORT" -python3 -u /eufyp2pstream.py --debug $DEBUG --ws_security_port $EUFY_WS_PORT --camera_serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN $CAM6_SN $CAM7_SN $CAM8_SN $CAM9_SN $CAM10_SN $CAM11_SN $CAM12_SN $CAM13_SN $CAM14_SN $CAM15_SN +python3 -u /eufyp2pstream.py $DEBUG --ws_security_port $EUFY_WS_PORT --camera_serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN $CAM6_SN $CAM7_SN $CAM8_SN $CAM9_SN $CAM10_SN $CAM11_SN $CAM12_SN $CAM13_SN $CAM14_SN $CAM15_SN echo "Exited with code $?" \ No newline at end of file From 03906065d2d788bb99481a669d36240638568cff Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 10:26:29 +0100 Subject: [PATCH 41/51] Debugging talkback error. --- config.yaml | 2 +- files/eufyp2pstream.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.yaml b/config.yaml index 1fb6bd8..f8fdd6b 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.29-beta +version: 0.2.30-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index daf0031..6961042 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -410,7 +410,7 @@ async def on_message(message): ): error_code = payload["errorCode"] logMessage(f"Talkback error: {error_code}") - logMessage(f"{payload}", True) + logMessage(f"{message}", True) # TODO: Handle error codes with muliple cameras. # if error_code == "device_talkback_not_running": # msg = START_TALKBACK.copy() @@ -443,7 +443,7 @@ async def on_message(message): queue.get(False) queue.put(event_value) elif message["event"] == "livestream error": - logMessage(f"Livestream Error! - {payload}") + logMessage(f"Livestream Error! - {message}") # TODO: Handle error codes with muliple cameras. # if self.ws and len(self.video_thread.queues) > 0: # msg = START_P2P_LIVESTREAM_MESSAGE.copy() From 41d4195c0c56c836c3d26f7dd208254b6cdf6faf Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 10:37:40 +0100 Subject: [PATCH 42/51] Trying to fix talkback error. --- config.yaml | 2 +- files/eufyp2pstream.py | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/config.yaml b/config.yaml index f8fdd6b..016cbb2 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.30-beta +version: 0.2.31-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 6961042..23eeeb7 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -360,6 +360,13 @@ def stop(self): print("Error shutdown socket") self.backchannel_sock.close() + def start_talkback(self): + """Start the talkback.""" + logMessage(f"Starting talkback for camera {self.serial_number}.") + msg = START_TALKBACK.copy() + msg["serialNumber"] = self.serial_number + asyncio.run(self.ws.send_message(json.dumps(msg))) + async def on_open(): """Callback when the websocket is opened.""" @@ -408,14 +415,15 @@ async def on_message(message): message_id == TALKBACK_RESULT_MESSAGE["messageId"] and "errorCode" in payload ): + # TODO: Handle error codes with muliple cameras. This one is tricky since + # the error code is not specific to a camera. Alternatives: 1) Send the + # START_TALKBACK message again for all cameras. 2) Somehow determine which + # camera caused the error and send the START_TALKBACK message for that camera. + # Don't know if 2) is possible. error_code = payload["errorCode"] - logMessage(f"Talkback error: {error_code}") - logMessage(f"{message}", True) - # TODO: Handle error codes with muliple cameras. - # if error_code == "device_talkback_not_running": - # msg = START_TALKBACK.copy() - # msg["serialNumber"] = self.serialno - # await self.ws.send_message(json.dumps(msg)) + if error_code == "device_talkback_not_running": + for handler in camera_handlers.values(): + handler.start_talkback() elif message_type == "event": message = payload[message_type] From ddcc8d61da55a9b4eee3528b5ab2c657f07a175b Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 10:53:59 +0100 Subject: [PATCH 43/51] Turned of talkback fix as it breaks all streaming. --- config.yaml | 2 +- files/eufyp2pstream.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/config.yaml b/config.yaml index 016cbb2..60852a2 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.31-beta +version: 0.2.32-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 23eeeb7..b46b271 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -420,11 +420,12 @@ async def on_message(message): # START_TALKBACK message again for all cameras. 2) Somehow determine which # camera caused the error and send the START_TALKBACK message for that camera. # Don't know if 2) is possible. - error_code = payload["errorCode"] - if error_code == "device_talkback_not_running": - for handler in camera_handlers.values(): - handler.start_talkback() - + # 1) is not ideal. It seems to break streaming completely. + # error_code = payload["errorCode"] + # if error_code == "device_talkback_not_running": + # for handler in camera_handlers.values(): + # handler.start_talkback() + pass elif message_type == "event": message = payload[message_type] event_type = message["event"] From 1fc131964dedaf3276f8cde3d7e3020484cea28e Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 11:09:18 +0100 Subject: [PATCH 44/51] Add-on fails to start if debug_log is set to false. Debugging. --- config.yaml | 2 +- files/run.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index 60852a2..cf1960e 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.32-beta +version: 0.2.33-beta slug: eufyp2pstream init: false startup: application diff --git a/files/run.sh b/files/run.sh index 001e681..b2826fd 100755 --- a/files/run.sh +++ b/files/run.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x set +u CONFIG_PATH=/data/options.json From 1b1047aa6e07cd8d590160d10f4e6d75ebba5e0c Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 11:16:37 +0100 Subject: [PATCH 45/51] Try changing name if DEBUG is special. --- config.yaml | 2 +- files/eufyp2pstream.py | 3 ++- files/run.sh | 10 +++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/config.yaml b/config.yaml index cf1960e..a35e13d 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.33-beta +version: 0.2.34-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index b46b271..8b9ffd2 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -509,7 +509,7 @@ async def init_websocket(ws_security_port): "--camera_serials", nargs="+", required=True, - help="List of camera serial numbers (e.g., --serials CAM1_SERIAL CAM2_SERIAL).", + help="List of camera serial numbers (e.g., --camera_serials CAM1_SERIAL CAM2_SERIAL).", ) parser.add_argument( "--ws_security_port", @@ -519,6 +519,7 @@ async def init_websocket(ws_security_port): ) args = parser.parse_args() debug = args.debug + print(f"Debug: {debug}") logMessage(f"WS Security Port: {args.ws_security_port}") # Define constants. diff --git a/files/run.sh b/files/run.sh index b2826fd..9ac5374 100755 --- a/files/run.sh +++ b/files/run.sh @@ -21,13 +21,13 @@ CAM12_SN=$(jq --raw-output ".camera_12_serial_number" $CONFIG_PATH) CAM13_SN=$(jq --raw-output ".camera_13_serial_number" $CONFIG_PATH) CAM14_SN=$(jq --raw-output ".camera_14_serial_number" $CONFIG_PATH) CAM15_SN=$(jq --raw-output ".camera_15_serial_number" $CONFIG_PATH) -DEBUG=$(jq --raw-output ".debug_log" $CONFIG_PATH) -if [ "$DEBUG" == "true" ]; then - DEBUG="--debug" +DEBUG_LOG=$(jq --raw-output ".debug_log" $CONFIG_PATH) +if [ "$DEBUG_LOG" == "true" ]; then + DEBUG_LOG="--debug" else - DEBUG="" + DEBUG_LOG="" fi echo "Starting EufyP2PStream. eufy_security_ws_port is $EUFY_WS_PORT" -python3 -u /eufyp2pstream.py $DEBUG --ws_security_port $EUFY_WS_PORT --camera_serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN $CAM6_SN $CAM7_SN $CAM8_SN $CAM9_SN $CAM10_SN $CAM11_SN $CAM12_SN $CAM13_SN $CAM14_SN $CAM15_SN +python3 -u /eufyp2pstream.py $DEBUG_LOG --ws_security_port $EUFY_WS_PORT --camera_serials $CAM1_SN $CAM2_SN $CAM3_SN $CAM4_SN $CAM5_SN $CAM6_SN $CAM7_SN $CAM8_SN $CAM9_SN $CAM10_SN $CAM11_SN $CAM12_SN $CAM13_SN $CAM14_SN $CAM15_SN echo "Exited with code $?" \ No newline at end of file From fb1761f0cd106d9ff1d76f76404f401a3f7ae77c Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 11:25:41 +0100 Subject: [PATCH 46/51] Tried adding unbuffered output. --- config.yaml | 2 +- files/eufyp2pstream.py | 1 + files/run.sh | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index a35e13d..7d3de20 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.34-beta +version: 0.2.35-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index 8b9ffd2..c9ed7f4 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -520,6 +520,7 @@ async def init_websocket(ws_security_port): args = parser.parse_args() debug = args.debug print(f"Debug: {debug}") + sys.stdout.flush() logMessage(f"WS Security Port: {args.ws_security_port}") # Define constants. diff --git a/files/run.sh b/files/run.sh index 9ac5374..ff90408 100755 --- a/files/run.sh +++ b/files/run.sh @@ -1,4 +1,6 @@ #!/bin/bash +export PYTHONUNBUFFERED=1 +export FORCE_UNBUFFERED=1 set -x set +u CONFIG_PATH=/data/options.json From 43e26ae7ff40e807a02a0ddca42d4405f93283f8 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 11:36:53 +0100 Subject: [PATCH 47/51] Cleaned up. --- config.yaml | 2 +- files/eufyp2pstream.py | 2 -- files/run.sh | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/config.yaml b/config.yaml index 7d3de20..91af4d6 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.35-beta +version: 0.2.36-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index c9ed7f4..c21e555 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -519,8 +519,6 @@ async def init_websocket(ws_security_port): ) args = parser.parse_args() debug = args.debug - print(f"Debug: {debug}") - sys.stdout.flush() logMessage(f"WS Security Port: {args.ws_security_port}") # Define constants. diff --git a/files/run.sh b/files/run.sh index ff90408..efa75ba 100755 --- a/files/run.sh +++ b/files/run.sh @@ -1,7 +1,6 @@ #!/bin/bash export PYTHONUNBUFFERED=1 export FORCE_UNBUFFERED=1 -set -x set +u CONFIG_PATH=/data/options.json From 896c24eef8050cd8b1df279807ec65563dfe1a0d Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 11:45:43 +0100 Subject: [PATCH 48/51] Try changing interpreter to home assistant docs. --- config.yaml | 2 +- files/eufyp2pstream.py | 2 ++ files/run.sh | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config.yaml b/config.yaml index 91af4d6..9dc0e61 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.36-beta +version: 0.2.37-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index c21e555..c9ed7f4 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -519,6 +519,8 @@ async def init_websocket(ws_security_port): ) args = parser.parse_args() debug = args.debug + print(f"Debug: {debug}") + sys.stdout.flush() logMessage(f"WS Security Port: {args.ws_security_port}") # Define constants. diff --git a/files/run.sh b/files/run.sh index efa75ba..4c5e6ae 100755 --- a/files/run.sh +++ b/files/run.sh @@ -1,6 +1,5 @@ -#!/bin/bash +#!/usr/bin/with-contenv bashio export PYTHONUNBUFFERED=1 -export FORCE_UNBUFFERED=1 set +u CONFIG_PATH=/data/options.json From 6b9d3bda6eee11bd016d96224692e8e306bde591 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Mon, 25 Nov 2024 12:12:20 +0100 Subject: [PATCH 49/51] Added sys flush at same places as single camera version. --- config.yaml | 2 +- files/eufyp2pstream.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/config.yaml b/config.yaml index 9dc0e61..3d285f6 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ description: Eufy P2P camera streaming application arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.37-beta +version: 0.2.38-beta slug: eufyp2pstream init: false startup: application diff --git a/files/eufyp2pstream.py b/files/eufyp2pstream.py index c9ed7f4..10dec45 100644 --- a/files/eufyp2pstream.py +++ b/files/eufyp2pstream.py @@ -140,6 +140,7 @@ def run(self): logMessage(f"stop talkback sent for {self.serialno}") while not self.run_event.is_set(): self.update_threads() + sys.stdout.flush() try: client_sock, client_addr = self.socket.accept() if self.name == "BackChannel": @@ -347,17 +348,17 @@ def stop(self): try: self.video_sock.shutdown(socket.SHUT_RDWR) except OSError: - print("Error shutdown socket") + logMessage("Error shutdown socket", True) self.video_sock.close() try: self.audio_sock.shutdown(socket.SHUT_RDWR) except OSError: - print("Error shutdown socket") + logMessage("Error shutdown socket", True) self.audio_sock.close() try: self.backchannel_sock.shutdown(socket.SHUT_RDWR) except OSError: - print("Error shutdown socket") + logMessage("Error shutdown socket", True) self.backchannel_sock.close() def start_talkback(self): @@ -392,6 +393,7 @@ async def on_message(message): """Callback when a message is received.""" payload = message.json() message_type: str = payload["type"] + sys.stdout.flush() if message_type == "result": message_id = payload["messageId"] if message_id != SEND_TALKBACK_AUDIO_DATA["messageId"]: @@ -429,6 +431,7 @@ async def on_message(message): elif message_type == "event": message = payload[message_type] event_type = message["event"] + sys.stdout.flush() if message["event"] == "livestream audio data": event_value = message[EVENT_CONFIGURATION[event_type]["value"]] event_data_type = EVENT_CONFIGURATION[event_type]["type"] From ffaa14ff5a5e32b57ca05901a5ffbda3d2432e56 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Tue, 26 Nov 2024 08:51:03 +0100 Subject: [PATCH 50/51] Restored repo path for PR. --- repository.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repository.yaml b/repository.yaml index 2a16802..9837e34 100644 --- a/repository.yaml +++ b/repository.yaml @@ -1,3 +1,3 @@ name: Eufy P2P Stream -url: https://github.com/neerdoc/eufyp2pstream +url: https://github.com/oischinger/eufyp2pstream maintainer: Hans Oischinger From 6a806a28217c236304861e34a7758767a6f60da6 Mon Sep 17 00:00:00 2001 From: Gustav Wulf Date: Tue, 26 Nov 2024 08:52:27 +0100 Subject: [PATCH 51/51] New version and restored url. --- config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.yaml b/config.yaml index 3d285f6..2cff105 100644 --- a/config.yaml +++ b/config.yaml @@ -1,11 +1,11 @@ # https://github.com/AlexxIT/builder/blob/master/builder.sh name: eufyp2pstream description: Eufy P2P camera streaming application -#url: http://192.168.178.252:8123/local_eufyp2pstream +url: http://192.168.178.252:8123/local_eufyp2pstream arch: [amd64, aarch64, i386, armv7] # https://developers.home-assistant.io/docs/add-ons/configuration -version: 0.2.38-beta +version: 0.3.0-beta slug: eufyp2pstream init: false startup: application