sx.blah.discord.api.internal.UDPVoiceSocket.java Source code

Java tutorial

Introduction

Here is the source code for sx.blah.discord.api.internal.UDPVoiceSocket.java

Source

/*
 *     This file is part of Discord4J.
 *
 *     Discord4J is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     Discord4J 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 Lesser General Public License for more details.
 *
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with Discord4J.  If not, see <http://www.gnu.org/licenses/>.
 */
package sx.blah.discord.api.internal;

import org.apache.commons.lang3.tuple.Pair;
import sx.blah.discord.Discord4J;
import sx.blah.discord.api.internal.json.requests.voice.SelectProtocolRequest;
import sx.blah.discord.api.internal.json.requests.voice.VoiceSpeakingRequest;
import sx.blah.discord.handle.audio.impl.AudioManager;
import sx.blah.discord.handle.obj.IUser;
import sx.blah.discord.util.HighPrecisionRecurrentTask;
import sx.blah.discord.util.LogMarkers;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * Facilitates the sending and receiving of voice data on a UDP socket with Discord.
 */
public class UDPVoiceSocket {

    /**
     * The audio frames sent to Discord when the bot stops sending audio data.
     * @see <a href=https://discordapp.com/developers/docs/topics/voice-connections#voice-data-interpolation>Voice Data Interpolation</a>
     */
    private static final byte[] SILENCE_FRAMES = { (byte) 0xF8, (byte) 0xFF, (byte) 0xFE };
    /**
     * Data sent on the UDP socket to prevent the port from being reclaimed. This specific packet is ignored by Discord.
     */
    private static final byte[] KEEP_ALIVE_DATA = { (byte) 0xC9, 0, 0, 0, 0, 0, 0, 0, 0 };
    /**
     * The maximum size in bytes of an audio packet received on the socket.
     */
    private static final int MAX_INCOMING_AUDIO_PACKET = OpusUtil.OPUS_FRAME_SIZE * 2 + 12; //two channels + 12 rtp header bytes.

    /**
     * The voice gateway associated with this socket.
     */
    private volatile DiscordVoiceWS voiceWS;
    /**
     * The underlying udp socket that data is sent and received on.
     */
    private volatile DatagramSocket udpSocket;
    /**
     * The socket address data is sent to.
     */
    private volatile InetSocketAddress address;

    /**
     * The thread send and receive logic is handled on.
     */
    private volatile HighPrecisionRecurrentTask audioTask;

    /**
     * The secret used for encryption.
     */
    private volatile byte[] secret;
    /**
     * Whether or not audio is currently being sent on this socket.
     */
    private volatile boolean isSpeaking = false;
    /**
     * Incremented for every packet sent .
     */
    private volatile char sequence = 0;
    /**
     * Incremented by {@link OpusUtil#OPUS_FRAME_TIME} for every packet sent.
     */
    private volatile int timestamp = 0;
    /**
     * The ssrc associated with our user.
     */
    private volatile int ssrc;

    /**
     * The number of silence frames left to send after the transmission of audio has stopped.
     */
    private volatile int silenceToSend = 5;

    /**
     * Indicates whether or not the {@link #begin()} method has been called. This is used in conjunction with
     * {@link #wasShutdown} to ensure that {@link #begin()} is called before {@link #shutdown()}.
      */
    private volatile boolean hasBegun = false;
    /**
     * Indicates whether or not the {@link #shutdown()} method has been called. This is used in conjunction with
     * {@link #hasBegun} to ensure that {@link #begin()} is called before {@link #shutdown()}.
     */
    private volatile boolean wasShutdown = false;

    /**
     * Function executed on the {@link #audioTask} for sending audio data.
     */
    private final Runnable sendRunnable = () -> {
        if (!wasShutdown) {
            try {
                byte[] audio = ((AudioManager) voiceWS.getGuild().getAudioManager()).sendAudio();
                if (audio != null && audio.length > 0) {
                    OpusPacket packet = new OpusPacket(sequence, timestamp, ssrc, audio);
                    packet.encrypt(secret);
                    byte[] toSend = packet.asByteArray();

                    if (!isSpeaking)
                        setSpeaking(true);
                    udpSocket.send(new DatagramPacket(toSend, toSend.length, address));

                    sequence++;
                    timestamp += OpusUtil.OPUS_FRAME_SIZE;
                    silenceToSend = 5;
                } else {
                    if (isSpeaking)
                        setSpeaking(false);
                    if (silenceToSend > 0) {
                        sendSilence();
                        silenceToSend--;
                    }
                }
            } catch (Exception e) {
                Discord4J.LOGGER.error(LogMarkers.VOICE_WEBSOCKET, "Discord4J Internal Exception", e);
            }
        }
    };

    /**
     * Function executed on the {@link #audioTask} for receiving audio data.
     */
    private final Runnable receiveRunnable = () -> {
        if (!wasShutdown) {
            //keep receiving audio for 10ms. This will consume as many frames as are readily available, without blocking the thread for more than 10ms.
            long start = System.nanoTime();
            while (System.nanoTime() - start < 10_000_000) {
                try {
                    DatagramPacket udpPacket = new DatagramPacket(new byte[MAX_INCOMING_AUDIO_PACKET],
                            MAX_INCOMING_AUDIO_PACKET);
                    udpSocket.receive(udpPacket);

                    OpusPacket opus = new OpusPacket(udpPacket);
                    opus.decrypt(secret);

                    IUser user = voiceWS.users.get(opus.header.ssrc);
                    if (user != null) {
                        ((AudioManager) voiceWS.getGuild().getAudioManager()).receiveAudio(opus.getAudio(), user,
                                opus.header.sequence, opus.header.timestamp);
                    }
                } catch (SocketTimeoutException ignored) {
                } catch (Exception e) {
                    Discord4J.LOGGER.error(LogMarkers.VOICE_WEBSOCKET, "Discord4J Internal Exception", e);
                }
            }
        }
    };

    /**
     * Function executed on the {@link #audioTask} for keeping the udp socket alive.
     */
    private final Runnable keepAliveRunnable = new Runnable() {
        int iterations = 0;

        @Override
        public void run() {
            if (!wasShutdown) {
                iterations++;
                if (iterations % (5000 / 20) == 0) { //once every 5 seconds, assuming that each invocation happens every 20ms.
                    try {
                        DatagramPacket packet = new DatagramPacket(KEEP_ALIVE_DATA, KEEP_ALIVE_DATA.length,
                                address);
                        udpSocket.send(packet);
                    } catch (Exception e) {
                        Discord4J.LOGGER.error(LogMarkers.VOICE_WEBSOCKET,
                                "Internal exception sending UDP keepalive: ", e);
                    }
                }
            }
        }
    };

    UDPVoiceSocket(DiscordVoiceWS voiceWS) {
        this.voiceWS = voiceWS;
    }

    /**
     * Called when the voice gateway receives {@link VoiceOps#READY}. This performs IP discovery and sends
     * {@link VoiceOps#SELECT_PROTOCOL} on the voice gateway.
     *
     * @param endpoint The endpoint to send audio data to.
     * @param port The port to send audio data on.
     * @param ssrc The self user's ssrc.
     */
    synchronized void setup(String endpoint, int port, int ssrc) {
        try {
            this.udpSocket = new DatagramSocket();
            this.address = new InetSocketAddress(endpoint, port);
            this.ssrc = ssrc;

            Pair<String, Integer> ourIp = doIPDiscovery(ssrc);

            udpSocket.setSoTimeout(5); //after IP discovery, every usage times out after 5ms, because it's better to drop a frame than block the thread.

            SelectProtocolRequest selectRequest = new SelectProtocolRequest(ourIp.getLeft(), ourIp.getRight());
            voiceWS.send(VoiceOps.SELECT_PROTOCOL, selectRequest);
        } catch (IOException e) {
            Discord4J.LOGGER.error(LogMarkers.VOICE_WEBSOCKET, "Encountered error opening voice UDP socket: ", e);
        }
    }

    /**
     * Called when {@link VoiceOps#SESSION_DESCRIPTION} is received on the voice gateway. At this point, the connection
     * handshake has been completed and all of the necessary information for sending and receiving voice data has been
     * received.
     *
     * <p><b>If this socket is not in the correct state according to {@link #hasBegun} and {@link #wasShutdown}, a call to
     * this method will be ignored.</b>
     */
    synchronized void begin() {
        if (!hasBegun && !wasShutdown) {
            hasBegun = true;
            audioTask = new HighPrecisionRecurrentTask(OpusUtil.OPUS_FRAME_TIME, 0.01f, () -> {
                synchronized (udpSocket) { //while the the audio handling is happening, lock the socket so no concurrent shutdown happens
                    if (!udpSocket.isClosed()) {
                        sendRunnable.run();
                        receiveRunnable.run();
                        keepAliveRunnable.run();
                    }
                }
            });
            audioTask.setDaemon(true);
            audioTask.start();
        }
    }

    /**
     * Called when {@link DiscordWS#shutdown()} is called.
     *
     * <p><b>If this socket is not in the correct state according to {@link #hasBegun} and {@link #wasShutdown}, a call to
     * this method will be ignored.</b>
     */
    synchronized void shutdown() {
        if (hasBegun && !wasShutdown) {
            wasShutdown = true;
            audioTask.setStop(true);
            synchronized (udpSocket) {
                udpSocket.close();
            }
        }
    }

    /**
     * Obtains the machine's external IP address and port to send to Discord.
     *
     * @param ssrc The self user's ssrc.
     * @return A pair of the machine's external IP address and port.
     * @throws IOException Thrown by underlying UDP socket.
     *
     * @see <a href=https://discordapp.com/developers/docs/topics/voice-connections#ip-discovery>IP Discovery</a>
     */
    private Pair<String, Integer> doIPDiscovery(int ssrc) throws IOException {
        byte[] data = ByteBuffer.allocate(70).putInt(ssrc).array();
        DatagramPacket discoveryPacket = new DatagramPacket(data, data.length, address);
        udpSocket.send(discoveryPacket);

        DatagramPacket responsePacket = new DatagramPacket(new byte[70], 70);
        udpSocket.receive(responsePacket);

        byte[] receivedData = responsePacket.getData();
        String ip = new String(Arrays.copyOfRange(receivedData, 4, 68)).trim();
        int port = ((((int) receivedData[69]) & 0x000000FF) << 8) | (((int) receivedData[68]) & 0x000000FF);

        return Pair.of(ip, port);
    }

    /**
     * Sets {@link #isSpeaking} and sends {@link VoiceOps#SPEAKING} on the voice gateway.
     *
     * @param isSpeaking Whether audio data is being sent or not.
     */
    private void setSpeaking(boolean isSpeaking) {
        this.isSpeaking = isSpeaking;
        voiceWS.send(VoiceOps.SPEAKING, new VoiceSpeakingRequest(isSpeaking));
    }

    /**
     * Sends {@link #SILENCE_FRAMES} on the voice socket.
     *
     * @throws IOException Thrown by the underlying UDP socket.
     */
    private void sendSilence() throws IOException {
        OpusPacket packet = new OpusPacket(sequence, timestamp, ssrc, SILENCE_FRAMES);
        packet.encrypt(secret);
        byte[] toSend = packet.asByteArray();

        udpSocket.send(new DatagramPacket(toSend, toSend.length, address));
    }

    /**
     * Sets the secret used for voice encryption and decryption.
     *
     * @param secret The secret used for voice encryption and decryption.
     */
    void setSecret(byte[] secret) {
        this.secret = secret;
    }
}