Просмотр исходного кода

voice messages: convert incoming voice messages to ogg

Sumner Evans 3 лет назад
Родитель
Сommit
d6978f2287
3 измененных файлов с 75 добавлено и 9 удалено
  1. 14 9
      mautrix_signal/portal.py
  2. 1 0
      mautrix_signal/util/__init__.py
  3. 60 0
      mautrix_signal/util/audio_convert.py

+ 14 - 9
mautrix_signal/portal.py

@@ -80,7 +80,7 @@ from .db import (
     Reaction as DBReaction,
 )
 from .formatter import matrix_to_signal, signal_to_matrix
-from .util import id_to_str
+from .util import id_to_str, to_ogg
 
 if TYPE_CHECKING:
     from .__main__ import SignalBridge
@@ -833,7 +833,9 @@ class Portal(DBPortal, BasePortal):
             self.log.debug(f"Didn't get event ID for {message.timestamp}")
 
     @staticmethod
-    def _make_media_content(attachment: Attachment) -> MediaMessageEventContent:
+    async def _make_media_content(
+        attachment: Attachment, data: bytes
+    ) -> tuple[MediaMessageEventContent, bytes]:
         if attachment.content_type.startswith("image/"):
             msgtype = MessageType.IMAGE
             info = ImageInfo(
@@ -846,7 +848,9 @@ class Portal(DBPortal, BasePortal):
             )
         elif attachment.voice_note or attachment.content_type.startswith("audio/"):
             msgtype = MessageType.AUDIO
-            info = AudioInfo(mimetype=attachment.content_type)
+            info = AudioInfo(
+                mimetype=attachment.content_type if not attachment.voice_note else "audio/ogg"
+            )
         else:
             msgtype = MessageType.FILE
             info = FileInfo(mimetype=attachment.content_type)
@@ -860,7 +864,7 @@ class Portal(DBPortal, BasePortal):
             msgtype=msgtype, info=info, body=attachment.custom_filename
         )
 
-        # Add the additional voice message metadata.
+        # If this is a voice note, add the additional voice message metadata and convert to OGG.
         if attachment.voice_note:
             content["org.matrix.msc1767.file"] = {
                 "url": content.url,
@@ -869,8 +873,9 @@ class Portal(DBPortal, BasePortal):
                 **(content.info.serialize() if content.info else {}),
             }
             content["org.matrix.msc3245.voice"] = {}
+            data = await to_ogg(data, attachment.content_type)
 
-        return content
+        return content, data
 
     async def _handle_signal_attachment(
         self, intent: IntentAPI, attachment: Attachment, sticker: bool = False
@@ -883,15 +888,15 @@ class Portal(DBPortal, BasePortal):
                 else "application/octet-stream"
             )
 
-        content = self._make_media_content(attachment)
-        if sticker:
-            self._adjust_sticker_size(content.info)
-
         with open(attachment.incoming_filename, "rb") as file:
             data = file.read()
         if self.config["signal.remove_file_after_handling"]:
             os.remove(attachment.incoming_filename)
 
+        content, data = await self._make_media_content(attachment, data)
+        if sticker:
+            self._adjust_sticker_size(content.info)
+
         await self._upload_attachment(intent, content, data, attachment.id)
         return content
 

+ 1 - 0
mautrix_signal/util/__init__.py

@@ -1,2 +1,3 @@
+from .audio_convert import to_ogg
 from .color_log import ColorFormatter
 from .id_to_str import id_to_str

+ 60 - 0
mautrix_signal/util/audio_convert.py

@@ -0,0 +1,60 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2021 Tulir Asokan, Sumner Evans
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+from typing import Optional
+import asyncio
+import logging
+import mimetypes
+import os
+import shutil
+import tempfile
+
+log = logging.getLogger("mau.util.audio")
+
+
+def abswhich(program: Optional[str]) -> Optional[str]:
+    if program is None:
+        return None
+    path = shutil.which(program)
+    return os.path.abspath(path) if path else None
+
+
+async def to_ogg(data: bytes, mime: str) -> Optional[bytes]:
+    default_ext = mimetypes.guess_extension(mime)
+    with tempfile.TemporaryDirectory(prefix="mxsignal_audio_") as tmpdir:
+        input_file_name = os.path.join(tmpdir, f"input{default_ext}")
+        output_file_name = os.path.join(tmpdir, "output.ogg")
+        with open(input_file_name, "wb") as file:
+            file.write(data)
+        proc = await asyncio.create_subprocess_exec(
+            "ffmpeg",
+            "-i",
+            input_file_name,
+            "-c:a",
+            "libvorbis",
+            output_file_name,
+            stdout=asyncio.subprocess.PIPE,
+            stdin=asyncio.subprocess.PIPE,
+        )
+        _, stderr = await proc.communicate()
+        if proc.returncode == 0:
+            with open(output_file_name, "rb") as file:
+                return file.read()
+        else:
+            err_text = (
+                stderr.decode("utf-8") if stderr is not None else f"unknown ({proc.returncode})"
+            )
+            raise ChildProcessError(f"ffmpeg error: {err_text}")