Discord Bot avec fonction d'enregistrement commençant par Python: (5) Exploitez directement l'API Discord

introduction

Cet article est une continuation du précédent Discord Bot avec fonction d'enregistrement commençant par Python: (4) Lecture de fichiers musicaux.

Dans cet article, comme étape préparatoire pour commencer à implémenter l'enregistrement vocal, essayez d'accéder à l'API de communication vocale à l'aide de l'API Discord pour comprendre comment la communication est effectuée.

Nous prévoyons d'écrire 7 articles au total et avons fini de rédiger jusqu'à 5 articles.

  1. Discord Bot avec fonction d'enregistrement commençant par Python: (1) Mise en route discord.py
  2. Discord Bot avec fonction d'enregistrement commençant par Python: (2) Fonction pratique (extension Bot, Cog, Embed)
  3. Discord Bot avec fonction d'enregistrement commençant par Python: (3) Coopération avec la base de données
  4. Discord Bot avec fonction d'enregistrement commençant par Python: (4) Lire des fichiers musicaux
  5. Discord Bot avec fonction d'enregistrement commençant par Python: (5) Exploitez directement l'API Discord

Comme vous pouvez le voir en supprimant "Débutant" de l'étiquette, le processus suivant est un peu gênant et implique principalement la partie de la couche basse.

Flux de communication

Un diagramme schématique de la communication lors de l'envoi et de la réception de la voix avec Discord est présenté ci-dessous.

main.png

Puisqu'il s'agit d'un diagramme schématique, une explication détaillée est omise, mais je pense que ce serait bien si nous pouvions comprendre que la connexion se fait par différents processus.

Ce flux est mis en œuvre sans utiliser discord.py, et la transmission et la réception de l'audio Discord sont traitées en détail.

Toutes les informations ultérieures sont une référence officielle (Voice Connection Gateway, Normal Gateway Il est décrit sur la base de docs / topics / gateway # gateways)).

Connexion à la passerelle

Discord Gateway dispose d'une passerelle qui envoie et reçoit des informations sur la voix de passerelle normale. Pour obtenir l'URL du point final pour la connexion à la passerelle vocale, authentifiez-vous d'abord avec la passerelle normale, puis connectez-vous à la passerelle vocale. Des informations seront envoyées.

Tout d'abord, créez un script pour essayer la connexion WebSocket en Python.

op10 Hello

import json
import asyncio
import aiohttp
from pprint import pprint


class Gateway:
    def __init__(self, loop=None):
        if loop is None:
            loop = asyncio.get_event_loop()
        self.endpoint = 'wss://gateway.discord.gg/?v=6&encoding=json'
        loop.create_task(self.receive_data())

    async def receive_data(self):
        async with aiohttp.ClientSession() as session:
            socket = await session.ws_connect(self.endpoint)
            while True:
                packet = await socket.receive()
                if packet.type in (aiohttp.WSMsgType.CLOSED,
                                   aiohttp.WSMsgType.CLOSING,
                                   aiohttp.WSMsgType.CLOSE,
                                   aiohttp.WSMsgType.ERROR):
                    print(packet)
                    print('==Fin de connexion==')
                    break
                pprint(json.loads(packet.data))

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    ws = Gateway(loop)
    loop.run_forever()

Lorsque cela est exécuté, la coroutine receive_data pour se connecter à la passerelle et afficher les données reçues de la passerelle une par une est appelée à partir de la fonction create_task dans la passerelle et commence le traitement. Une fois cette opération exécutée, les données suivantes seront envoyées à partir de Discord Gateway.

{'d': {'_trace': ['["gateway-prd-main-xwmj",{"micros":0.0}]'],
       'heartbeat_interval': 41250},
 'op': 10,
 's': None,
 't': None}

Les données envoyées par Discord utilisent «d» et «op», et rarement «t» pour représenter les données. Dans «op», le type de données est stocké, et dans «d», le corps d'information des données est stocké. «t» est fondamentalement «Aucun», mais si vous avez besoin de transmettre des informations plus détaillées, ce sera une chaîne de caractères qui transmettra les détails de ces informations.

op1 Heartbeat

Ici, ʻop = 10. Il s'agit d'une réponse appelée «Hello», qui correspond aux données envoyées lorsque vous vous connectez pour la première fois comme son nom l'indique. Les données importantes dans Hello sont «heartbeat_interval». Ici, il s'agit de «41250», qui doit envoyer une simple donnée appelée «Heartbeat» pour vous dire que la passerelle est toujours connectée chaque milliseconde spécifiée (41,25 secondes). Il y a. Créez une classe qui hérite de threading.Threaden tant que classe auxiliaire qui exécute ce processus Heartbeat. En écrivant le processus souhaité dans la fonctionrun` et en appelant la fonction de démarrage depuis l'instance, le processus sera exécuté dans un autre thread.

import json
import asyncio
import aiohttp
import threading
from pprint import pprint


class HeartbeatHandler(threading.Thread):
    def __init__(self, ws, interval):
        self.ws = ws
        self.interval = interval
        self.stop_ev = threading.Event()
        super().__init__()

    def run(self):
        self.send()
        while not self.stop_ev.wait(self.interval):
            self.send()

    def send(self):
        data = self.get_payload()
        asyncio.run_coroutine_threadsafe(
            self.ws.socket.send_json(data),
            self.ws.loop
        )
        print('==Envoyer==')
        print(data)

    def stop(self):
        self.stop_ev.set()

    def get_payload(self):
        raise NotImplementedError


class GatewayHeartbeat(HeartbeatHandler):
    def __init__(self, ws, interval):
        super().__init__(ws, interval)

    def get_payload(self):
        return {'op': 1, 'd': None}


class Gateway:
    def __init__(self, loop=None):
        if loop is None:
            self.loop = asyncio.get_event_loop()
        else:
            self.loop = loop
        self.endpoint = 'wss://gateway.discord.gg/?v=6&encoding=json'
        self.loop.create_task(self.receive_data())

    async def receive_data(self):
        async with aiohttp.ClientSession() as session:
            self.socket = await session.ws_connect(self.endpoint)
            while True:
                packet = await self.socket.receive()
                if packet.type in (aiohttp.WSMsgType.CLOSED,
                                   aiohttp.WSMsgType.CLOSING,
                                   aiohttp.WSMsgType.CLOSE,
                                   aiohttp.WSMsgType.ERROR):
                    print(packet)
                    print('==Fin de connexion==')
                    break
                print('==Recevoir==')
                pprint(json.loads(packet.data))
                await self.handle_message(json.loads(packet.data))
            if hasattr(self, 'heartbeat'):
                self.heartbeat.stop()

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 10:
            self.heartbeat = GatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            return


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    ws = Gateway(loop)
    loop.run_forever()

Ça s'appelle run_coroutine_threadsafe ** Quoi qu'il en soit! !! Puisqu'il existe une fonction **, utilisez-la. Lorsque ceci est exécuté, l'état de la communication entre eux est émis toutes les 40 secondes.

==Recevoir==
{'d': {'_trace': ['["gateway-prd-main-w7j9",{"micros":0.0}]'],
       'heartbeat_interval': 41250},
 'op': 10,
 's': None,
 't': None}
==Envoyer==
{'op': 1, 'd': None}
==Recevoir==
{'d': None, 'op': 11, 's': None, 't': None}
==Envoyer==
{'op': 1, 'd': None}
==Recevoir==
{'d': None, 'op': 11, 's': None, 't': None}
...

Si cela n'est pas fait, la connexion sera déconnectée de Discord Gateway après 40 secondes. Cependant, si vous n'effectuez que Heartbeat, vous pouvez recevoir une demande de reconnexion du côté Discord. Pour le moment, nous n'effectuerons pas de traitement tel que la reconnexion ici.

op2 Identify

Ensuite, vous devez envoyer un jeton Bot pour informer la passerelle des informations de connexion. Ces informations sont envoyées par op2, mais en plus du jeton Bot, des informations de connexion simples sont ajoutées aux propriétés de la charge utile. De plus, si vous exploitez un Bot à grande échelle et effectuez un «Sharding», un traitement supplémentaire est nécessaire, mais ici nous effectuerons le traitement en supposant que le Sharding n'est pas utilisé avec un Bot à petite échelle.

class Gateway:
    def __init__(self, loop=None):
        if loop is None:
            self.loop = asyncio.get_event_loop()
        else:
            self.loop = loop
        self.endpoint = 'wss://gateway.discord.gg/?v=6&encoding=json'
        self.loop.create_task(self.receive_data())
        self.identified = asyncio.Event()

    async def receive_data(self):
        async with aiohttp.ClientSession() as session:
            self.socket = await session.ws_connect(self.endpoint)
            while True:
                packet = await self.socket.receive()
                if packet.type in (aiohttp.WSMsgType.CLOSED,
                                   aiohttp.WSMsgType.CLOSING,
                                   aiohttp.WSMsgType.CLOSE,
                                   aiohttp.WSMsgType.ERROR):
                    print('==Fin de connexion==')
                    print(packet)
                    break
                print('==Recevoir==')
                pprint(json.loads(packet.data))
                await self.handle_message(json.loads(packet.data))
            if hasattr(self, 'heartbeat'):
                self.heartbeat.stop()

    async def identify(self):
        payload = {
            'op': 2,
            'd': {
                'token': 'BOT_TOKEN',
                'properties': {
                    '$os': 'linux',
                    '$browser': 'python',
                    '$device': 'python',
                },
                'v': 3
            }
        }
        print('==Envoyer==')
        print(payload)
        await self.socket.send_json(payload)
        self.identified.set()

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 10:
            self.heartbeat = GatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            await self.identify()
            return

L'authentification est effectuée en envoyant le jeton Bot, et les informations Bot et les informations du serveur sur lequel le Bot est installé seront reçues. Il est facile de l'oublier car diverses informations sont envoyées, mais si les informations t = READY sont envoyées avec ʻop = 0, cela signifie que vous êtes prêt à communiquer entre vous en utilisant Gateway. De plus, le session_id dans le d` est utilisé pour la connexion vocale, donc enregistrez-le.

{'d': {
       ...
       'session_id': 'f0d7bba081bc0df51e43c1eef8092adcb',
       ... 
      },
 'op': 0,
 's': 1,
 't': 'READY'}

Connectez-vous à la passerelle vocale

op4 Gateway Voice State Update

Afin d'obtenir les informations pour se connecter à la passerelle vocale, il est nécessaire d'envoyer la connexion à la passerelle normale avec ʻop = 4`.

Lorsque ʻop = 4`, spécifiez l'ID du serveur et du canal audio et l'état de coupure de lui-même et envoyez-le à la passerelle. Cela vous donnera l'URL du point de terminaison de la passerelle vocale utilisée par le serveur.

class Gateway:
    ...

    async def voice_state_update(self):
        payload = {
            'op': 4,
            'd': {
                'guild_id': '705...',
                'channel_id': '706...',
                "self_mute": False,  #S'il faut couper le son
                "self_deaf": False,  #S'il faut couper le son du haut-parleur
            }
        }
        print('==Envoyer==')
        print(payload)
        await self.socket.send_json(payload)

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 10:
            self.heartbeat = GatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            await self.identify()
            return
        if op == 0:
            if t == 'READY':
                self.session_id = d['session_id']
                await self.voice_state_update()

Quand ceci est exécuté, le Bot sera connecté au canal audio et les deux données suivantes seront reçues.

==Recevoir==
{'d': {'channel_id': '705...',
       'deaf': False,
       'guild_id': '706...',
       'member': ...,
       'mute': False,
       'self_deaf': False,
       'self_mute': False,
       'self_video': False,
       'session_id': 'f0d7bba081bc0df51e43c1eef8092adcb',
       'suppress': False,
       'user_id': '743...'},
 'op': 0,
 's': 3,
 't': 'VOICE_STATE_UPDATE'}
==Recevoir==
{'d': {'endpoint': 'japan396.discord.media:80',
       'guild_id': '705...',
       'token': '0123456789abcdef'},
 'op': 0,
 's': 4,
 't': 'VOICE_SERVER_UPDATE'}

Le «point final» de «VOICE_SERVER_UPDATE» ci-dessous est le point final de la passerelle vocale, et «token» est utilisé comme jeton d'authentification.

op3 Heartbeat

De là, la communication avec la passerelle vocale démarre.

Créez une nouvelle classe pour la communication WebSocket avec le point de terminaison obtenu précédemment.

class Gateway:
    ...

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 10:
            self.heartbeat = GatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            await self.identify()
            return
        if op == 0:
            if t == 'READY':
                self.session_id = d['session_id']
                await self.voice_state_update()
            if t == 'VOICE_SERVER_UPDATE':
                self.voice_endpoint = d['endpoint']
                self.token = d['token']
                self.voice_gw = VoiceGateway(self, self.loop)


class VoiceGateway:
    def __init__(self, gateway, loop=None):
        self.gateway = gateway
        if loop is None:
            self.loop = asyncio.get_event_loop()
        else:
            self.loop = loop
        self.endpoint = f'wss://{gateway.voice_endpoint.replace(":80", "")}/?v=4'
        self.loop.create_task(self.receive_data())
        self.identified = asyncio.Event()

    async def receive_data(self):
        async with aiohttp.ClientSession() as session:
            self.socket = await session.ws_connect(self.endpoint)
            while True:
                packet = await self.socket.receive()
                if packet.type in (aiohttp.WSMsgType.CLOSED,
                                   aiohttp.WSMsgType.CLOSING,
                                   aiohttp.WSMsgType.CLOSE,
                                   aiohttp.WSMsgType.ERROR):
                    print('**Fin de connexion**')
                    print(packet)
                    break
                print('**Recevoir**')
                pprint(json.loads(packet.data))
                await self.handle_message(json.loads(packet.data))

    async def handle_message(self, msg):
        pass

Si vous faites cela et que cela fonctionne, il renverra l'intervalle Heartbeat ainsi que la première passerelle.

==Recevoir==
{'d': ...
 'op': 0,
 's': 5,
 't': 'VOICE_SERVER_UPDATE'}
**Recevoir**
{'d': {'heartbeat_interval': 13750.25, 'v': 4}, 'op': 8}

Pour conserver la connexion, envoyez cette fois Heartbeat avec ʻop = 3`. Un horodatage est donné comme données.

import json
import asyncio
import aiohttp
import threading
import time # <-ajouter à
from pprint import pprint


class VoiceGatewayHeartbeat(HeartbeatHandler):
    def __init__(self, ws, interval):
        super().__init__(ws, interval)

    def get_payload(self):
        #Temps en milliseconde'd'Mis à
        return {'op': 3, 'd': time.time_ns()//1000}

class VoiceGateway:
    ...

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 8:
            self.heartbeat = VoiceGatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            return

Étant donné que ce battement de cœur doit être utilisé après l'authentification du bot, la communication n'est pas démarrée et elle est laissée telle quelle.

op0 Identify

Pour vous authentifier avec la passerelle vocale, utilisez l'ID de serveur, l'ID utilisateur du bot, session_id et token comme charge utile et envoyez avec ʻop = 0`.

class VoiceGateway:
    ...

    async def identify(self):
        payload = {
            'op': 0,
            'd': {
                'token': self.gateway.token,
                'user_id': '743853432007557210',
                'server_id': '705052322761277540',
                'session_id': self.gateway.session_id,
            }
        }
        print('**Envoyer**')
        print(payload)
        await self.socket.send_json(payload)

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 8:
            await self.identify()
            self.heartbeat = VoiceGatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            return

Si authentifié avec succès, vous recevrez «op2 Ready». «IP» et «port» correspondent à l'adresse pour obtenir des informations vocales, «modes» correspond à la méthode de cryptage de la voix prise en charge par Discord, et «ssrc» correspond à l'identifiant.

**Recevoir**
{'d': {'heartbeat_interval': 13750.25, 'v': 4}, 'op': 8}
**Envoyer**
{'op': 0, 'd': {'token': '871d40956f7cf34a', 'user_id': '743853432007557210', 'server_id': '705052322761277540', 'session_id': 'c412a670dbed864b559a25009459f15a'}}
==Envoyer==
{'op': 3, 'd': 1598314493140616}
**Recevoir**
{'d': {'experiments': ['bwe_conservative_link_estimate',
                       'bwe_remote_locus_client'],
       'ip': '123.123.123.123',
       'modes': ['aead_aes256_gcm',
                 'xsalsa20_poly1305_lite',
                 'xsalsa20_poly1305_suffix',
                 'xsalsa20_poly1305'],
       'port': 50004,
       'ssrc': 364117},
 'op': 2}
**Recevoir**
{'d': 1598314493140616, 'op': 6}
==Envoyer==
{'op': 3, 'd': 1598314506891112}
**Recevoir**
{'d': 1598314506891112, 'op': 6}

Connexion UDP

Je fais une connexion UDP à l'adresse IP obtenue lors de la communication précédente et j'obtiens des données vocales, mais cette adresse IP est obscurcie via NAT -connections # ip-discovery), vous devez donc obtenir l'adresse et le port ouverts au public. Pour l'obtenir, envoyez le paquet UDP suivant au serveur de ʻip, port`.

champ La description Taille
type 0x1 2 octets
longueur 70 2 octets
SSRC Entier non signé 4 octets
adresse IP code ascii(Le surplus est0x0(Caractère nul)Pack. 0 lors de l'envoi) 64 octets
Port Entier non signé(0 lors de l'envoi) 2 octets

Lorsqu'il est envoyé, le même paquet de 74 octets contenant des données dans l'adresse IP et le port est envoyé, de sorte que les informations IP et de port sont obtenues à partir de ce paquet.

import json
import asyncio
import aiohttp
import threading
import time
import socket # <-ajouter à
import struct # <-ajouter à
from pprint import pprint

class VoiceGateway:
    ...

    async def ip_discovering(self):
        self.udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.udp.setblocking(False)
        packet = bytearray(74)
        packet[:2] = struct.pack('>H', 1)
        packet[2:4] = struct.pack('>H', 70)
        packet[4:8] = struct.pack('>I', self.ssrc)
        self.udp.sendto(bytes(packet), (self.ip, self.port))
        data = await self.loop.sock_recv(self.udp, 2048)
        self.external_ip, self.external_port = struct.unpack_from(
            '>64sH', data, 8
        )
        self.external_ip = self.external_ip.decode(encoding='ascii').rstrip('\x00')
        print(self.external_ip, self.external_port)

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 8:
            await self.identify()
            self.heartbeat = VoiceGatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            return
        if op == 2:
            self.ip = d['ip']
            self.port = d['port']
            self.modes = d['modes']
            self.ssrc = d['ssrc']
            await self.ip_discovering()

Struct (package standard) est utilisé pour créer des paquets de données UDP. Lorsque ceci est exécuté, un paquet UDP est reçu à l'aide de la boucle d'événements et l'adresse IP et le port sont envoyés à la console.

**Recevoir**
{'d': ...,
 'op': 2}
201.158.201.158 54345

La raison de passer par un processus aussi fastidieux est d'obtenir la clé pour déchiffrer la voix cryptée. En envoyant l'adresse IP externe et le port obtenus dans ce processus à la passerelle vocale, vous pouvez obtenir la clé de décryptage en tant que réponse. Ce qu'on appelle libsodium est utilisé pour le cryptage de la voix, et dans le cas de Python, le cryptage et le décryptage à l'aide de libsodium peuvent être effectués en ajoutant le package PyNaCl.

op1 Select Protocol

Rendre possible l'obtention de la clé à utiliser avec libsodium. Pour mode dans la charge utile de op1, il est nécessaire de sélectionner l'une des méthodes de cryptage parmi les modes obtenus dans ʻop2 plus tôt, mais ici nous utiliserons systématiquement xsalsa20_poly1305. .. Si vous envoyez ʻop1, ʻop4 Session Description` sera envoyée comme réponse. Il y a une clé de déchiffrement dans cette charge utile, alors retirez-la.

class VoiceGateway:
    ...

    async def select_protocol(self):
        payload = {
            'op': 1,
            'd': {
                'protocol': 'udp',
                'data': {
                    'address': self.external_ip,
                    'port': self.external_port,
                    'mode': 'xsalsa20_poly1305'
                }
            }
        }
        print('**Envoyer**')
        print(payload)
        await self.socket.send_json(payload)

    async def receive_audio_packet(self):
        while True:
            data = await self.loop.sock_recv(self.udp, 2048)
            print('**Réception vocale**')
            print(data)

    async def handle_message(self, msg):
        op = msg.get('op')
        d = msg.get('d')
        t = msg.get('t')
        if op == 8:
            await self.identify()
            self.heartbeat = VoiceGatewayHeartbeat(
                self, d['heartbeat_interval'] / 1000
            )
            self.heartbeat.start()
            return
        if op == 2:
            self.ip = d['ip']
            self.port = d['port']
            self.modes = d['modes']
            self.ssrc = d['ssrc']
            await self.ip_discovering()
            await self.select_protocol()
        if op == 4:
            self.secret_key = d['secret_key']
            self.loop.create_task(self.receive_audio_packet())

Après avoir reçu «op4», les données vocales seront envoyées au socket UDP, donc create_task est exécuté pour démarrer la tâche de réception des données vocales.

**Envoyer**
{'op': 1, 'd': {'protocol': 'udp', 'data': {'address': '106.73.199.128', 'port': 42057, 'mode': 'xsalsa20_poly1305'}}}
**Recevoir**
{'d': {'audio_codec': 'opus',
       ...
       'mode': 'xsalsa20_poly1305',
       'secret_key': [244,
                      157,
                      ...
                      214],
       'video_codec': None},
 'op': 4}
**Réception vocale**
b'\x81\xc9\x00\x07\x00\x07\xdd(\x9fI\xb9\xd6\x00G\xce\xa2\xa4\x85M[\xed\xd3\x0fu\x15\x89|\xa6W\x1e\xc3U\x06\xc8\xd5S\x8fJ\x08\xfcx\xff\xe9\x83k\xca\xa9\xec'
**Réception vocale**
b'\x81\xc9\x00\x07\x00\x07\xdd(\x00\x9c^\x83\x90\xc5V\xafX\xff\x14\x97\xf5\xf1/\xad\x15\x89|\xa6W\x1e\xc3U\x06\xc8\xd5S\x8fJ\x08\xfcx\xff\xe9\x83k\xcb\xa9\x02'
**Réception vocale**
b'\x81\xc9\x00\x07\x00\x07\xdd(j\x88B\\O\xd0\rs`\xc1_\x92\xc6\xe6\xe7=\x15\x89|\xa6W\x1e\xc3U\x06\xc8\xd5S\x8fJ\x08\xfcx\xff\xe9\x83k\xc8\xa9\xfd'
**Réception vocale**
b'\x81\xc9\x00\x07\x00\x07\xdd(\x05\x02\xf56\x8a\x13\x9e\xc2\xb6\x8c,\xe6r5\x0e\n\x15\x89|\xa6W\x1e\xc3U\x06\xc8\xd5S\x8fJ\x08\xfcx\xff\xe9\x83k\xc9\xa9\x14'

Les protocoles utilisés pour envoyer et recevoir l'audio Discord sont RTP et RTCP. Chaque paquet qui stocke des données vocales est un paquet RTP qui envoie des données vocales pendant 20 ms à la fois, et un paquet RTCP envoie des informations supplémentaires sur ces données vocales.

Pour faire la distinction entre RTP et RTCP, concentrez-vous sur la valeur du deuxième octet du paquet. Selon la définition du protocole, le deuxième octet de RTCP est plage 200 à 204, donc il peut être identifié ici.

Pour calculer la longueur de l'en-tête RTP, faites attention à X = «1er octet 4e bit» et CC = «1er octet 5-8». Je n'expliquerai pas le rôle de chaque bit, mais

Si $ X = 0 $

header\\_length=12+4\times CC

Si $ X = 1 $

header\\_length=16+4\times CC+4\times len(EX\\_header)

Il peut être calculé comme suit. len (EX_header) est une valeur indiquant la longueur d'en-tête supplémentaire, qui correspond à la valeur de 2 octets de l'octet «14 + 4 × CC».

Pour plus de détails, veuillez consulter le tableau sur Wikipedia.

Cette fois, il n'y a pas de problème si vous ne pouvez obtenir que Timestamp qui est l'heure de transmission de la voix dans l'en-tête RTP, donc [API Reference](https://discord.com/developers/docs/topics/voice-connections#encrypting-and -sending-voice-voice-packet-structure) et extraire les 4ème-8ème octets.

Vous pouvez maintenant récupérer les données audio pour le moment.

en conclusion

À partir de ces informations, il est possible de séparer la charge utile et l'en-tête du paquet RTP, mais des problèmes subsistent.

Dans le premier cas, vous avez déjà la clé, vous pouvez donc la déchiffrer en fonction de celle-ci. Ce dernier nécessite un traitement un peu compliqué et rend une bibliothèque C appelée libopus disponible à partir de Python, et si vous appelez sa fonction de décodage, elle peut être enregistrée en tant que données Wav normales.

Si vous parvenez à effacer ces deux, vous pourrez sauvegarder les données audio. La prochaine fois, j'étendrai le discord.py existant en fonction des connaissances acquises à partir de cette couche inférieure sur la façon de récupérer les données et d'enregistrer les données audio.

Recommended Posts

Discord Bot avec fonction d'enregistrement commençant par Python: (5) Exploitez directement l'API Discord
Discord Bot avec fonction d'enregistrement commençant par Python: (3) Coopération avec la base de données
Discord Bot avec fonction d'enregistrement commençant par Python: (1) Introduction discord.py
Discord Bot avec fonction d'enregistrement commençant par Python: (4) Lire des fichiers musicaux
Comment faire fonctionner l'API Discord avec Python (enregistrement de bot)
Appelez l'API avec python3.
Accédez à l'API Etherpad-lite avec Python
Lancez le bot Discord Python pendant 24 heures.
[Python] Utiliser automatiquement le navigateur avec Selenium
Discord bot raspberry pi zéro avec python [Note]
LINE BOT avec Python + AWS Lambda + API Gateway
J'ai essayé d'utiliser Linux avec Discord Bot
Essayez d'accéder à l'API YQL directement depuis Python 3
[Introduction à Python] Comment itérer avec la fonction range?
Utiliser Kinesis avec Python
Python à partir de Windows 7
Exploration avec Python et Twitter API 1 - Fonction de recherche simple
GRPC commençant par Python
Faire fonctionner Blender avec Python
Utiliser Excel avec Python (1)
J'ai essayé de frapper l'API avec le client python d'echonest
[LINE Messaging API] Créer un BOT de retour de perroquet avec Python
Utiliser Excel avec Python (2)
Essayez d'utiliser l'API Twitter rapidement et facilement avec Python
L'histoire de la mise en œuvre du sujet Facebook Messenger Bot avec python
Unifier l'environnement de l'équipe de développement Python en commençant par Poetry
La première API à créer avec le framework Python Djnago REST
Créez un bot Mastodon avec une fonction pour répondre automatiquement avec Python
Exécutons la commande à temps avec le bot discord
Touchons l'API de Netatmo Weather Station avec Python. #Python #Netatmo
Créez un Twitter BOT avec le SDK GoogleAppEngine pour Python
Commerce du système à partir de Python3: obtenez le dernier code de programme
[Introduction à Python] Comment obtenir des données avec la fonction listdir
Exploitez Excel avec Python open pyxl
Utiliser l'API Trello avec python
Exploitez TwitterBot avec Lambda, Python
Utiliser l'API Twitter avec Python
Un débutant en Python lance Discord Bot
Apprentissage amélioré à partir de Python
À propos de la fonction enumerate (python)
[Note] Faites fonctionner MongoDB avec Python
API Web avec Python + Falcon
[Python] [SQLite3] Exploiter SQLite avec Python (basique)
Utiliser l'API subsonique avec python3
Python commençant par Hello world!
Faites fonctionner Jupyter avec l'API REST pour extraire et enregistrer le code Python
Je veux connaître la météo avec LINE bot avec Heroku + Python
[Introduction à Python] Comment fractionner une chaîne de caractères avec la fonction split
Exploration avec Python et Twitter API 2-Implémentation de la fonction de recherche d'utilisateurs
Créer une API REST qui renvoie l'heure actuelle avec Python3 + Falcon
[Python] Explique comment utiliser la fonction format avec un exemple
[LINE Messaging API] Créez un BOT qui se connecte à quelqu'un avec Python
Utilisez Firefox avec Selenium depuis python et enregistrez la capture d'écran
Comment envoyer une requête à l'API DMM (FANZA) avec python
Créer une API REST pour faire fonctionner dynamodb avec le Framework Django REST
Utilisez Nutanix avec l'API REST, partie 2