From 3b473bcfa513f7cade3a9d3248c210e92cb49e9e Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Sat, 17 May 2025 21:45:30 +0200 Subject: [PATCH 1/4] Attempt to do audio, audio itself is not playable --- aiopppp/session.py | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/aiopppp/session.py b/aiopppp/session.py index 7607923..2119b5f 100644 --- a/aiopppp/session.py +++ b/aiopppp/session.py @@ -5,6 +5,7 @@ from enum import Enum from typing import Callable +from .adpcm import decode from .const import JSON_COMMAND_NAMES, PTZ, BinaryCommands, JsonCommands, PacketType from .encrypt import ENC_METHODS from .exceptions import AuthError, CommandResultError @@ -64,8 +65,10 @@ class VideoQueueMixin: def __init__(self, *args, **kwargs): super().__init__() self.video_chunk_queue = asyncio.Queue() + self.audio_chunk_queue = asyncio.Queue() self.frame_buffer = SharedFrameBuffer() self.process_video_task = None + self.process_audio_task = None self.last_drw_pkt_idx = 0 self.video_epoch = 0 # number of overflows over 0xffff DRW index @@ -78,9 +81,17 @@ async def process_video_queue(self): pkt_epoch, pkt = await self.video_chunk_queue.get() await self.handle_incoming_video_packet(pkt_epoch, pkt) + async def process_audio_queue(self): + while True: + pkt_epoch, pkt = await self.audio_chunk_queue.get() + await self.handle_incoming_audio_packet(pkt_epoch, pkt) + def start_video_queue(self): self.process_video_task = asyncio.create_task(self.process_video_queue()) + def start_audio_queue(self): + self.process_audio_task = asyncio.create_task(self.process_audio_queue()) + async def handle_incoming_video_packet(self, pkt_epoch, pkt): video_payload = pkt.get_drw_payload() # logger.info(f'- video frame {pkt._cmd_idx}') @@ -96,6 +107,17 @@ async def handle_incoming_video_packet(self, pkt_epoch, pkt): self.video_received[video_chunk_idx] = video_payload await self.process_video_frame() + async def handle_incoming_audio_packet(self, pkt_epoch, pkt): + audio_payload = pkt.get_drw_payload() + + audio_marker = b'\x55\xaa\x15\xa8' + if audio_payload.startswith(audio_marker): + actual_payload = audio_payload[0x20:] + else: + actual_payload = audio_payload + + open('/tmp/test.raw', 'ab').write(actual_payload) + async def process_video_frame(self): if len(self.video_boundaries) <= 1: return @@ -219,6 +241,7 @@ async def start_video(self): logger.info('Start video') self.last_drw_pkt_at = datetime.datetime.now() await self._request_video(1) + await self._request_audio(1) self.is_video_requested = True async def stop_video(self): @@ -231,7 +254,10 @@ async def stop_video(self): self.last_video_frame = -1 while not self.video_chunk_queue.empty(): self.video_chunk_queue.get_nowait() + while not self.audio_chunk_queue.empty(): + self.audio_chunk_queue.get_nowait() await self._request_video(0) + await self._request_audio(0) async def _request_video(self, mode): """ @@ -319,11 +345,12 @@ def start(self): self.device_is_ready.clear() self.start_packet_queue() self.start_video_queue() + self.start_audio_queue() self.main_task = asyncio.create_task(self._run()) return self.main_task def running_tasks(self): - return tuple(x for x in (self.main_task, self.process_packet_task, self.process_video_task) if x) + return tuple(x for x in (self.main_task, self.process_packet_task, self.process_video_task, self.process_audio_task) if x) def _on_device_lost(self): logger.warning('Device %s lost', self.dev.dev_id) @@ -338,6 +365,7 @@ def stop(self): self.device_is_ready.set() self.process_packet_task.cancel() self.process_video_task.cancel() + self.process_audio_task.cancel() self.main_task.cancel() self.transport.close() self.transport = None @@ -417,7 +445,8 @@ async def handle_drw(self, drw_pkt): self.video_stale_at = None self.video_chunk_queue.put_nowait((pkt_epoch, drw_pkt)) elif drw_pkt._channel == Channel.Audio: - pass + # logger.debug(f'Got audio data {drw_pkt.get_drw_payload()}') + self.audio_chunk_queue.put_nowait((pkt_epoch, drw_pkt)) elif drw_pkt._channel == Channel.Command: await self.handle_incoming_command_packet(drw_pkt) @@ -482,6 +511,7 @@ async def loop_step(self): self.video_stale_at = self.last_drw_pkt_at logger.info('No video for 5 seconds. Re-request video ') await self._request_video(1) + await self._request_audio(1) if self.video_stale_at and (datetime.datetime.now() - self.video_stale_at).total_seconds() > 10: # camera disconnected logger.warning('No video for 10 seconds. Disconnecting') @@ -546,6 +576,8 @@ class BinarySession(Session): BinaryCommands.CMD_PEER_VIDEOPARAM_SET: BinaryCommands.ACK_PEER_VIDEOPARAM_SET, BinaryCommands.CMD_PEER_LIVEVIDEO_START: BinaryCommands.ACK_PEER_LIVEVIDEO_START, BinaryCommands.CMD_PEER_LIVEVIDEO_STOP: BinaryCommands.ACK_PEER_LIVEVIDEO_STOP, + BinaryCommands.CMD_PEER_LIVEAUDIO_START: BinaryCommands.ACK_PEER_LIVEAUDIO_START, + BinaryCommands.CMD_PEER_LIVEAUDIO_STOP: BinaryCommands.ACK_PEER_LIVEAUDIO_STOP, BinaryCommands.CMD_SYSTEM_STATUS_GET: BinaryCommands.ACK_SYSTEM_STATUS_GET, } REV_ACKS = {v: k for k, v in ACKS.items()} @@ -583,7 +615,7 @@ async def handle_drw(self, drw_pkt): self.video_stale_at = None self.video_chunk_queue.put_nowait((pkt_epoch, drw_pkt)) elif drw_pkt._channel == Channel.Audio: - pass + self.audio_chunk_queue.put_nowait((pkt_epoch, drw_pkt)) elif drw_pkt._channel == Channel.Command: await self.handle_incoming_command_packet(drw_pkt) @@ -678,6 +710,13 @@ async def _request_video(self, mode): else: await self.send_command(BinaryCommands.CMD_PEER_LIVEVIDEO_STOP, b'', with_response=True) + async def _request_audio(self, mode): + logger.info('Request audio %s', mode) + if mode: + await self.send_command(BinaryCommands.CMD_PEER_LIVEAUDIO_START, b'', with_response=True) + else: + await self.send_command(BinaryCommands.CMD_PEER_LIVEAUDIO_STOP, b'', with_response=True) + async def login(self): # type is char account[0x20]; char password[0x80]; payload = struct.pack('>32s128s', self.auth_login.encode('utf-8'), self.auth_password.encode('utf-8')) From fdd59cd4a02381aaf7f7ab3b0c0c1bbe7fb0e2b1 Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Sat, 17 May 2025 21:52:05 +0200 Subject: [PATCH 2/4] This file was converted from NodeJS by ChatGPT --- aiopppp/adpcm.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 aiopppp/adpcm.py diff --git a/aiopppp/adpcm.py b/aiopppp/adpcm.py new file mode 100644 index 0000000..39afd87 --- /dev/null +++ b/aiopppp/adpcm.py @@ -0,0 +1,66 @@ +import struct +from pathlib import Path + +index_table = [-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8] +stepsize_table = [ + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, + 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, + 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, + 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, + 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767, +] + + +def decode(indata: bytes) -> bytes: + state_index = 0 + state_valpred = stepsize_table[state_index] + + outbuf = bytearray(len(indata) * 4) # 2 samples per byte, 2 bytes per sample + + inp = 0 + outp = 0 + + step = stepsize_table[state_index] + valpred = state_valpred + index = state_index + bufferstep = False + inputbuffer = 0 + + for _ in range(len(indata) * 2): + if bufferstep: + delta = inputbuffer & 0xF + else: + inputbuffer = indata[inp] + inp += 1 + delta = (inputbuffer >> 4) & 0xF + bufferstep = not bufferstep + + index += index_table[delta] + index = max(0, min(index, 88)) + + sign = delta & 8 + delta = delta & 7 + + vpdiff = step >> 3 + if delta & 4: + vpdiff += step + if delta & 2: + vpdiff += step >> 1 + if delta & 1: + vpdiff += step >> 2 + + if sign: + valpred -= vpdiff + else: + valpred += vpdiff + + valpred = max(-32768, min(32767, valpred)) + step = stepsize_table[index] + + struct.pack_into(' Date: Sat, 17 May 2025 23:40:10 +0200 Subject: [PATCH 3/4] Maybe the right direction --- aiopppp/session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiopppp/session.py b/aiopppp/session.py index 2119b5f..43d7852 100644 --- a/aiopppp/session.py +++ b/aiopppp/session.py @@ -75,6 +75,7 @@ def __init__(self, *args, **kwargs): self.video_received = {} self.video_boundaries = set() self.last_video_frame = -1 + self.last_audio_frame = -1 async def process_video_queue(self): while True: @@ -109,13 +110,14 @@ async def handle_incoming_video_packet(self, pkt_epoch, pkt): async def handle_incoming_audio_packet(self, pkt_epoch, pkt): audio_payload = pkt.get_drw_payload() - audio_marker = b'\x55\xaa\x15\xa8' + if audio_payload.startswith(audio_marker): actual_payload = audio_payload[0x20:] else: actual_payload = audio_payload + # open('/tmp/test.raw', 'ab').write(decode(actual_payload)) open('/tmp/test.raw', 'ab').write(actual_payload) async def process_video_frame(self): From bba61744a1e4b182da0a8078adb1e3c5066b9bd7 Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Sat, 17 May 2025 23:57:00 +0200 Subject: [PATCH 4/4] Remove adpcm --- aiopppp/adpcm.py | 66 ---------------------------------------------- aiopppp/session.py | 3 +-- 2 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 aiopppp/adpcm.py diff --git a/aiopppp/adpcm.py b/aiopppp/adpcm.py deleted file mode 100644 index 39afd87..0000000 --- a/aiopppp/adpcm.py +++ /dev/null @@ -1,66 +0,0 @@ -import struct -from pathlib import Path - -index_table = [-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8] -stepsize_table = [ - 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, - 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, - 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, - 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, - 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, - 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, - 32767, -] - - -def decode(indata: bytes) -> bytes: - state_index = 0 - state_valpred = stepsize_table[state_index] - - outbuf = bytearray(len(indata) * 4) # 2 samples per byte, 2 bytes per sample - - inp = 0 - outp = 0 - - step = stepsize_table[state_index] - valpred = state_valpred - index = state_index - bufferstep = False - inputbuffer = 0 - - for _ in range(len(indata) * 2): - if bufferstep: - delta = inputbuffer & 0xF - else: - inputbuffer = indata[inp] - inp += 1 - delta = (inputbuffer >> 4) & 0xF - bufferstep = not bufferstep - - index += index_table[delta] - index = max(0, min(index, 88)) - - sign = delta & 8 - delta = delta & 7 - - vpdiff = step >> 3 - if delta & 4: - vpdiff += step - if delta & 2: - vpdiff += step >> 1 - if delta & 1: - vpdiff += step >> 2 - - if sign: - valpred -= vpdiff - else: - valpred += vpdiff - - valpred = max(-32768, min(32767, valpred)) - step = stepsize_table[index] - - struct.pack_into('