recorder-mx-py

[MIRROR of https://git.nordgedanken.dev/mtrnord/matrix-call-multitrack-recorder/] A tool to record every participant of a legacy or new matrix, call to separate audio and video files.
git clone git://archive.git.mtrnord.blog/MTRNord/recorder-mx-py.git
Log | Files | Refs | README | LICENSE

commit f7312b84970b99cf847944b27b6af48e86b125ee
parent d23b54a772bc133eab09ba681d9eca01d968ae1d
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sun, 29 Jan 2023 18:43:39 +0100

Further state tracking fixes

Diffstat:
Mmatrix_call_multitrack_recorder/recorder.py | 129++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
1 file changed, 75 insertions(+), 54 deletions(-)

diff --git a/matrix_call_multitrack_recorder/recorder.py b/matrix_call_multitrack_recorder/recorder.py @@ -36,7 +36,6 @@ logger.level = logbook.INFO # directory to store recordings RECORDING_PATH = "./recordings/" # local directory -# TODO: do not rely on room id UserID = str ConfID = str RoomID = str @@ -63,6 +62,7 @@ class Recorder: client: AsyncClient loop: asyncio.AbstractEventLoop conf_room: Dict[ConfID, MatrixRoom] + recording_rooms: List[ConfID] room_conf: Dict[RoomID, ConfID] others: Dict[ConfID, List[Others]] @@ -75,6 +75,7 @@ class Recorder: self.conf_room = {} self.room_conf = {} self.others = {} + self.recording_rooms = list() self.party_id = "".join( random.choices(string.ascii_letters + string.digits, k=8) ) @@ -222,32 +223,31 @@ class Recorder: call_id = state["state_key"] logger.info(f"Found call id {call_id} for {room.room_id}") self.add_call(call_id, room) - await self.client.room_put_state( - room.room_id, - "org.matrix.msc3401.call.member", - { - "m.calls": [ - { - # TODO: Invalid call_id - "m.call_id": self.room_conf[room.room_id], - "m.devices": [ - { - "device_id": self.client.device_id, - "expires_ts": int(time.time() * 1000) - + (1000 * 60 * 60), - "session_id": f"{self.client.user_id}_{self.client.device_id}_session", - "feeds": [ - { - "purpose": "m.usermedia", - } - ], - } - ], - } - ] - }, - state_key=self.client.user_id, - ) + await self.client.room_put_state( + room.room_id, + "org.matrix.msc3401.call.member", + { + "m.calls": [ + { + "m.call_id": self.room_conf[room.room_id], + "m.devices": [ + { + "device_id": self.client.device_id, + "expires_ts": int(time.time() * 1000) + + (1000 * 60 * 60), + "session_id": f"{self.client.user_id}_{self.client.device_id}_session", + "feeds": [ + { + "purpose": "m.usermedia", + } + ], + } + ], + } + ] + }, + state_key=self.client.user_id, + ) async def handle_call_invite( self, @@ -290,26 +290,32 @@ class Recorder: # output_tracks = {"audio": MediaBlackhole(), "video": MediaBlackhole()} async def task() -> None: + if isinstance(event, CallInviteEvent): + conf_path = RECORDING_PATH + else: + conf_path = os.path.join(RECORDING_PATH, f"{event.conf_id}") + if not os.path.exists(conf_path): + os.mkdir(conf_path) base_name_audio = f"{event.sender}_{event.call_id}" base_name_video = f"{event.sender}_{event.call_id}" - if os.path.exists(os.path.join(RECORDING_PATH, f"{base_name_audio}.wav")): + if os.path.exists(os.path.join(conf_path, f"{base_name_audio}.wav")): i = 1 while os.path.exists( - os.path.join(RECORDING_PATH, f"{base_name_audio}_{i}.wav") + os.path.join(conf_path, f"{base_name_audio}_{i}.wav") ): i += 1 base_name_audio = f"{base_name_audio}_{i}" - if os.path.exists(os.path.join(RECORDING_PATH, f"{base_name_video}.mp4")): + if os.path.exists(os.path.join(conf_path, f"{base_name_video}.mp4")): i = 1 while os.path.exists( - os.path.join(RECORDING_PATH, f"{base_name_video}_{i}.mp4") + os.path.join(conf_path, f"{base_name_video}_{i}.mp4") ): i += 1 base_name_video = f"{base_name_video}_{i}" - wav_file = os.path.join(RECORDING_PATH, f"{base_name_audio}.wav") - mp4_file = os.path.join(RECORDING_PATH, f"{base_name_video}.mp4") + wav_file = os.path.join(conf_path, f"{base_name_audio}.wav") + mp4_file = os.path.join(conf_path, f"{base_name_video}.mp4") audio_recorder = MediaRecorder(wav_file, format="wav") audio_recorder.addTrack(input_tracks["audio"]) await audio_recorder.start() @@ -465,26 +471,30 @@ class Recorder: ) await self.client.to_device(to_device_message) if room: - await self.client.room_send( - room.room_id, - "m.room.message", - { - "msgtype": "m.notice", - "body": "Successfully started recording", - }, - ignore_unverified_devices=True, - ) + if event.call_id not in self.recording_rooms: + self.recording_rooms.append(event.call_id) + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.notice", + "body": "Successfully started recording", + }, + ignore_unverified_devices=True, + ) logger.info(f"Sent answer to {event.sender} in {room.room_id}") elif isinstance(event, ToDeviceCallInviteEvent): - await self.client.room_send( - self.conf_room[event.conf_id].room_id, - "m.room.message", - { - "msgtype": "m.notice", - "body": "Successfully started recording", - }, - ignore_unverified_devices=True, - ) + if event.conf_id not in self.recording_rooms: + self.recording_rooms.append(event.conf_id) + await self.client.room_send( + self.conf_room[event.conf_id].room_id, + "m.room.message", + { + "msgtype": "m.notice", + "body": "Successfully started recording", + }, + ignore_unverified_devices=True, + ) logger.info( f"Sent answer to {event.sender} with device {event.source['sender_device']}" ) @@ -494,8 +504,8 @@ class Recorder: room: Optional[MatrixRoom], event: Union[CallCandidatesEvent, ToDeviceCallCandidatesEvent], ) -> None: - logger.info("Delaying candidates for 3 seconds") - await asyncio.sleep(3) + # logger.info("Delaying candidates for 3 seconds") + # await asyncio.sleep(3) if room: logger.info( f"Received call candidates from {event.sender} in {room.room_id}" @@ -507,6 +517,14 @@ class Recorder: unique_id: UniqueCallID = (event.sender, event.call_id) else: unique_id: UniqueCallID = (event.sender, event.conf_id) + + if event.conf_id not in self.conf_room: + logger.warning("Got invalid to-device call candidates") + return + while unique_id not in self.conns: + logger.info("Waiting for connection to be created") + await asyncio.sleep(0.2) + try: conn = self.conns[unique_id] except KeyError as e: @@ -518,7 +536,10 @@ class Recorder: for raw_candidate in event.candidates: if not raw_candidate.get("candidate"): # End of candidates - conn.candidate_waiter.set_result(None) + try: + conn.candidate_waiter.set_result(None) + except asyncio.InvalidStateError: + pass break try: candidate = candidate_from_aioice(