├── .github
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
├── API.md
├── LICENSE
├── README.md
├── Server
├── build.gradle
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── sedmelluq
│ │ │ └── discord
│ │ │ └── lavaplayer
│ │ │ └── udpqueue
│ │ │ └── natives
│ │ │ ├── UdpQueueManager.java
│ │ │ └── UdpQueueManagerLibrary.java
│ ├── kotlin
│ │ ├── me
│ │ │ └── uport
│ │ │ │ └── knacl
│ │ │ │ └── NaClLowLevel.kt
│ │ └── obsidian
│ │ │ ├── bedrock
│ │ │ ├── Bedrock.kt
│ │ │ ├── BedrockClient.kt
│ │ │ ├── Event.kt
│ │ │ ├── MediaConnection.kt
│ │ │ ├── VoiceServerInfo.kt
│ │ │ ├── codec
│ │ │ │ ├── Codec.kt
│ │ │ │ ├── CodecType.kt
│ │ │ │ ├── OpusCodec.kt
│ │ │ │ └── framePoller
│ │ │ │ │ ├── AbstractFramePoller.kt
│ │ │ │ │ ├── FramePoller.kt
│ │ │ │ │ ├── FramePollerFactory.kt
│ │ │ │ │ ├── QueueManagerPool.kt
│ │ │ │ │ ├── UdpQueueFramePollerFactory.kt
│ │ │ │ │ └── UdpQueueOpusFramePoller.kt
│ │ │ ├── crypto
│ │ │ │ ├── DefaultEncryptionModes.kt
│ │ │ │ ├── EncryptionMode.kt
│ │ │ │ ├── PlainEncryptionMode.kt
│ │ │ │ ├── UnsupportedEncryptionModeException.kt
│ │ │ │ ├── XSalsa20Poly1305EncryptionMode.kt
│ │ │ │ ├── XSalsa20Poly1305LiteEncryptionMode.kt
│ │ │ │ └── XSalsa20Poly1305SuffixEncryptionMode.kt
│ │ │ ├── gateway
│ │ │ │ ├── AbstractMediaGatewayConnection.kt
│ │ │ │ ├── GatewayVersion.kt
│ │ │ │ ├── MediaGatewayConnection.kt
│ │ │ │ ├── MediaGatewayV4Connection.kt
│ │ │ │ ├── SpeakingFlags.kt
│ │ │ │ └── event
│ │ │ │ │ ├── Command.kt
│ │ │ │ │ ├── Event.kt
│ │ │ │ │ └── Op.kt
│ │ │ ├── handler
│ │ │ │ ├── ConnectionHandler.kt
│ │ │ │ ├── DiscordUDPConnection.kt
│ │ │ │ └── HolepunchHandler.kt
│ │ │ ├── media
│ │ │ │ ├── IntReference.kt
│ │ │ │ ├── MediaFrameProvider.kt
│ │ │ │ └── OpusAudioFrameProvider.kt
│ │ │ └── util
│ │ │ │ ├── Interval.kt
│ │ │ │ ├── NettyBootstrapFactory.kt
│ │ │ │ └── RTPHeaderWriter.kt
│ │ │ └── server
│ │ │ ├── Obsidian.kt
│ │ │ ├── io
│ │ │ ├── Dispatch.kt
│ │ │ ├── Magma.kt
│ │ │ ├── MagmaClient.kt
│ │ │ ├── MagmaCloseReason.kt
│ │ │ ├── Op.kt
│ │ │ ├── Operation.kt
│ │ │ ├── RoutePlannerUtil.kt
│ │ │ ├── StatsBuilder.kt
│ │ │ ├── controllers
│ │ │ │ ├── RoutePlanner.kt
│ │ │ │ └── Tracks.kt
│ │ │ └── search
│ │ │ │ ├── AudioLoader.kt
│ │ │ │ ├── LoadResult.kt
│ │ │ │ └── LoadType.kt
│ │ │ ├── player
│ │ │ ├── FrameLossTracker.kt
│ │ │ ├── Link.kt
│ │ │ ├── ObsidianPlayerManager.kt
│ │ │ ├── PlayerEvents.kt
│ │ │ ├── PlayerUpdates.kt
│ │ │ ├── TrackEndMarkerHandler.kt
│ │ │ └── filter
│ │ │ │ ├── Filter.kt
│ │ │ │ ├── FilterChain.kt
│ │ │ │ └── impl
│ │ │ │ ├── ChannelMixFilter.kt
│ │ │ │ ├── EqualizerFilter.kt
│ │ │ │ ├── KaraokeFilter.kt
│ │ │ │ ├── LowPassFilter.kt
│ │ │ │ ├── RotationFilter.kt
│ │ │ │ ├── TimescaleFilter.kt
│ │ │ │ ├── TremoloFilter.kt
│ │ │ │ ├── VibratoFilter.kt
│ │ │ │ └── VolumeFilter.kt
│ │ │ └── util
│ │ │ ├── ByteRingBuffer.kt
│ │ │ ├── CpuTimer.kt
│ │ │ ├── LogbackColorConverter.kt
│ │ │ ├── NativeUtil.kt
│ │ │ ├── ThreadFactory.kt
│ │ │ ├── TrackUtil.kt
│ │ │ ├── config
│ │ │ ├── LoggingConfig.kt
│ │ │ └── ObsidianConfig.kt
│ │ │ └── kxs
│ │ │ └── AudioTrackSerializer.kt
│ └── resources
│ │ └── logback.xml
│ └── test
│ └── resources
│ ├── routePlanner.http
│ └── tracks.http
├── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── obsidian.yml
└── settings.gradle
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: 'type: bug, s: help wanted'
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Environment
11 |
12 | **Java**:
13 | **Client & Version**:
14 | **Obsidian Version**:
15 |
16 | ## Description
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | .idea/
3 | .settings/
4 |
5 | /build/
6 | /logs/
7 | /test/
8 | /target/
9 | /Server/bin
10 | /Server/build
11 |
12 | .obsidianrc
13 | *.iml
14 | *.jar
15 |
16 | .project
17 | .classpath
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Warning: Obsidian is not being actively developed, PRs are welcomed.
2 |
3 | # Obsidian
4 |
5 | > A performant standalone audio sending node meant for Discord Bots.
6 |
7 | - Built using [**Ktor**](https://ktor.io)
8 | - Uses [**Lavaplayer**](https://github.com/sedmelluq/lavaplayer)
9 | - Performant, low memory and cpu footprint.
10 |
11 | ## Usage.
12 |
13 | For obsidian to work correctly you must use **Java 11** or above.
14 |
15 | - Goto the [releases page](/releases).
16 | - Download the **Latest Jar File**
17 | - Make an [`obsidian.yml`](/obsidian.yml) file in the same directory as the jar file
18 | - Open a prompt in the same directory as the jar file.
19 |
20 | ```sh
21 | java -jar Obsidian.jar
22 | ```
23 |
24 | Now go make a bot with the language and client of your choice!
25 |
26 | ## Clients
27 |
28 | > Clients are used to interface with Magma, Obsidian's WebSocket and REST API.
29 |
30 | - [obby.js](https://github.com/Sxmurai/obby.js), NodeJS (v14+)
31 |
32 | **Want to make your own? Read our [API Docs](/API.md)**
33 |
34 | ---
35 |
36 | > Made by the Mixtape Team.
37 |
38 | - [GitHub](https://github.com/mixtape-bot)
39 | - [Discord](https://discord.gg/TUYc4nn)
40 |
41 | ###### Credits
42 |
43 | - [**Koe**](https://github.com/kyokobot/koe)
44 |
45 | ###### Contributors
46 |
47 | - [@Sxmurai](https://github.com/Sxmurai)
48 | - [@SerenModz21](https://github.com/SerenModz21)
49 |
50 |
Mixtape Bot • 2021
51 |
--------------------------------------------------------------------------------
/Server/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "kotlin"
2 | apply plugin: "kotlinx-serialization"
3 | apply plugin: "application"
4 | apply plugin: "com.github.johnrengelman.shadow"
5 |
6 | description = "A robust and performant audio sending node meant for Discord Bots."
7 | mainClassName = "obsidian.server.Obsidian"
8 | version "1.0.0"
9 |
10 | ext {
11 | moduleName = "Server"
12 | }
13 |
14 | dependencies {
15 | /* kotlin shit */
16 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
17 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version"
18 |
19 | /* audio */
20 | implementation ("com.sedmelluq:lavaplayer:$lavaplayer_version") {
21 | exclude group: "com.sedmelluq", module: "lavaplayer-natives"
22 | }
23 |
24 | implementation("com.sedmelluq:lavaplayer-ext-youtube-rotator:$lavaplayer_ip_rotator_config") {
25 | exclude group: "com.sedmelluq", module: "lavaplayer"
26 | }
27 |
28 | // native library loading
29 | implementation "com.github.natanbc:native-loader:$native_loader_version"
30 | implementation "com.github.natanbc:lp-cross:$lpcross_version"
31 |
32 | // filters
33 | implementation "com.github.natanbc:lavadsp:$lavadsp_version"
34 |
35 | /* logging */
36 | implementation "ch.qos.logback:logback-classic:$logback_version"
37 | implementation "com.github.ajalt.mordant:mordant:$mordant_version"
38 |
39 | /* config */
40 | implementation "com.uchuhimo:konf-core:$konf_version"
41 | implementation "com.uchuhimo:konf-yaml:$konf_version"
42 |
43 | /* serialization */
44 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_json_version"
45 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.1.0"
46 |
47 | /* netty */
48 | implementation "io.netty:netty-transport:$netty_version"
49 | implementation "io.netty:netty-transport-native-epoll:$netty_version:linux-x86_64"
50 |
51 | /* ktor */
52 |
53 | // Serialization
54 | implementation "io.ktor:ktor-serialization:$ktor_version"
55 |
56 | // Server
57 | implementation "io.ktor:ktor-locations:$ktor_version"
58 | implementation "io.ktor:ktor-websockets:$ktor_version"
59 | implementation "io.ktor:ktor-server-cio:$ktor_version"
60 | implementation "io.ktor:ktor-server-core:$ktor_version"
61 |
62 | // Client
63 | implementation "io.ktor:ktor-client-core:$ktor_version"
64 | implementation "io.ktor:ktor-client-okhttp:$ktor_version"
65 | implementation "io.ktor:ktor-client-websockets:$ktor_version"
66 | }
67 |
68 | shadowJar {
69 | archiveBaseName.set("Obsidian")
70 | archiveClassifier.set("")
71 | archiveVersion.set("")
72 | }
73 |
74 | jar {
75 | manifest {
76 | attributes "Main-Class": mainClassName
77 | }
78 | }
79 |
80 | compileJava.options.encoding = "UTF-8"
81 |
82 | compileKotlin {
83 | sourceCompatibility = JavaVersion.VERSION_13
84 | targetCompatibility = JavaVersion.VERSION_13
85 |
86 | kotlinOptions {
87 | jvmTarget = "11"
88 | incremental = true
89 | freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalStdlibApi"
90 | freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi"
91 | freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
92 | freeCompilerArgs += "-Xopt-in=io.ktor.locations.KtorExperimentalLocationsAPI"
93 | freeCompilerArgs += "-Xinline-classes"
94 | }
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.sedmelluq.discord.lavaplayer.udpqueue.natives;
18 |
19 | import com.sedmelluq.lava.common.natives.NativeResourceHolder;
20 |
21 | import java.net.InetSocketAddress;
22 | import java.nio.ByteBuffer;
23 |
24 | /**
25 | * Manages sending out queues of UDP packets at a fixed interval.
26 | */
27 | public class UdpQueueManager extends NativeResourceHolder {
28 | private final int bufferCapacity;
29 | private final ByteBuffer packetBuffer;
30 | private final UdpQueueManagerLibrary library;
31 | private final long instance;
32 | private boolean released;
33 |
34 | /**
35 | * @param bufferCapacity Maximum number of packets in one queue
36 | * @param packetInterval Time interval between packets in a queue
37 | * @param maximumPacketSize Maximum packet size
38 | */
39 | public UdpQueueManager(int bufferCapacity, long packetInterval, int maximumPacketSize) {
40 | this.bufferCapacity = bufferCapacity;
41 | packetBuffer = ByteBuffer.allocateDirect(maximumPacketSize);
42 | library = UdpQueueManagerLibrary.getInstance();
43 | instance = library.create(bufferCapacity, packetInterval);
44 | }
45 |
46 | /**
47 | * If the queue does not exist yet, returns the maximum number of packets in a queue.
48 | *
49 | * @param key Unique queue identifier
50 | * @return Number of empty packet slots in the specified queue
51 | */
52 | public int getRemainingCapacity(long key) {
53 | synchronized (library) {
54 | if (released) {
55 | return 0;
56 | }
57 |
58 | return library.getRemainingCapacity(instance, key);
59 | }
60 | }
61 |
62 | /**
63 | * @return Total capacity used for queues in this manager.
64 | */
65 | public int getCapacity() {
66 | return bufferCapacity;
67 | }
68 |
69 | /**
70 | * Adds one packet to the specified queue. Will fail if the maximum size of the queue is reached. There is no need to
71 | * manually create a queue, it is automatically created when the first packet is added to it and deleted when it
72 | * becomes empty.
73 | *
74 | * @param key Unique queue identifier
75 | * @param packet Packet to add to the queue
76 | * @return True if adding the packet to the queue succeeded
77 | */
78 | public boolean queuePacket(long key, ByteBuffer packet, InetSocketAddress address) {
79 | synchronized (library) {
80 | if (released) {
81 | return false;
82 | }
83 |
84 | int length = packet.remaining();
85 | packetBuffer.clear();
86 | packetBuffer.put(packet);
87 |
88 | int port = address.getPort();
89 | String hostAddress = address.getAddress().getHostAddress();
90 | return library.queuePacket(instance, key, hostAddress, port, packetBuffer, length);
91 | }
92 | }
93 |
94 | /**
95 | * This is the method that should be called to start processing the queues. It will use the current thread and return
96 | * only when close() method is called on the queue manager.
97 | */
98 | public void process() {
99 | library.process(instance);
100 | }
101 |
102 | @Override
103 | protected void freeResources() {
104 | synchronized (library) {
105 | released = true;
106 | library.destroy(instance);
107 | }
108 | }
109 |
110 | /**
111 | * Simulate a GC pause stop-the-world by starting a heap iteration via JVMTI. The behaviour of this stop-the-world is
112 | * identical to that of an actual GC pause, so nothing in Java can execute during the pause.
113 | *
114 | * @param length Length of the pause in milliseconds
115 | */
116 | public static void pauseDemo(int length) {
117 | UdpQueueManagerLibrary.getInstance();
118 | UdpQueueManagerLibrary.pauseDemo(length);
119 | }
120 | }
121 |
122 |
--------------------------------------------------------------------------------
/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.sedmelluq.discord.lavaplayer.udpqueue.natives;
18 |
19 |
20 | import com.sedmelluq.lava.common.natives.NativeLibraryLoader;
21 |
22 | import java.nio.ByteBuffer;
23 |
24 | public class UdpQueueManagerLibrary {
25 | private static final NativeLibraryLoader nativeLoader =
26 | NativeLibraryLoader.create(UdpQueueManagerLibrary.class, "udpqueue");
27 |
28 | private UdpQueueManagerLibrary() {
29 |
30 | }
31 |
32 | public static UdpQueueManagerLibrary getInstance() {
33 | nativeLoader.load();
34 | return new UdpQueueManagerLibrary();
35 | }
36 |
37 | public native long create(int bufferCapacity, long packetInterval);
38 |
39 | public native void destroy(long instance);
40 |
41 | public native int getRemainingCapacity(long instance, long key);
42 |
43 | public native boolean queuePacket(long instance, long key, String address, int port, ByteBuffer dataDirectBuffer,
44 | int dataLength);
45 |
46 | public native boolean queuePacketWithSocket(long instance, long key, String address, int port,
47 | ByteBuffer dataDirectBuffer, int dataLength, long explicitSocket);
48 |
49 | public native boolean deleteQueue(long instance, long key);
50 |
51 | public native void process(long instance);
52 |
53 | public native void processWithSocket(long instance, long ipv4Handle, long ipv6Handle);
54 |
55 | public static native void pauseDemo(int length);
56 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/Bedrock.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock
18 |
19 | import com.uchuhimo.konf.ConfigSpec
20 | import io.netty.buffer.ByteBufAllocator
21 | import io.netty.buffer.PooledByteBufAllocator
22 | import io.netty.buffer.UnpooledByteBufAllocator
23 | import io.netty.channel.EventLoopGroup
24 | import io.netty.channel.epoll.EpollDatagramChannel
25 | import io.netty.channel.epoll.EpollEventLoopGroup
26 | import io.netty.channel.epoll.EpollSocketChannel
27 | import io.netty.channel.nio.NioEventLoopGroup
28 | import io.netty.channel.socket.DatagramChannel
29 | import io.netty.channel.socket.SocketChannel
30 | import io.netty.channel.socket.nio.NioDatagramChannel
31 | import io.netty.channel.socket.nio.NioSocketChannel
32 | import obsidian.bedrock.gateway.GatewayVersion
33 | import obsidian.bedrock.codec.framePoller.FramePollerFactory
34 | import obsidian.bedrock.codec.framePoller.UdpQueueFramePollerFactory
35 | import obsidian.server.Obsidian.config
36 | import org.slf4j.Logger
37 | import org.slf4j.LoggerFactory
38 |
39 | object Bedrock {
40 | private val logger: Logger = LoggerFactory.getLogger(Bedrock::class.java)
41 | private val epollAvailable: Boolean by lazy { config[Config.UseEpoll] }
42 |
43 | val highPacketPriority: Boolean
44 | get() = config[Config.HighPacketPriority]
45 |
46 | /**
47 | * The [FramePollerFactory] to use.
48 | */
49 | val framePollerFactory: FramePollerFactory = UdpQueueFramePollerFactory()
50 |
51 | /**
52 | * The netty [ByteBufAllocator] to use when sending audio frames.
53 | */
54 | val byteBufAllocator: ByteBufAllocator by lazy {
55 | when (val allocator = config[Config.Allocator]) {
56 | "pooled", "default" -> PooledByteBufAllocator.DEFAULT
57 | "netty" -> ByteBufAllocator.DEFAULT
58 | "unpooled" -> UnpooledByteBufAllocator.DEFAULT
59 | else -> {
60 | logger.warn("Invalid byte buf allocator '$allocator', defaulting to the 'pooled' byte buf allocator.")
61 | PooledByteBufAllocator.DEFAULT
62 | }
63 | }
64 | }
65 |
66 | /**
67 | * The netty [EventLoopGroup] being used.
68 | * Defaults to [NioEventLoopGroup] if Epoll isn't available.
69 | */
70 | val eventLoopGroup: EventLoopGroup by lazy {
71 | if (epollAvailable) EpollEventLoopGroup() else NioEventLoopGroup()
72 | }
73 |
74 | /**
75 | * The class of the netty [DatagramChannel] being used.
76 | * Defaults to [NioDatagramChannel] if Epoll isn't available
77 | */
78 | val datagramChannelClass: Class by lazy {
79 | if (epollAvailable) EpollDatagramChannel::class.java else NioDatagramChannel::class.java
80 | }
81 |
82 | /**
83 | * The [GatewayVersion] to use.
84 | */
85 | val gatewayVersion = GatewayVersion.V4
86 |
87 | object Config : ConfigSpec("bedrock") {
88 | val UseEpoll by optional(true, "use-epoll")
89 | val Allocator by optional("pooled")
90 | val HighPacketPriority by optional(true, "high-packet-priority")
91 | }
92 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/BedrockClient.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock
18 |
19 | import java.util.concurrent.ConcurrentHashMap
20 |
21 | class BedrockClient(val clientId: Long) {
22 | /**
23 | * All media connections that are currently being handled.
24 | */
25 | private val connections = ConcurrentHashMap()
26 |
27 | /**
28 | * Creates a new media connection for the provided guild id.
29 | *
30 | * @param guildId The guild id.
31 | */
32 | fun createConnection(guildId: Long): MediaConnection =
33 | connections.computeIfAbsent(guildId) { MediaConnection(this, guildId) }
34 |
35 | /**
36 | * Get the MediaConnection for the provided guild id.
37 | *
38 | * @param guildId
39 | */
40 | fun getConnection(guildId: Long): MediaConnection? =
41 | connections[guildId]
42 |
43 | /**
44 | * Destroys the MediaConnection for the provided guild id.
45 | *
46 | * @param guildId
47 | */
48 | suspend fun destroyConnection(guildId: Long) =
49 | removeConnection(guildId)?.close()
50 |
51 | /**
52 | * Removes the MediaConnection of the provided guild id.
53 | *
54 | * @param guildId
55 | */
56 | fun removeConnection(guildId: Long): MediaConnection? =
57 | connections.remove(guildId)
58 |
59 | /**
60 | * Closes this BedrockClient.
61 | */
62 | suspend fun close() {
63 | if (!connections.isEmpty()) {
64 | for ((id, conn) in connections) {
65 | removeConnection(id)
66 | conn.close();
67 | }
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/Event.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock
18 |
19 | import io.ktor.util.network.*
20 | import obsidian.bedrock.gateway.event.ClientConnect
21 |
22 | interface Event {
23 | /**
24 | * Media connection
25 | */
26 | val mediaConnection: MediaConnection
27 |
28 | /**
29 | * Client that emitted this event
30 | */
31 | val client: BedrockClient
32 | get() = mediaConnection.bedrockClient
33 |
34 | /**
35 | * ID of the guild
36 | */
37 | val guildId: Long
38 | get() = mediaConnection.id
39 | }
40 |
41 | data class GatewayClosedEvent(
42 | override val mediaConnection: MediaConnection,
43 | val code: Short,
44 | val reason: String?
45 | ) : Event
46 |
47 | data class GatewayReadyEvent(
48 | override val mediaConnection: MediaConnection,
49 | val ssrc: Int,
50 | val target: NetworkAddress
51 | ) : Event
52 |
53 | data class HeartbeatSentEvent(
54 | override val mediaConnection: MediaConnection,
55 | val nonce: Long
56 | ) : Event
57 |
58 | data class HeartbeatAcknowledgedEvent(
59 | override val mediaConnection: MediaConnection,
60 | val nonce: Long
61 | ) : Event
62 |
63 | data class UserConnectedEvent(
64 | override val mediaConnection: MediaConnection,
65 | val event: ClientConnect
66 | ) : Event
67 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/MediaConnection.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock
18 |
19 | import io.ktor.util.*
20 | import kotlinx.coroutines.*
21 | import kotlinx.coroutines.channels.Channel
22 | import kotlinx.coroutines.flow.*
23 | import obsidian.bedrock.codec.Codec
24 | import obsidian.bedrock.codec.OpusCodec
25 | import obsidian.bedrock.codec.framePoller.FramePoller
26 | import obsidian.bedrock.gateway.MediaGatewayConnection
27 | import obsidian.bedrock.handler.ConnectionHandler
28 | import obsidian.bedrock.media.MediaFrameProvider
29 | import org.slf4j.Logger
30 | import org.slf4j.LoggerFactory
31 | import kotlin.coroutines.CoroutineContext
32 |
33 | class MediaConnection(
34 | val bedrockClient: BedrockClient,
35 | val id: Long,
36 | private val dispatcher: CoroutineDispatcher = Dispatchers.Default
37 | ) : CoroutineScope {
38 |
39 | /**
40 | * The [ConnectionHandler].
41 | */
42 | var connectionHandler: ConnectionHandler? = null
43 |
44 | /**
45 | * The [VoiceServerInfo] provided.
46 | */
47 | var info: VoiceServerInfo? = null
48 |
49 | /**
50 | * The [MediaFrameProvider].
51 | */
52 | var frameProvider: MediaFrameProvider? = null
53 | set(value) {
54 | if (field != null) {
55 | field?.dispose()
56 | }
57 |
58 | field = value
59 | }
60 |
61 | /**
62 | * Event flow
63 | */
64 | val events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE)
65 |
66 | override val coroutineContext: CoroutineContext
67 | get() = dispatcher + SupervisorJob()
68 |
69 | /**
70 | * The [MediaGatewayConnection].
71 | */
72 | private var mediaGatewayConnection: MediaGatewayConnection? = null
73 |
74 | /**
75 | * The audio [Codec] to use when sending frames.
76 | */
77 | private val audioCodec: Codec by lazy { OpusCodec.INSTANCE }
78 |
79 | /**
80 | * The [FramePoller].
81 | */
82 | private val framePoller: FramePoller = Bedrock.framePollerFactory.createFramePoller(audioCodec, this)!!
83 |
84 | /**
85 | * Connects to the Discord voice server described in [info]
86 | *
87 | * @param info The voice server info.
88 | */
89 | suspend fun connect(info: VoiceServerInfo) {
90 | if (mediaGatewayConnection != null) {
91 | disconnect()
92 | }
93 |
94 | val connection = Bedrock.gatewayVersion.createConnection(this, info)
95 | mediaGatewayConnection = connection
96 | connection.start()
97 | }
98 |
99 | /**
100 | * Disconnects from the voice server.
101 | */
102 | suspend fun disconnect() {
103 | logger.debug("Disconnecting...")
104 |
105 | stopFramePolling()
106 | if (mediaGatewayConnection != null && mediaGatewayConnection?.open == true) {
107 | mediaGatewayConnection?.close(1000, null)
108 | mediaGatewayConnection = null
109 | }
110 |
111 | if (connectionHandler != null) {
112 | withContext(Dispatchers.IO) {
113 | connectionHandler?.close()
114 | }
115 |
116 | connectionHandler = null
117 | }
118 | }
119 |
120 | /**
121 | * Starts the [FramePoller] for this media connection.
122 | */
123 | suspend fun startFramePolling() {
124 | if (this.framePoller.polling) {
125 | return
126 | }
127 |
128 | this.framePoller.start()
129 | }
130 |
131 | /**
132 | * Stops the [FramePoller] for this media connection
133 | */
134 | fun stopFramePolling() {
135 | if (!this.framePoller.polling) {
136 | return
137 | }
138 |
139 | this.framePoller.stop()
140 | }
141 |
142 | /**
143 | * Updates the speaking state with the provided [mask]
144 | *
145 | * @param mask The speaking mask to update with
146 | */
147 | suspend fun updateSpeakingState(mask: Int) =
148 | mediaGatewayConnection?.updateSpeaking(mask)
149 |
150 | /**
151 | * Closes this media connection.
152 | */
153 | suspend fun close() {
154 | if (frameProvider != null) {
155 | frameProvider?.dispose()
156 | frameProvider = null
157 | }
158 |
159 | disconnect()
160 | coroutineContext.cancel()
161 | bedrockClient.removeConnection(id)
162 | }
163 |
164 | companion object {
165 | val logger: Logger = LoggerFactory.getLogger(MediaConnection::class.java)
166 | }
167 | }
168 |
169 | /**
170 | * Convenience method that calls [block] whenever event [T] is emitted on [MediaConnection.events]
171 | *
172 | * @param scope Scope to launch the job in
173 | * @param block Block to call when [T] is emitted
174 | *
175 | * @return A [Job] that can be used to cancel any further processing of event [T]
176 | */
177 | inline fun MediaConnection.on(
178 | scope: CoroutineScope = this,
179 | crossinline block: suspend T.() -> Unit
180 | ): Job {
181 | return events.buffer(Channel.UNLIMITED)
182 | .filterIsInstance()
183 | .onEach { event ->
184 | event
185 | .runCatching { block() }
186 | .onFailure { MediaConnection.logger.error(it) }
187 | }
188 | .launchIn(scope)
189 | }
190 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/VoiceServerInfo.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock
18 |
19 | data class VoiceServerInfo(
20 | val sessionId: String,
21 | val token: String,
22 | val endpoint: String
23 | )
24 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/codec/Codec.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.codec
18 |
19 | import obsidian.bedrock.gateway.event.CodecDescription
20 |
21 | abstract class Codec {
22 | /**
23 | * The name of this Codec
24 | */
25 | abstract val name: String
26 |
27 | /**
28 | * The type of payload this codec provides.
29 | */
30 | abstract val payloadType: Byte
31 |
32 | /**
33 | * The priority of this codec.
34 | */
35 | abstract val priority: Int
36 |
37 | /**
38 | * The JSON description of this Codec.
39 | */
40 | abstract val description: CodecDescription
41 |
42 | /**
43 | * The type of this codec, can only be audio
44 | */
45 | val codecType: CodecType = CodecType.AUDIO
46 |
47 | /**
48 | * The type of rtx-payload this codec provides.
49 | */
50 | val rtxPayloadType: Byte = 0
51 |
52 | override operator fun equals(other: Any?): Boolean {
53 | if (this === other) {
54 | return true
55 | }
56 |
57 | if (other == null || javaClass != other.javaClass) {
58 | return false
59 | }
60 |
61 | return payloadType == (other as Codec).payloadType
62 | }
63 |
64 | override fun hashCode(): Int {
65 | var result = name.hashCode()
66 | result = 31 * result + payloadType
67 | result = 31 * result + priority
68 | result = 31 * result + description.hashCode()
69 | result = 31 * result + codecType.hashCode()
70 | result = 31 * result + rtxPayloadType
71 |
72 | return result
73 | }
74 |
75 | companion object {
76 | /**
77 | * List of all audio codecs available
78 | */
79 | private val AUDIO_CODECS: List by lazy {
80 | listOf(OpusCodec.INSTANCE)
81 | }
82 |
83 | /**
84 | * Gets audio codec description by name.
85 | *
86 | * @param name the codec name
87 | * @return Codec instance or null if the codec is not found/supported by Bedrock
88 | */
89 | fun getAudio(name: String): Codec? = AUDIO_CODECS.find {
90 | it.name == name
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/codec/CodecType.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.codec
18 |
19 | import kotlinx.serialization.SerialName
20 | import kotlinx.serialization.Serializable
21 |
22 | @Serializable
23 | enum class CodecType {
24 | @SerialName("audio")
25 | AUDIO
26 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/codec/OpusCodec.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.codec
18 |
19 | import obsidian.bedrock.gateway.event.CodecDescription
20 |
21 | class OpusCodec : Codec() {
22 | override val name = "opus"
23 |
24 | override val priority = 1000
25 |
26 | override val payloadType: Byte = PAYLOAD_TYPE
27 |
28 | override val description = CodecDescription(
29 | name = name,
30 | payloadType = payloadType,
31 | priority = priority,
32 | type = CodecType.AUDIO
33 | )
34 |
35 | companion object {
36 | /**
37 | * The payload type of the Opus codec.
38 | */
39 | const val PAYLOAD_TYPE: Byte = 120
40 |
41 | /**
42 | * The frame duration for every Opus frame.
43 | */
44 | const val FRAME_DURATION: Int = 20
45 |
46 | /**
47 | * Represents a Silence Frame within opus.
48 | */
49 | val SILENCE_FRAME = byteArrayOf(0xF8.toByte(), 0xFF.toByte(), 0xFE.toByte())
50 |
51 | /**
52 | * A pre-defined instance of [OpusCodec]
53 | */
54 | val INSTANCE: OpusCodec by lazy { OpusCodec() }
55 | }
56 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/AbstractFramePoller.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.codec.framePoller
18 |
19 | import io.netty.buffer.ByteBufAllocator
20 | import io.netty.channel.EventLoopGroup
21 | import kotlinx.coroutines.ExecutorCoroutineDispatcher
22 | import kotlinx.coroutines.asCoroutineDispatcher
23 | import obsidian.bedrock.Bedrock
24 | import obsidian.bedrock.MediaConnection
25 |
26 | abstract class AbstractFramePoller(protected val connection: MediaConnection) : FramePoller {
27 | /**
28 | * Whether we're polling or not.
29 | */
30 | override var polling = false
31 |
32 | /**
33 | * The [ByteBufAllocator] to use.
34 | */
35 | protected val allocator: ByteBufAllocator = Bedrock.byteBufAllocator
36 |
37 | /**
38 | * The [EventLoopGroup] being used.
39 | */
40 | protected val eventLoop: EventLoopGroup = Bedrock.eventLoopGroup
41 |
42 | /**
43 | * The [eventLoop] as a [ExecutorCoroutineDispatcher]
44 | */
45 | protected val eventLoopDispatcher = eventLoop.asCoroutineDispatcher()
46 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/FramePoller.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.codec.framePoller
18 |
19 | interface FramePoller {
20 | /**
21 | * Used to check whether this FramePoller is currently polling.
22 | */
23 | val polling: Boolean
24 |
25 | /**
26 | * Used to start polling.
27 | */
28 | suspend fun start()
29 |
30 | /**
31 | * Used to stop polling.
32 | */
33 | fun stop()
34 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/FramePollerFactory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.codec.framePoller
18 |
19 | import obsidian.bedrock.MediaConnection
20 | import obsidian.bedrock.codec.Codec
21 |
22 | interface FramePollerFactory {
23 | /**
24 | * Creates a frame poller using the provided [Codec] and [MediaConnection]
25 | */
26 | fun createFramePoller(codec: Codec, connection: MediaConnection): FramePoller?
27 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/QueueManagerPool.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.codec.framePoller
18 |
19 | import com.sedmelluq.discord.lavaplayer.udpqueue.natives.UdpQueueManager
20 | import obsidian.bedrock.codec.OpusCodec
21 | import java.net.InetSocketAddress
22 | import java.nio.ByteBuffer
23 | import java.util.concurrent.TimeUnit
24 | import java.util.concurrent.atomic.AtomicLong
25 | import kotlin.concurrent.thread
26 |
27 | class QueueManagerPool(
28 | size: Int,
29 | bufferDuration: Int
30 | ) {
31 | private val queueKeySeq = AtomicLong()
32 | private val managers: List
33 | private var closed = false
34 |
35 | init {
36 | require(size > 0) {
37 | "Pool size must be higher or equal to 1."
38 | }
39 |
40 | managers = List(size) {
41 | val queueManager = UdpQueueManager(
42 | bufferDuration / OpusCodec.FRAME_DURATION,
43 | TimeUnit.MILLISECONDS.toNanos(OpusCodec.FRAME_DURATION.toLong()),
44 | UdpQueueFramePollerFactory.MAXIMUM_PACKET_SIZE
45 | )
46 |
47 | /* create thread */
48 | thread(
49 | name = "Queue Manager Pool $it",
50 | isDaemon = true,
51 | priority = (Thread.NORM_PRIORITY + Thread.MAX_PRIORITY) / 2,
52 | block = queueManager::process
53 | )
54 |
55 | /* return queue manager */
56 | queueManager
57 | }
58 | }
59 |
60 | fun close() {
61 | if (closed) {
62 | return
63 | }
64 |
65 | closed = true
66 | managers.forEach(UdpQueueManager::close)
67 | }
68 |
69 | fun getNextWrapper(): UdpQueueWrapper =
70 | getWrapperForKey(this.queueKeySeq.getAndIncrement())
71 |
72 | fun getWrapperForKey(queueKey: Long): UdpQueueWrapper =
73 | UdpQueueWrapper(
74 | managers[(queueKey % managers.size.toLong()).toInt()],
75 | queueKey
76 | )
77 |
78 | class UdpQueueWrapper(val manager: UdpQueueManager, val queueKey: Long) {
79 | val remainingCapacity: Int
80 | get() = manager.getRemainingCapacity(this.queueKey)
81 |
82 | fun queuePacket(packet: ByteBuffer, address: InetSocketAddress): Boolean =
83 | manager.queuePacket(this.queueKey, packet, address)
84 | }
85 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/UdpQueueFramePollerFactory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.codec.framePoller
18 |
19 | import obsidian.bedrock.MediaConnection
20 | import obsidian.bedrock.codec.Codec
21 | import obsidian.bedrock.codec.OpusCodec
22 |
23 | class UdpQueueFramePollerFactory(
24 | bufferDuration: Int = DEFAULT_BUFFER_DURATION,
25 | poolSize: Int = Runtime.getRuntime().availableProcessors()
26 | ) : FramePollerFactory {
27 | private val pool = QueueManagerPool(poolSize, bufferDuration)
28 |
29 | override fun createFramePoller(codec: Codec, connection: MediaConnection): FramePoller? {
30 | if (OpusCodec.INSTANCE == codec) {
31 | return UdpQueueOpusFramePoller(pool.getNextWrapper(), connection)
32 | }
33 |
34 | return null
35 | }
36 |
37 | companion object {
38 | /**
39 | * The default packet size used by Opus frames
40 | */
41 | const val MAXIMUM_PACKET_SIZE = 4096
42 |
43 | /**
44 | * The default frame buffer duration.
45 | */
46 | const val DEFAULT_BUFFER_DURATION: Int = 400
47 | }
48 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/UdpQueueOpusFramePoller.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.codec.framePoller
18 |
19 | import kotlinx.coroutines.CoroutineScope
20 | import kotlinx.coroutines.Job
21 | import kotlinx.coroutines.runBlocking
22 | import obsidian.bedrock.MediaConnection
23 | import obsidian.bedrock.codec.OpusCodec
24 | import obsidian.bedrock.handler.DiscordUDPConnection
25 | import obsidian.bedrock.media.IntReference
26 | import java.net.InetSocketAddress
27 | import java.util.concurrent.ScheduledExecutorService
28 | import java.util.concurrent.ScheduledFuture
29 | import java.util.concurrent.TimeUnit
30 | import kotlin.coroutines.CoroutineContext
31 |
32 | class UdpQueueOpusFramePoller(
33 | private val wrapper: QueueManagerPool.UdpQueueWrapper,
34 | connection: MediaConnection
35 | ) : AbstractFramePoller(connection), CoroutineScope {
36 | private val timestamp = IntReference()
37 | private var lastFrame: Long = 0
38 |
39 | override val coroutineContext: CoroutineContext
40 | get() = eventLoopDispatcher + Job()
41 |
42 | override suspend fun start() {
43 | if (polling) {
44 | throw IllegalStateException("Polling already started!")
45 | }
46 |
47 | polling = true
48 | lastFrame = System.currentTimeMillis()
49 | populateQueue()
50 | }
51 |
52 | override fun stop() {
53 | if (polling) {
54 | polling = false
55 | }
56 | }
57 |
58 | private suspend fun populateQueue() {
59 | if (!polling) {
60 | return
61 | }
62 |
63 | val handler = connection.connectionHandler as DiscordUDPConnection?
64 | val frameProvider = connection.frameProvider
65 | val codec = OpusCodec.INSTANCE
66 |
67 | for (i in 0 until wrapper.remainingCapacity) {
68 | if (frameProvider != null && handler != null && frameProvider.canSendFrame(codec)) {
69 | val buf = allocator.buffer()
70 | val start = buf.writerIndex()
71 |
72 | frameProvider.retrieve(codec, buf, timestamp)
73 |
74 | val packet =
75 | handler.createPacket(OpusCodec.PAYLOAD_TYPE, timestamp.get(), buf, buf.writerIndex() - start, false)
76 |
77 | if (packet != null) {
78 | wrapper.queuePacket(packet.nioBuffer(), handler.serverAddress as InetSocketAddress)
79 | packet.release()
80 | }
81 |
82 | buf.release()
83 | }
84 | }
85 |
86 | val frameDelay = 40 - (System.currentTimeMillis() - lastFrame)
87 | if (frameDelay > 0) {
88 | eventLoop.schedule(frameDelay) {
89 | runBlocking(coroutineContext) { loop() }
90 | }
91 | } else {
92 | loop()
93 | }
94 | }
95 |
96 | private suspend fun loop() {
97 | if (System.currentTimeMillis() < lastFrame + 60) {
98 | lastFrame += 40
99 | } else {
100 | lastFrame = System.currentTimeMillis()
101 | }
102 |
103 | populateQueue()
104 | }
105 |
106 | companion object {
107 | fun ScheduledExecutorService.schedule(
108 | delay: Long,
109 | timeUnit: TimeUnit = TimeUnit.MILLISECONDS,
110 | block: Runnable
111 | ): ScheduledFuture<*> =
112 | schedule(block, delay, timeUnit)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/crypto/DefaultEncryptionModes.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.crypto
18 |
19 | object DefaultEncryptionModes {
20 | val encryptionModes = mapOf(
21 | "xsalsa20_poly1305_lite" to { XSalsa20Poly1305LiteEncryptionMode() },
22 | "xsalsa20_poly1305_suffix" to { XSalsa20Poly1305SuffixEncryptionMode() },
23 | "xsalsa20_poly1305" to { XSalsa20Poly1305EncryptionMode() },
24 | "plain" to { PlainEncryptionMode() }
25 | )
26 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/crypto/EncryptionMode.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.crypto
18 |
19 | import io.netty.buffer.ByteBuf
20 |
21 | interface EncryptionMode {
22 | val name: String
23 |
24 | fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean
25 |
26 | companion object {
27 | fun select(modes: List): String {
28 | for (mode in modes) {
29 | val impl = DefaultEncryptionModes.encryptionModes[mode]
30 | if (impl != null) {
31 | return mode
32 | }
33 | }
34 |
35 | throw UnsupportedEncryptionModeException("Cannot find a suitable encryption mode for this connection!")
36 | }
37 |
38 | operator fun get(mode: String): EncryptionMode? =
39 | DefaultEncryptionModes.encryptionModes[mode]?.invoke()
40 | }
41 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/crypto/PlainEncryptionMode.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.crypto
18 |
19 | import io.netty.buffer.ByteBuf
20 |
21 | class PlainEncryptionMode : EncryptionMode {
22 | override val name: String = "plain"
23 |
24 | override fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean {
25 | opus.readerIndex(start)
26 | output.writeBytes(opus)
27 |
28 | return true
29 | }
30 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/crypto/UnsupportedEncryptionModeException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.crypto
18 |
19 | class UnsupportedEncryptionModeException(message: String) : IllegalArgumentException(message)
20 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305EncryptionMode.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.crypto
18 |
19 | import io.netty.buffer.ByteBuf
20 | import me.uport.knacl.NaClLowLevel
21 |
22 | class XSalsa20Poly1305EncryptionMode : EncryptionMode {
23 | override val name: String = "xsalsa20_poly1305"
24 |
25 | private val extendedNonce = ByteArray(24)
26 | private val m = ByteArray(984)
27 | private val c = ByteArray(984)
28 |
29 | override fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean {
30 | for (i in c.indices) {
31 | m[i] = 0
32 | c[i] = 0
33 | }
34 |
35 | for (i in 0 until start) {
36 | m[(i + 32)] = opus.readByte()
37 | }
38 |
39 | output.getBytes(0, extendedNonce, 0, 12)
40 | if (NaClLowLevel.crypto_secretbox(c, m, (start + 32).toLong(), extendedNonce, secretKey) == 0) {
41 | for (i in 0 until start + 16) {
42 | output.writeByte(c[(i + 16)].toInt())
43 | }
44 |
45 | return true
46 | }
47 |
48 | return false
49 | }
50 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305LiteEncryptionMode.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.crypto
18 |
19 | import io.netty.buffer.ByteBuf
20 | import me.uport.knacl.NaClLowLevel
21 |
22 | class XSalsa20Poly1305LiteEncryptionMode : EncryptionMode {
23 | override val name: String = "xsalsa20_poly1305_lite"
24 |
25 | private val extendedNonce = ByteArray(24)
26 | private val m = ByteArray(984)
27 | private val c = ByteArray(984)
28 | private var seq = 0x80000000
29 |
30 | override fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean {
31 | for (i in c.indices) {
32 | m[i] = 0
33 | c[i] = 0
34 | }
35 |
36 | for (i in 0 until start) {
37 | m[(i + 32)] = opus.readByte()
38 | }
39 |
40 | val s = seq++
41 | extendedNonce[0] = (s and 0xff).toByte()
42 | extendedNonce[1] = ((s shr 8) and 0xff).toByte()
43 | extendedNonce[2] = ((s shr 16) and 0xff).toByte()
44 | extendedNonce[3] = ((s shr 24) and 0xff).toByte()
45 |
46 | if (NaClLowLevel.crypto_secretbox(c, m, (start + 32).toLong(), extendedNonce, secretKey) == 0) {
47 | for (i in 0 until start + 16) {
48 | output.writeByte(c[(i + 16)].toInt())
49 | }
50 |
51 | output.writeIntLE(s.toInt())
52 | return true
53 | }
54 |
55 | return false
56 | }
57 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305SuffixEncryptionMode.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.crypto
18 |
19 | import io.netty.buffer.ByteBuf
20 | import me.uport.knacl.NaClLowLevel
21 | import java.util.concurrent.ThreadLocalRandom
22 |
23 | class XSalsa20Poly1305SuffixEncryptionMode : EncryptionMode {
24 | override val name: String = "xsalsa20_poly1305_suffix"
25 |
26 | private val extendedNonce = ByteArray(24)
27 | private val m = ByteArray(984)
28 | private val c = ByteArray(984)
29 |
30 | override fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean {
31 | for (i in c.indices) {
32 | m[i] = 0
33 | c[i] = 0
34 | }
35 |
36 | for (i in 0 until start) m[i + 32] = opus.readByte()
37 |
38 | ThreadLocalRandom.current().nextBytes(extendedNonce)
39 | if (NaClLowLevel.crypto_secretbox(c, m, (start + 32).toLong(), extendedNonce, secretKey) == 0) {
40 | for (i in 0 until start + 16) {
41 | output.writeByte(c[i + 16].toInt())
42 | }
43 |
44 | output.writeBytes(extendedNonce)
45 | return true
46 | }
47 |
48 | return false
49 | }
50 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/gateway/GatewayVersion.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.gateway
18 |
19 | import obsidian.bedrock.MediaConnection
20 | import obsidian.bedrock.VoiceServerInfo
21 |
22 | typealias MediaGatewayConnectionFactory = (MediaConnection, VoiceServerInfo) -> MediaGatewayConnection
23 |
24 | enum class GatewayVersion(private val factory: MediaGatewayConnectionFactory) {
25 | V4({ a, b -> MediaGatewayV4Connection(a, b) });
26 |
27 | /**
28 | * Creates a new [MediaGatewayConnection]
29 | *
30 | * @param connection The media connection.
31 | * @param voiceServerInfo The voice server information.
32 | */
33 | fun createConnection(connection: MediaConnection, voiceServerInfo: VoiceServerInfo): MediaGatewayConnection =
34 | factory.invoke(connection, voiceServerInfo)
35 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayConnection.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.gateway
18 |
19 | import kotlinx.coroutines.CoroutineScope
20 |
21 | interface MediaGatewayConnection : CoroutineScope {
22 | /**
23 | * Whether the gateway connection is opened.
24 | */
25 | val open: Boolean
26 |
27 | /**
28 | * Starts connecting to the gateway.
29 | */
30 | suspend fun start()
31 |
32 | /**
33 | * Closes the gateway connection.
34 | *
35 | * @param code The close code.
36 | * @param reason The close reason.
37 | */
38 | suspend fun close(code: Short, reason: String?)
39 |
40 | /**
41 | * Updates the speaking state of the Client.
42 | *
43 | * @param mask The speaking mask.
44 | */
45 | suspend fun updateSpeaking(mask: Int)
46 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayV4Connection.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.gateway
18 |
19 | import io.ktor.util.network.*
20 | import kotlinx.coroutines.ObsoleteCoroutinesApi
21 | import obsidian.bedrock.MediaConnection
22 | import obsidian.bedrock.VoiceServerInfo
23 | import obsidian.bedrock.codec.OpusCodec
24 | import obsidian.bedrock.crypto.EncryptionMode
25 | import obsidian.bedrock.*
26 | import obsidian.bedrock.gateway.event.*
27 | import obsidian.bedrock.handler.DiscordUDPConnection
28 | import obsidian.bedrock.util.Interval
29 | import java.util.*
30 |
31 | @ObsoleteCoroutinesApi
32 | class MediaGatewayV4Connection(
33 | mediaConnection: MediaConnection,
34 | voiceServerInfo: VoiceServerInfo
35 | ) : AbstractMediaGatewayConnection(mediaConnection, voiceServerInfo, 4) {
36 | private var ssrc = 0
37 | private var address: NetworkAddress? = null
38 | private var rtcConnectionId: UUID? = null
39 | private var interval: Interval = Interval()
40 |
41 | private lateinit var encryptionModes: List
42 |
43 | init {
44 | on {
45 | logger.debug("Received HELLO, heartbeat interval: $heartbeatInterval")
46 | startHeartbeating(heartbeatInterval)
47 | }
48 |
49 | on {
50 | logger.debug("Received READY, ssrc: $ssrc")
51 |
52 | /* update state */
53 | this@MediaGatewayV4Connection.ssrc = ssrc
54 | address = NetworkAddress(ip, port)
55 | encryptionModes = modes
56 |
57 | /* emit event */
58 | mediaConnection.events.emit(GatewayReadyEvent(mediaConnection, ssrc, address!!))
59 |
60 | /* select protocol */
61 | selectProtocol("udp")
62 | }
63 |
64 | on {
65 | mediaConnection.events.emit(HeartbeatAcknowledgedEvent(mediaConnection, nonce))
66 | }
67 |
68 | on {
69 | if (mediaConnection.connectionHandler != null) {
70 | mediaConnection.connectionHandler?.handleSessionDescription(this)
71 | } else {
72 | logger.warn("Received session description before protocol selection? (connection id = $rtcConnectionId)")
73 | }
74 | }
75 |
76 | on {
77 | mediaConnection.events.emit(UserConnectedEvent(mediaConnection, this))
78 | }
79 | }
80 |
81 | override suspend fun close(code: Short, reason: String?) {
82 | interval.stop()
83 | super.close(code, reason)
84 | }
85 |
86 | private suspend fun selectProtocol(protocol: String) {
87 | val mode = EncryptionMode.select(encryptionModes)
88 | logger.debug("Selected preferred encryption mode: $mode")
89 |
90 | rtcConnectionId = UUID.randomUUID()
91 | logger.debug("Generated new connection id: $rtcConnectionId")
92 |
93 | when (protocol.toLowerCase()) {
94 | "udp" -> {
95 | val connection = DiscordUDPConnection(mediaConnection, address!!, ssrc)
96 | val externalAddress = connection.connect()
97 |
98 | logger.debug("Connected, our external address is '$externalAddress'")
99 |
100 | sendPayload(SelectProtocol(
101 | protocol = "udp",
102 | codecs = SUPPORTED_CODECS,
103 | connectionId = rtcConnectionId!!,
104 | data = SelectProtocol.UDPInformation(
105 | address = externalAddress.address.hostAddress,
106 | port = externalAddress.port,
107 | mode = mode
108 | )
109 | ))
110 |
111 | sendPayload(Command.ClientConnect(
112 | audioSsrc = ssrc,
113 | videoSsrc = 0,
114 | rtxSsrc = 0
115 | ))
116 |
117 | mediaConnection.connectionHandler = connection
118 | logger.debug("Waiting for session description...")
119 | }
120 |
121 | else -> throw IllegalArgumentException("Protocol \"$protocol\" is not supported by Bedrock.")
122 | }
123 | }
124 |
125 | override suspend fun identify() {
126 | logger.debug("Identifying...")
127 |
128 | sendPayload(Identify(
129 | token = voiceServerInfo.token,
130 | guildId = mediaConnection.id,
131 | userId = mediaConnection.bedrockClient.clientId,
132 | sessionId = voiceServerInfo.sessionId
133 | ))
134 | }
135 |
136 | override suspend fun onClose(code: Short, reason: String?) {
137 | if (interval.started) {
138 | interval.stop()
139 | }
140 |
141 | val event = GatewayClosedEvent(mediaConnection, code, reason)
142 | mediaConnection.events.emit(event)
143 | }
144 |
145 | /**
146 | * Updates the speaking state of the Client.
147 | *
148 | * @param mask The speaking mask.
149 | */
150 | override suspend fun updateSpeaking(mask: Int) {
151 | sendPayload(Speaking(
152 | speaking = mask,
153 | delay = 0,
154 | ssrc = ssrc
155 | ))
156 | }
157 |
158 | /**
159 | * Starts the heartbeat ticker.
160 | *
161 | * @param delay Delay, in milliseconds, between heart-beats.
162 | */
163 | @ObsoleteCoroutinesApi
164 | private suspend fun startHeartbeating(delay: Double) {
165 | interval.start(delay.toLong()) {
166 | val nonce = System.currentTimeMillis()
167 |
168 | /* emit event */
169 | val event = HeartbeatSentEvent(mediaConnection, nonce)
170 | mediaConnection.events.tryEmit(event)
171 |
172 | /* send payload */
173 | sendPayload(Heartbeat(nonce))
174 | }
175 | }
176 |
177 | companion object {
178 | /**
179 | * All supported audio codecs.
180 | */
181 | val SUPPORTED_CODECS = listOf(OpusCodec.INSTANCE.description)
182 | }
183 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/gateway/SpeakingFlags.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.gateway
18 |
19 | object SpeakingFlags {
20 | const val NORMAL = 1
21 | const val SOUND_SHARE = 1 shl 1
22 | const val PRIORITY = 1 shl 2
23 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Command.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.gateway.event
18 |
19 | import kotlinx.serialization.KSerializer
20 | import kotlinx.serialization.SerialName
21 | import kotlinx.serialization.Serializable
22 | import kotlinx.serialization.SerializationStrategy
23 | import kotlinx.serialization.descriptors.PrimitiveKind
24 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
25 | import kotlinx.serialization.descriptors.SerialDescriptor
26 | import kotlinx.serialization.descriptors.buildClassSerialDescriptor
27 | import kotlinx.serialization.encoding.Decoder
28 | import kotlinx.serialization.encoding.Encoder
29 | import kotlinx.serialization.json.JsonObject
30 | import obsidian.bedrock.codec.CodecType
31 | import java.util.*
32 |
33 | sealed class Command {
34 | @Serializable
35 | data class ClientConnect(
36 | @SerialName("audio_ssrc") val audioSsrc: Int,
37 | @SerialName("video_ssrc") val videoSsrc: Int,
38 | @SerialName("rtx_ssrc") val rtxSsrc: Int,
39 | ) : Command()
40 |
41 | companion object : SerializationStrategy {
42 | override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Command") {
43 | element("op", Op.descriptor)
44 | element("d", JsonObject.serializer().descriptor)
45 | }
46 |
47 | override fun serialize(encoder: Encoder, value: Command) {
48 | val composite = encoder.beginStructure(descriptor)
49 | when (value) {
50 | is SelectProtocol -> {
51 | composite.encodeSerializableElement(descriptor, 0, Op, Op.SelectProtocol)
52 | composite.encodeSerializableElement(descriptor, 1, SelectProtocol.serializer(), value)
53 | }
54 |
55 | is Heartbeat -> {
56 | composite.encodeSerializableElement(descriptor, 0, Op, Op.Heartbeat)
57 | composite.encodeSerializableElement(descriptor, 1, Heartbeat.serializer(), value)
58 | }
59 |
60 | is ClientConnect -> {
61 | composite.encodeSerializableElement(descriptor, 0, Op, Op.ClientConnect)
62 | composite.encodeSerializableElement(descriptor, 1, ClientConnect.serializer(), value)
63 | }
64 |
65 | is Identify -> {
66 | composite.encodeSerializableElement(descriptor, 0, Op, Op.Identify)
67 | composite.encodeSerializableElement(descriptor, 1, Identify.serializer(), value)
68 | }
69 |
70 | is Speaking -> {
71 | composite.encodeSerializableElement(descriptor, 0, Op, Op.Speaking)
72 | composite.encodeSerializableElement(descriptor, 1, Speaking.serializer(), value)
73 | }
74 |
75 | }
76 |
77 | composite.endStructure(descriptor)
78 | }
79 | }
80 | }
81 |
82 | @Serializable
83 | data class SelectProtocol(
84 | val protocol: String,
85 | val codecs: List,
86 | @Serializable(with = UUIDSerializer::class)
87 | @SerialName("rtc_connection_id")
88 | val connectionId: UUID,
89 | val data: UDPInformation
90 | ) : Command() {
91 | @Serializable
92 | data class UDPInformation(
93 | val address: String,
94 | val port: Int,
95 | val mode: String
96 | )
97 | }
98 |
99 | @Serializable
100 | data class CodecDescription(
101 | val name: String,
102 | @SerialName("payload_type")
103 | val payloadType: Byte,
104 | val priority: Int,
105 | val type: CodecType
106 | )
107 |
108 | @Serializable
109 | data class Heartbeat(
110 | val nonce: Long
111 | ) : Command() {
112 | companion object : SerializationStrategy {
113 | override val descriptor: SerialDescriptor =
114 | PrimitiveSerialDescriptor("Heartbeat", PrimitiveKind.LONG)
115 |
116 | override fun serialize(encoder: Encoder, value: Heartbeat) {
117 | encoder.encodeLong(value.nonce)
118 | }
119 | }
120 | }
121 |
122 |
123 | @Serializable
124 | data class Identify(
125 | val token: String,
126 | @SerialName("server_id")
127 | val guildId: Long,
128 | @SerialName("user_id")
129 | val userId: Long,
130 | @SerialName("session_id")
131 | val sessionId: String
132 | ) : Command()
133 |
134 | @Serializable
135 | data class Speaking(
136 | val speaking: Int,
137 | val delay: Int,
138 | val ssrc: Int
139 | ) : Command()
140 |
141 | object UUIDSerializer : KSerializer {
142 | override val descriptor: SerialDescriptor =
143 | PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
144 |
145 | override fun serialize(encoder: Encoder, value: UUID) {
146 | encoder.encodeString(value.toString())
147 | }
148 |
149 | override fun deserialize(decoder: Decoder): UUID =
150 | UUID.fromString(decoder.decodeString())
151 | }
152 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Event.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.gateway.event
18 |
19 | import kotlinx.serialization.*
20 | import kotlinx.serialization.builtins.nullable
21 | import kotlinx.serialization.descriptors.PrimitiveKind
22 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
23 | import kotlinx.serialization.descriptors.SerialDescriptor
24 | import kotlinx.serialization.descriptors.buildClassSerialDescriptor
25 | import kotlinx.serialization.encoding.CompositeDecoder
26 | import kotlinx.serialization.encoding.Decoder
27 | import kotlinx.serialization.json.JsonElement
28 | import kotlinx.serialization.json.JsonObject
29 | import obsidian.server.io.Operation
30 |
31 | sealed class Event {
32 | companion object : DeserializationStrategy {
33 | override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Event") {
34 | element("op", Op.descriptor)
35 | element("d", JsonObject.serializer().descriptor, isOptional = true)
36 | }
37 |
38 | @ExperimentalSerializationApi
39 | override fun deserialize(decoder: Decoder): Event? {
40 | var op: Op? = null
41 | var data: Event? = null
42 |
43 | with(decoder.beginStructure(descriptor)) {
44 | loop@ while (true) {
45 | val idx = decodeElementIndex(descriptor)
46 | fun decode(serializer: DeserializationStrategy) =
47 | decodeSerializableElement(Operation.descriptor, idx, serializer)
48 |
49 | when (idx) {
50 | CompositeDecoder.DECODE_DONE -> break@loop
51 |
52 | 0 ->
53 | op = Op.deserialize(decoder)
54 |
55 | 1 -> data =
56 | when (op) {
57 | Op.Hello ->
58 | decode(Hello.serializer())
59 |
60 | Op.Ready ->
61 | decode(Ready.serializer())
62 |
63 | Op.HeartbeatAck ->
64 | decode(HeartbeatAck.serializer())
65 |
66 | Op.SessionDescription ->
67 | decode(SessionDescription.serializer())
68 |
69 | Op.ClientConnect ->
70 | decode(ClientConnect.serializer())
71 |
72 | else -> {
73 | decodeNullableSerializableElement(Operation.descriptor, idx, JsonElement.serializer().nullable)
74 | data
75 | }
76 | }
77 | }
78 | }
79 |
80 | endStructure(descriptor)
81 | return data
82 | }
83 | }
84 | }
85 | }
86 |
87 | @Serializable
88 | data class Hello(
89 | @SerialName("heartbeat_interval")
90 | val heartbeatInterval: Double
91 | ) : Event()
92 |
93 | @Serializable
94 | data class Ready(
95 | val ssrc: Int,
96 | val ip: String,
97 | val port: Int,
98 | val modes: List
99 | ) : Event()
100 |
101 | @Serializable
102 | data class HeartbeatAck(val nonce: Long) : Event() {
103 | companion object : DeserializationStrategy {
104 | override val descriptor: SerialDescriptor
105 | get() = PrimitiveSerialDescriptor("HeartbeatAck", PrimitiveKind.LONG)
106 |
107 | override fun deserialize(decoder: Decoder): HeartbeatAck =
108 | HeartbeatAck(decoder.decodeLong())
109 | }
110 | }
111 |
112 | @Serializable
113 | data class SessionDescription(
114 | val mode: String,
115 | @SerialName("audio_codec")
116 | val audioCodec: String,
117 | @SerialName("secret_key")
118 | val secretKey: List
119 | ) : Event()
120 |
121 | @Serializable
122 | data class ClientConnect(
123 | @SerialName("user_id")
124 | val userId: String,
125 | @SerialName("audio_ssrc")
126 | val audioSsrc: Int = 0,
127 | @SerialName("video_ssrc")
128 | val videoSsrc: Int = 0,
129 | @SerialName("rtx_ssrc")
130 | val rtxSsrc: Int = 0,
131 | ) : Event()
132 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Op.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.gateway.event
18 |
19 | import kotlinx.serialization.KSerializer
20 | import kotlinx.serialization.descriptors.PrimitiveKind
21 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
22 | import kotlinx.serialization.descriptors.SerialDescriptor
23 | import kotlinx.serialization.encoding.Decoder
24 | import kotlinx.serialization.encoding.Encoder
25 |
26 | enum class Op(val code: Int) {
27 | Unknown(Int.MIN_VALUE),
28 |
29 | Identify(0),
30 | SelectProtocol(1),
31 | Ready(2),
32 | Heartbeat(3),
33 | SessionDescription(4),
34 | Speaking(5),
35 | HeartbeatAck(6),
36 | Hello(8),
37 | ClientConnect(12);
38 |
39 | companion object Serializer : KSerializer {
40 | /**
41 | * Finds the Op for the provided [code]
42 | *
43 | * @param code The operation code.
44 | */
45 | operator fun get(code: Int): Op? =
46 | values().find { it.code == code }
47 |
48 | override val descriptor: SerialDescriptor
49 | get() = PrimitiveSerialDescriptor("op", PrimitiveKind.INT)
50 |
51 | override fun deserialize(decoder: Decoder): Op =
52 | this[decoder.decodeInt()] ?: Unknown
53 |
54 | override fun serialize(encoder: Encoder, value: Op) =
55 | encoder.encodeInt(value.code)
56 | }
57 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/handler/ConnectionHandler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.handler
18 |
19 | import io.ktor.util.network.*
20 | import io.netty.buffer.ByteBuf
21 | import obsidian.bedrock.gateway.event.SessionDescription
22 | import java.io.Closeable
23 |
24 | /**
25 | * This interface specifies Discord voice connection handler, allowing to implement other methods of establishing voice
26 | * connections/transmitting audio packets eg. TCP or browser/WebRTC way via ICE instead of their minimalistic custom
27 | * discovery protocol.
28 | */
29 | interface ConnectionHandler : Closeable {
30 |
31 | /**
32 | * Handles a session description
33 | *
34 | * @param data The session description data.
35 | */
36 | suspend fun handleSessionDescription(data: SessionDescription)
37 |
38 | /**
39 | * Connects to the Discord UDP Socket.
40 | *
41 | * @return Our external network address.
42 | */
43 | suspend fun connect(): NetworkAddress
44 |
45 | suspend fun sendFrame(payloadType: Byte, timestamp: Int, data: ByteBuf, start: Int, extension: Boolean)
46 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/handler/DiscordUDPConnection.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.handler
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.channel.ChannelInitializer
21 | import io.netty.channel.socket.DatagramChannel
22 | import io.netty.util.internal.ThreadLocalRandom
23 | import kotlinx.coroutines.future.await
24 | import obsidian.bedrock.Bedrock
25 | import obsidian.bedrock.MediaConnection
26 | import obsidian.bedrock.codec.Codec
27 | import obsidian.bedrock.crypto.EncryptionMode
28 | import obsidian.bedrock.gateway.event.SessionDescription
29 | import obsidian.bedrock.util.NettyBootstrapFactory
30 | import obsidian.bedrock.util.writeV2
31 | import org.slf4j.Logger
32 | import org.slf4j.LoggerFactory
33 | import java.io.Closeable
34 | import java.net.InetSocketAddress
35 | import java.net.SocketAddress
36 | import java.util.concurrent.CompletableFuture
37 |
38 | class DiscordUDPConnection(
39 | private val connection: MediaConnection,
40 | val serverAddress: SocketAddress,
41 | val ssrc: Int
42 | ) : Closeable, ConnectionHandler {
43 |
44 | private var allocator = Bedrock.byteBufAllocator
45 | private var bootstrap = NettyBootstrapFactory.createDatagram()
46 |
47 | private var encryptionMode: EncryptionMode? = null
48 | private var channel: DatagramChannel? = null
49 | private var secretKey: ByteArray? = null
50 |
51 | private var seq = ThreadLocalRandom.current().nextInt() and 0xffff
52 |
53 | override suspend fun connect(): InetSocketAddress {
54 | logger.debug("Connecting to '$serverAddress'...")
55 |
56 | val future = CompletableFuture()
57 | bootstrap.handler(Initializer(this, future))
58 | .connect(serverAddress)
59 | .addListener { res ->
60 | if (!res.isSuccess) {
61 | future.completeExceptionally(res.cause())
62 | }
63 | }
64 |
65 | return future.await()
66 | }
67 |
68 | override fun close() {
69 | if (channel != null && channel!!.isOpen) {
70 | channel?.close()
71 | }
72 | }
73 |
74 | override suspend fun handleSessionDescription(data: SessionDescription) {
75 | encryptionMode = EncryptionMode[data.mode]
76 |
77 | val audioCodec = Codec.getAudio(data.audioCodec)
78 | if (audioCodec == null) {
79 | logger.warn("Unsupported audio codec type: {}, no audio data will be polled", data.audioCodec)
80 | }
81 |
82 | checkNotNull(encryptionMode) {
83 | "Encryption mode selected by Discord is not supported by Bedrock or the " +
84 | "protocol changed! Open an issue!"
85 | }
86 |
87 | secretKey = ByteArray(data.secretKey.size) { idx ->
88 | (data.secretKey[idx] and 0xff).toByte()
89 | }
90 |
91 | connection.startFramePolling()
92 | }
93 |
94 | override suspend fun sendFrame(payloadType: Byte, timestamp: Int, data: ByteBuf, start: Int, extension: Boolean) {
95 | val buf = createPacket(payloadType, timestamp, data, start, extension)
96 | if (buf != null) {
97 | channel?.writeAndFlush(buf)
98 | }
99 | }
100 |
101 | fun createPacket(payloadType: Byte, timestamp: Int, data: ByteBuf, len: Int, extension: Boolean): ByteBuf? {
102 | if (secretKey == null) {
103 | return null
104 | }
105 |
106 | val buf = allocator.buffer()
107 | buf.clear()
108 |
109 | writeV2(buf, payloadType, nextSeq(), timestamp, ssrc, extension)
110 |
111 | if (encryptionMode!!.box(data, len, buf, secretKey!!)) {
112 | return buf
113 | } else {
114 | logger.debug("Encryption failed!")
115 | buf.release()
116 | }
117 |
118 | return null
119 | }
120 |
121 | private fun nextSeq(): Int {
122 | if (seq + 1 > 0xffff) {
123 | seq = 0
124 | } else {
125 | seq++
126 | }
127 |
128 | return seq
129 | }
130 |
131 | inner class Initializer constructor(
132 | private val connection: DiscordUDPConnection,
133 | private val future: CompletableFuture
134 | ) : ChannelInitializer() {
135 | override fun initChannel(datagramChannel: DatagramChannel) {
136 | connection.channel = datagramChannel
137 |
138 | val handler = HolepunchHandler(future, connection.ssrc)
139 | datagramChannel.pipeline().addFirst("handler", handler)
140 | }
141 | }
142 |
143 | companion object {
144 | private val logger: Logger = LoggerFactory.getLogger(DiscordUDPConnection::class.java)
145 | }
146 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/handler/HolepunchHandler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.handler
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 | import io.netty.channel.ChannelHandlerContext
22 | import io.netty.channel.SimpleChannelInboundHandler
23 | import io.netty.channel.socket.DatagramPacket
24 | import org.slf4j.LoggerFactory
25 | import java.net.InetSocketAddress
26 | import java.net.SocketTimeoutException
27 | import java.util.concurrent.CompletableFuture
28 | import java.util.concurrent.TimeUnit
29 |
30 | class HolepunchHandler(
31 | private val future: CompletableFuture?,
32 | private val ssrc: Int = 0
33 | ) : SimpleChannelInboundHandler() {
34 |
35 | private var tries = 0
36 | private var packet: DatagramPacket? = null
37 |
38 | override fun channelActive(ctx: ChannelHandlerContext) {
39 | holepunch(ctx)
40 | }
41 |
42 | override fun channelRead0(ctx: ChannelHandlerContext, packet: DatagramPacket) {
43 | val buf: ByteBuf = packet.content()
44 | if (!future!!.isDone) {
45 | if (buf.readableBytes() != 74) return
46 |
47 | buf.skipBytes(8)
48 |
49 | val stringBuilder = StringBuilder()
50 | var b: Byte
51 | while (buf.readByte().also { b = it }.toInt() != 0) {
52 | stringBuilder.append(b.toChar())
53 | }
54 |
55 | val ip = stringBuilder.toString()
56 | val port: Int = buf.getUnsignedShort(72)
57 |
58 | ctx.pipeline().remove(this)
59 | future.complete(InetSocketAddress(ip, port))
60 | }
61 | }
62 |
63 | fun holepunch(ctx: ChannelHandlerContext) {
64 | if (future!!.isDone) {
65 | return
66 | }
67 |
68 | if (tries++ > 10) {
69 | logger.debug("Discovery failed.")
70 | future.completeExceptionally(SocketTimeoutException("Failed to discover external UDP address."))
71 | return
72 | }
73 |
74 | logger.debug("Holepunch [attempt {}/10, local ip: {}]", tries, ctx.channel().localAddress())
75 | if (packet == null) {
76 | val buf = Unpooled.buffer(74)
77 | buf.writeShort(1)
78 | buf.writeShort(0x46)
79 | buf.writeInt(ssrc)
80 | buf.writerIndex(74)
81 | packet = DatagramPacket(buf, ctx.channel().remoteAddress() as InetSocketAddress)
82 | }
83 |
84 | packet!!.retain()
85 | ctx.writeAndFlush(packet)
86 | ctx.executor().schedule({ holepunch(ctx) }, 1, TimeUnit.SECONDS)
87 | }
88 |
89 | companion object {
90 | private val logger = LoggerFactory.getLogger(HolepunchHandler::class.java)
91 | }
92 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/media/IntReference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.media
18 |
19 | /**
20 | * Mutable reference to an int value. Provides no atomicity guarantees
21 | * and should not be shared between threads without external synchronization.
22 | */
23 | class IntReference {
24 | private var value = 0
25 |
26 | fun get(): Int {
27 | return value
28 | }
29 |
30 | fun set(value: Int) {
31 | this.value = value
32 | }
33 |
34 | fun add(amount: Int) {
35 | value += amount
36 | }
37 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/media/MediaFrameProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.media
18 |
19 | import io.netty.buffer.ByteBuf
20 | import obsidian.bedrock.codec.Codec
21 |
22 | /**
23 | * Base interface for media frame providers. Note that Bedrock doesn't handle stuff such as speaking state, silent frames
24 | * or etc., these are implemented by codec-specific frame provider classes.
25 | *
26 | * @see OpusAudioFrameProvider for Opus audio codec specific implementation that handles speaking state and etc.
27 | */
28 | interface MediaFrameProvider {
29 | /**
30 | * Frame interval between polling attempts or sets the delay between polling attempts.
31 | */
32 | var frameInterval: Int
33 |
34 | /**
35 | * Called when this [MediaFrameProvider] should clean up it's event handlers and etc.
36 | */
37 | fun dispose()
38 |
39 | /**
40 | * @return If true, Bedrock will request media data for given [Codec] by calling [retrieve] method.
41 | */
42 | fun canSendFrame(codec: Codec): Boolean
43 |
44 | /**
45 | * If [canSendFrame] returns true, Bedrock will attempt to retrieve an media frame encoded with specified [Codec] type, by calling this method with target [ByteBuf] where the data should be written to.
46 | * Do not call [ByteBuf.release] - memory management is already handled by Bedrock itself. In case if no data gets written to the buffer, audio packet won't be sent.
47 | *
48 | * Do not let this method block - all data should be queued on another thread or pre-loaded in memory - otherwise it will very likely have significant impact on application performance.
49 | *
50 | * @param codec [Codec] type this handler was registered with.
51 | * @param buf [ByteBuf] the buffer where the media data should be written to.
52 | * @param timestamp [IntReference] reference to current frame timestamp, which must be updated with timestamp of written frame.
53 | *
54 | * @return If true, Bedrock will immediately attempt to poll a next frame, this is meant for video transmissions.
55 | */
56 | suspend fun retrieve(codec: Codec?, buf: ByteBuf?, timestamp: IntReference?): Boolean
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/media/OpusAudioFrameProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.media
18 |
19 | import io.netty.buffer.ByteBuf
20 | import obsidian.bedrock.MediaConnection
21 | import obsidian.bedrock.codec.Codec
22 | import obsidian.bedrock.codec.OpusCodec
23 | import obsidian.bedrock.UserConnectedEvent
24 | import obsidian.bedrock.gateway.SpeakingFlags
25 | import obsidian.bedrock.on
26 |
27 | abstract class OpusAudioFrameProvider(val connection: MediaConnection) : MediaFrameProvider {
28 | override var frameInterval = OpusCodec.FRAME_DURATION
29 |
30 | private var speakingMask = SpeakingFlags.NORMAL
31 | private var counter = 0
32 | private var lastProvide = false
33 | private var lastSpeaking = false
34 | private var lastFramePolled: Long = 0
35 | private var speaking = false
36 |
37 | private val userConnectedJob = connection.on {
38 | if (speaking) {
39 | connection.updateSpeakingState(speakingMask)
40 | }
41 | }
42 |
43 | override fun dispose() {
44 | userConnectedJob.cancel()
45 | }
46 |
47 | override fun canSendFrame(codec: Codec): Boolean {
48 | if (codec.payloadType != OpusCodec.PAYLOAD_TYPE) {
49 | return false
50 | }
51 |
52 | if (counter > 0) {
53 | return true
54 | }
55 |
56 | val provide = canProvide()
57 | if (lastProvide != provide) {
58 | lastProvide = provide;
59 | if (!provide) {
60 | counter = SILENCE_FRAME_COUNT;
61 | return true;
62 | }
63 | }
64 |
65 | return provide;
66 | }
67 |
68 | override suspend fun retrieve(codec: Codec?, buf: ByteBuf?, timestamp: IntReference?): Boolean {
69 | if (codec?.payloadType != OpusCodec.PAYLOAD_TYPE) {
70 | return false
71 | }
72 |
73 | if (counter > 0) {
74 | counter--
75 | buf!!.writeBytes(OpusCodec.SILENCE_FRAME)
76 | if (speaking) {
77 | setSpeaking(false)
78 | }
79 |
80 | timestamp!!.add(960)
81 | return false
82 | }
83 |
84 | val startIndex = buf!!.writerIndex()
85 | retrieveOpusFrame(buf)
86 |
87 | val written = buf.writerIndex() != startIndex
88 | if (written && !speaking) {
89 | setSpeaking(true)
90 | }
91 |
92 | if (!written) {
93 | counter = SILENCE_FRAME_COUNT
94 | }
95 |
96 | val now = System.currentTimeMillis()
97 | val changeTalking = now - lastFramePolled > OpusCodec.FRAME_DURATION
98 |
99 | lastFramePolled = now
100 | if (changeTalking) {
101 | setSpeaking(written)
102 | }
103 |
104 | timestamp!!.add(960)
105 | return false
106 | }
107 |
108 | private suspend fun setSpeaking(state: Boolean) {
109 | speaking = state
110 | if (speaking != lastSpeaking) {
111 | lastSpeaking = state
112 | connection.updateSpeakingState(if (state) speakingMask else 0)
113 | }
114 | }
115 |
116 |
117 | /**
118 | * Called every time Opus frame poller tries to retrieve an Opus audio frame.
119 | *
120 | * @return If this method returns true, Bedrock will attempt to retrieve an Opus audio frame.
121 | */
122 | abstract fun canProvide(): Boolean
123 |
124 | /**
125 | * If [canProvide] returns true, this method will attempt to retrieve an Opus audio frame.
126 | *
127 | *
128 | * This method must not block, otherwise it might cause severe performance issues, due to event loop thread
129 | * getting blocked, therefore it's recommended to load all data before or in parallel, not when Bedrock frame poller
130 | * calls this method. If no data gets written, the frame won't be sent.
131 | *
132 | * @param targetBuffer the target [ByteBuf] audio data should be written to.
133 | */
134 | abstract fun retrieveOpusFrame(targetBuffer: ByteBuf)
135 |
136 | companion object {
137 | private const val SILENCE_FRAME_COUNT = 5
138 | }
139 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/util/Interval.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.util
18 |
19 | import kotlinx.coroutines.*
20 | import kotlinx.coroutines.channels.ReceiveChannel
21 | import kotlinx.coroutines.channels.consumeEach
22 | import kotlinx.coroutines.channels.ticker
23 | import kotlinx.coroutines.sync.Mutex
24 | import kotlinx.coroutines.sync.withLock
25 | import org.slf4j.Logger
26 | import org.slf4j.LoggerFactory
27 | import kotlin.coroutines.CoroutineContext
28 |
29 | /**
30 | * A reusable fixed rate interval.
31 | *
32 | * @param dispatcher The dispatchers the events will be fired on.
33 | */
34 | @ObsoleteCoroutinesApi
35 | class Interval(private val dispatcher: CoroutineDispatcher = Dispatchers.Default) : CoroutineScope {
36 | /**
37 | * The coroutine context.
38 | */
39 | override val coroutineContext: CoroutineContext
40 | get() = dispatcher + Job()
41 |
42 | /**
43 | * Whether this interval has been started.
44 | */
45 | var started: Boolean = false
46 | private set
47 |
48 | /**
49 | * The mutex.
50 | */
51 | private val mutex = Mutex()
52 |
53 | /**
54 | * The kotlin ticker.
55 | */
56 | private var ticker: ReceiveChannel? = null
57 |
58 | /**
59 | * Executes the provided [block] every [delay] milliseconds.
60 | *
61 | * @param delay The delay (in milliseconds) between every execution
62 | * @param block The block to execute.
63 | */
64 | suspend fun start(delay: Long, block: suspend () -> Unit) {
65 | coroutineScope {
66 | stop()
67 | mutex.withLock {
68 | ticker = ticker(delay)
69 |
70 | launch {
71 | started = true
72 |
73 | ticker?.consumeEach {
74 | try {
75 | block()
76 | } catch (exception: Exception) {
77 | logger.error("Ran into an exception.", exception)
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
85 | /**
86 | * Stops the this interval.
87 | */
88 | suspend fun stop() {
89 | mutex.withLock {
90 | ticker?.cancel()
91 | started = false
92 | }
93 | }
94 |
95 | companion object {
96 | private val logger: Logger = LoggerFactory.getLogger(Interval::class.java)
97 | }
98 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/util/NettyBootstrapFactory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.util
18 |
19 | import io.netty.bootstrap.Bootstrap
20 | import io.netty.channel.ChannelOption
21 | import obsidian.bedrock.Bedrock
22 |
23 | object NettyBootstrapFactory {
24 | /**
25 | * Creates a Datagram [Bootstrap]
26 | */
27 | fun createDatagram(): Bootstrap {
28 | val bootstrap = Bootstrap()
29 | .group(Bedrock.eventLoopGroup)
30 | .channel(Bedrock.datagramChannelClass)
31 | .option(ChannelOption.SO_REUSEADDR, true)
32 |
33 | if (Bedrock.highPacketPriority) {
34 | // IPTOS_LOWDELAY | IPTOS_THROUGHPUT
35 | bootstrap.option(ChannelOption.IP_TOS, 0x10 or 0x08)
36 | }
37 |
38 | return bootstrap
39 | }
40 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/bedrock/util/RTPHeaderWriter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.bedrock.util
18 |
19 | import io.netty.buffer.ByteBuf
20 | import kotlin.experimental.and
21 |
22 | fun writeV2(output: ByteBuf, payloadType: Byte, seq: Int, timestamp: Int, ssrc: Int, extension: Boolean) {
23 | output.writeByte(if (extension) 0x90 else 0x80)
24 | output.writeByte(payloadType.and(0x7f).toInt())
25 | output.writeChar(seq)
26 | output.writeInt(timestamp)
27 | output.writeInt(ssrc)
28 | }
29 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/server/Obsidian.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.server
18 |
19 | import ch.qos.logback.classic.Level
20 | import ch.qos.logback.classic.Logger
21 | import ch.qos.logback.classic.LoggerContext
22 | import com.github.natanbc.nativeloader.NativeLibLoader
23 | import com.github.natanbc.nativeloader.SystemNativeLibraryProperties
24 | import com.github.natanbc.nativeloader.system.SystemType
25 | import com.uchuhimo.konf.Config
26 | import com.uchuhimo.konf.source.yaml
27 | import io.ktor.application.*
28 | import io.ktor.auth.*
29 | import io.ktor.features.*
30 | import io.ktor.http.*
31 | import io.ktor.locations.*
32 | import io.ktor.request.*
33 | import io.ktor.response.*
34 | import io.ktor.routing.*
35 | import io.ktor.serialization.*
36 | import io.ktor.server.cio.*
37 | import io.ktor.server.engine.*
38 | import io.ktor.websocket.*
39 | import kotlinx.coroutines.runBlocking
40 | import obsidian.bedrock.Bedrock
41 | import obsidian.server.io.Magma.Companion.magma
42 | import obsidian.server.player.ObsidianPlayerManager
43 | import obsidian.server.util.NativeUtil
44 | import obsidian.server.util.config.LoggingConfig
45 | import obsidian.server.util.config.ObsidianConfig
46 | import org.slf4j.LoggerFactory
47 | import kotlin.system.exitProcess
48 |
49 | object Obsidian {
50 | /**
51 | * Configuration
52 | */
53 | val config = Config {
54 | addSpec(ObsidianConfig)
55 | addSpec(Bedrock.Config)
56 | addSpec(LoggingConfig)
57 | }
58 | .from.yaml.file("obsidian.yml", true)
59 | .from.env()
60 | .from.systemProperties()
61 |
62 | /**
63 | * Player manager
64 | */
65 | val playerManager = ObsidianPlayerManager()
66 |
67 | /**
68 | * Lol i just like comments
69 | */
70 | private val logger = LoggerFactory.getLogger(Obsidian::class.java)
71 |
72 | @JvmStatic
73 | fun main(args: Array) {
74 | runBlocking {
75 | /* setup logging */
76 | configureLogging()
77 |
78 | /* native library loading lololol */
79 | try {
80 | val type = SystemType.detect(SystemNativeLibraryProperties(null, "nativeloader."))
81 |
82 | logger.info("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}")
83 | logger.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}")
84 | } catch (e: Exception) {
85 | val message =
86 | "Unable to load system info" + if (e is UnsatisfiedLinkError || e is RuntimeException && e.cause is UnsatisfiedLinkError)
87 | ", this isn't an error" else "."
88 |
89 | logger.warn(message, e)
90 | }
91 |
92 | try {
93 | logger.info("Loading Native Libraries")
94 | NativeUtil.load()
95 | } catch (ex: Exception) {
96 | logger.error("Fatal exception while loading native libraries.", ex)
97 | exitProcess(1)
98 | }
99 |
100 | /* setup server */
101 | val server = embeddedServer(CIO, host = config[ObsidianConfig.Host], port = config[ObsidianConfig.Port]) {
102 | install(Locations)
103 |
104 | install(WebSockets)
105 |
106 | install(ContentNegotiation) {
107 | json()
108 | }
109 |
110 | install(Authentication) {
111 | provider {
112 | pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
113 | val authorization = call.request.authorization()
114 | if (!ObsidianConfig.validateAuth(authorization)) {
115 | val cause =
116 | if (authorization == null) AuthenticationFailedCause.NoCredentials
117 | else AuthenticationFailedCause.InvalidCredentials
118 |
119 | context.challenge("ObsidianAuth", cause) {
120 | call.respond(HttpStatusCode.Unauthorized)
121 | it.complete()
122 | }
123 | }
124 | }
125 | }
126 | }
127 |
128 | routing {
129 | magma.use(this)
130 | }
131 | }
132 |
133 | if (config[ObsidianConfig.Password].isEmpty()) {
134 | logger.warn("No password has been configured, thus allowing no authorization for the websocket server and REST requests.")
135 | }
136 |
137 | server.start(wait = true)
138 | magma.shutdown()
139 | }
140 | }
141 |
142 | private fun configureLogging() {
143 | val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext
144 |
145 | val rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME) as Logger
146 | rootLogger.level = Level.toLevel(config[LoggingConfig.Level.Root], Level.INFO)
147 |
148 | val obsidianLogger = loggerContext.getLogger("obsidian") as Logger
149 | obsidianLogger.level = Level.toLevel(config[LoggingConfig.Level.Obsidian], Level.INFO)
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/server/io/Magma.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.server.io
18 |
19 | import io.ktor.auth.*
20 | import io.ktor.features.*
21 | import io.ktor.http.*
22 | import io.ktor.http.cio.websocket.*
23 | import io.ktor.locations.*
24 | import io.ktor.request.*
25 | import io.ktor.response.*
26 | import io.ktor.routing.*
27 | import io.ktor.util.*
28 | import io.ktor.util.pipeline.*
29 | import io.ktor.websocket.*
30 | import obsidian.server.Obsidian.config
31 | import obsidian.server.io.MagmaCloseReason.CLIENT_EXISTS
32 | import obsidian.server.io.MagmaCloseReason.INVALID_AUTHORIZATION
33 | import obsidian.server.io.MagmaCloseReason.MISSING_CLIENT_NAME
34 | import obsidian.server.io.MagmaCloseReason.NO_USER_ID
35 | import obsidian.server.io.controllers.routePlanner
36 | import obsidian.server.io.controllers.tracks
37 | import obsidian.server.util.config.ObsidianConfig
38 | import obsidian.server.util.threadFactory
39 | import org.slf4j.LoggerFactory
40 | import java.util.concurrent.ConcurrentHashMap
41 | import java.util.concurrent.Executors
42 | import java.util.concurrent.ScheduledExecutorService
43 |
44 | class Magma private constructor() {
45 | /**
46 | * Executor
47 | */
48 | val executor: ScheduledExecutorService =
49 | Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Cleanup", daemon = true))
50 |
51 | /**
52 | * All connected clients.
53 | * `Client ID -> MagmaClient`
54 | */
55 | private val clients = ConcurrentHashMap()
56 |
57 | fun use(routing: Routing) {
58 | routing {
59 | webSocket("/") {
60 | val request = call.request
61 |
62 | /* Used within logs to easily identify different clients. */
63 | val clientName = request.headers["Client-Name"]
64 | ?: request.queryParameters["client-name"]
65 |
66 | /* check if client names are required, if so check if one is provided. */
67 | if (config[ObsidianConfig.RequireClientName] && clientName.isNullOrBlank()) {
68 | logger.warn("${request.local.remoteHost} - missing 'Client-Name' header")
69 | return@webSocket close(MISSING_CLIENT_NAME)
70 | }
71 |
72 | val identification = "${request.local.remoteHost}${if (!clientName.isNullOrEmpty()) "($clientName)" else ""}"
73 |
74 | /* validate authorization. */
75 | val auth = request.authorization()
76 | ?: request.queryParameters["auth"]
77 |
78 | if (!ObsidianConfig.validateAuth(auth)) {
79 | logger.warn("$identification - authentication failed")
80 | return@webSocket close(INVALID_AUTHORIZATION)
81 | }
82 |
83 | logger.info("$identification - incoming connection")
84 |
85 | /* check for userId */
86 | val userId = request.headers["User-Id"]?.toLongOrNull()
87 | ?: request.queryParameters["user-id"]?.toLongOrNull()
88 |
89 | if (userId == null) {
90 | /* no user id was given, close the connection */
91 | logger.info("$identification - missing 'User-Id' header")
92 | return@webSocket close(NO_USER_ID)
93 | }
94 |
95 | /* check if a client for the provided userId already exists. */
96 | var client = clients[userId]
97 | if (client != null) {
98 | /* check for a resume key, if one was given check if the client has the same resume key/ */
99 | val resumeKey: String? = request.headers["Resume-Key"]
100 | if (resumeKey != null && client.resumeKey == resumeKey) {
101 | /* resume the client session */
102 | client.resume(this)
103 | return@webSocket
104 | }
105 |
106 | return@webSocket close(CLIENT_EXISTS)
107 | }
108 |
109 | /* create client */
110 | client = MagmaClient(clientName, userId, this)
111 | clients[userId] = client
112 |
113 | /* listen for incoming messages */
114 | try {
115 | client.listen()
116 | } catch (ex: Throwable) {
117 | logger.error("${client.identification} -", ex)
118 | close(CloseReason(4005, ex.message ?: "unknown exception"))
119 | }
120 |
121 | client.handleClose()
122 | }
123 |
124 | authenticate {
125 | get("/stats") {
126 | context.respond(StatsBuilder.build())
127 | }
128 | }
129 | }
130 |
131 | routing.tracks()
132 | routing.routePlanner()
133 | }
134 |
135 | suspend fun shutdown(client: MagmaClient) {
136 | client.shutdown()
137 | clients.remove(client.clientId)
138 | }
139 |
140 | suspend fun shutdown() {
141 | if (clients.isNotEmpty()) {
142 | logger.info("Shutting down ${clients.size} clients.")
143 | for ((_, client) in clients) {
144 | client.shutdown()
145 | }
146 | } else {
147 | logger.info("No clients to shutdown.")
148 | }
149 | }
150 |
151 | companion object {
152 | val magma: Magma by lazy { Magma() }
153 | private val logger = LoggerFactory.getLogger(Magma::class.java)
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/server/io/MagmaCloseReason.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.server.io
18 |
19 | import io.ktor.http.cio.websocket.CloseReason
20 |
21 | object MagmaCloseReason {
22 | val INVALID_AUTHORIZATION = CloseReason(4001, "Invalid Authorization")
23 | val NO_USER_ID = CloseReason(4002, "No user id provided.")
24 | val CLIENT_EXISTS = CloseReason(4004, "A client for the provided user already exists.")
25 | val MISSING_CLIENT_NAME = CloseReason(4006, "This server requires the 'Client-Name' to be present.")
26 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/server/io/Op.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.server.io
18 |
19 | import kotlinx.serialization.KSerializer
20 | import kotlinx.serialization.Serializable
21 | import kotlinx.serialization.descriptors.PrimitiveKind
22 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
23 | import kotlinx.serialization.descriptors.SerialDescriptor
24 | import kotlinx.serialization.encoding.Decoder
25 | import kotlinx.serialization.encoding.Encoder
26 |
27 | @Serializable(with = Op.Serializer::class)
28 | enum class Op(val code: Int) {
29 | Unknown(Int.MIN_VALUE),
30 | SubmitVoiceUpdate(0),
31 |
32 | // obsidian related.
33 | Stats(1),
34 | SetupResuming(10),
35 | SetupDispatchBuffer(11),
36 |
37 | // player information.
38 | PlayerEvent(2),
39 | PlayerUpdate(3),
40 |
41 | // player control.
42 | PlayTrack(4),
43 | StopTrack(5),
44 | Pause(6),
45 | Filters(7),
46 | Seek(8),
47 | Destroy(9),
48 | Configure(12);
49 |
50 | companion object Serializer : KSerializer {
51 | /**
52 | * Finds the Op for the provided [code]
53 | *
54 | * @param code The operation code.
55 | */
56 | operator fun get(code: Int): Op? =
57 | values().firstOrNull { it.code == code }
58 |
59 | override val descriptor: SerialDescriptor
60 | get() = PrimitiveSerialDescriptor("op", PrimitiveKind.INT)
61 |
62 | override fun deserialize(decoder: Decoder): Op =
63 | this[decoder.decodeInt()] ?: Unknown
64 |
65 | override fun serialize(encoder: Encoder, value: Op) =
66 | encoder.encodeInt(value.code)
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/server/io/Operation.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.server.io
18 |
19 | import kotlinx.serialization.*
20 | import kotlinx.serialization.builtins.LongAsStringSerializer
21 | import kotlinx.serialization.builtins.nullable
22 | import kotlinx.serialization.descriptors.SerialDescriptor
23 | import kotlinx.serialization.descriptors.buildClassSerialDescriptor
24 | import kotlinx.serialization.encoding.CompositeDecoder
25 | import kotlinx.serialization.encoding.Decoder
26 | import kotlinx.serialization.json.JsonElement
27 | import kotlinx.serialization.json.JsonObject
28 | import obsidian.server.player.filter.impl.*
29 |
30 | sealed class Operation {
31 | companion object : DeserializationStrategy {
32 | override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Operation") {
33 | element("op", Op.descriptor)
34 | element("d", JsonObject.serializer().descriptor, isOptional = true)
35 | }
36 |
37 | @ExperimentalSerializationApi
38 | override fun deserialize(decoder: Decoder): Operation? {
39 | var op: Op? = null
40 | var data: Operation? = null
41 |
42 | with(decoder.beginStructure(descriptor)) {
43 | loop@ while (true) {
44 | val idx = decodeElementIndex(descriptor)
45 | fun decode(serializer: DeserializationStrategy) =
46 | decodeSerializableElement(descriptor, idx, serializer)
47 |
48 | when (idx) {
49 | CompositeDecoder.DECODE_DONE -> break@loop
50 |
51 | 0 ->
52 | op = Op.deserialize(decoder)
53 |
54 | 1 ->
55 | data = when (op) {
56 | Op.SubmitVoiceUpdate ->
57 | decode(SubmitVoiceUpdate.serializer())
58 |
59 | Op.PlayTrack ->
60 | decode(PlayTrack.serializer())
61 |
62 | Op.StopTrack ->
63 | decode(StopTrack.serializer())
64 |
65 | Op.Pause ->
66 | decode(Pause.serializer())
67 |
68 | Op.Filters ->
69 | decode(Filters.serializer())
70 |
71 | Op.Seek ->
72 | decode(Seek.serializer())
73 |
74 | Op.Destroy ->
75 | decode(Destroy.serializer())
76 |
77 | Op.SetupResuming ->
78 | decode(SetupResuming.serializer())
79 |
80 | Op.SetupDispatchBuffer ->
81 | decode(SetupDispatchBuffer.serializer())
82 |
83 | Op.Configure ->
84 | decode(Configure.serializer())
85 |
86 | else -> if (data == null) {
87 | val element = decodeNullableSerializableElement(descriptor, idx, JsonElement.serializer().nullable)
88 | error("Unknown 'd' field for operation ${op?.name}: $element")
89 | } else {
90 | decodeNullableSerializableElement(descriptor, idx, JsonElement.serializer().nullable)
91 | data
92 | }
93 | }
94 | }
95 | }
96 |
97 | endStructure(descriptor)
98 | return data
99 | }
100 | }
101 | }
102 | }
103 |
104 | @Serializable
105 | data class PlayTrack(
106 | val track: String,
107 |
108 | @SerialName("guild_id")
109 | val guildId: Long,
110 |
111 | @SerialName("no_replace")
112 | val noReplace: Boolean = false,
113 |
114 | @SerialName("start_time")
115 | val startTime: Long = 0,
116 |
117 | @SerialName("end_time")
118 | val endTime: Long = 0
119 | ) : Operation()
120 |
121 | @Serializable
122 | data class StopTrack(
123 | @SerialName("guild_id")
124 | val guildId: Long
125 | ) : Operation()
126 |
127 | @Serializable
128 | data class SubmitVoiceUpdate(
129 | val endpoint: String,
130 | val token: String,
131 |
132 | @SerialName("guild_id")
133 | val guildId: Long,
134 |
135 | @SerialName("session_id")
136 | val sessionId: String,
137 | ) : Operation()
138 |
139 | @Serializable
140 | data class Pause(
141 | @SerialName("guild_id")
142 | val guildId: Long,
143 | val state: Boolean = true
144 | ) : Operation()
145 |
146 | @Serializable
147 | data class Filters(
148 | @SerialName("guild_id")
149 | val guildId: Long,
150 |
151 | val volume: Float? = null,
152 | val tremolo: TremoloFilter? = null,
153 | val equalizer: EqualizerFilter? = null,
154 | val timescale: TimescaleFilter? = null,
155 | val karaoke: KaraokeFilter? = null,
156 | @SerialName("channel_mix")
157 | val channelMix: ChannelMixFilter? = null,
158 | val vibrato: VibratoFilter? = null,
159 | val rotation: RotationFilter? = null,
160 | @SerialName("low_pass")
161 | val lowPass: LowPassFilter? = null
162 | ) : Operation()
163 |
164 | @Serializable
165 | data class Seek(
166 | @SerialName("guild_id")
167 | val guildId: Long,
168 | val position: Long
169 | ) : Operation()
170 |
171 | @Serializable
172 | data class Configure(
173 | @SerialName("guild_id")
174 | val guildId: Long,
175 | val pause: Boolean?,
176 | val filters: Filters?,
177 | @SerialName("send_player_updates")
178 | val sendPlayerUpdates: Boolean?
179 | ) : Operation()
180 |
181 | @Serializable
182 | data class Destroy(@SerialName("guild_id") val guildId: Long) : Operation()
183 |
184 | @Serializable
185 | data class SetupResuming(val key: String, val timeout: Long?) : Operation()
186 |
187 | @Serializable
188 | data class SetupDispatchBuffer(val timeout: Long) : Operation()
189 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.server.io
18 |
19 | import com.sedmelluq.lava.extensions.youtuberotator.planner.AbstractRoutePlanner
20 | import com.sedmelluq.lava.extensions.youtuberotator.planner.NanoIpRoutePlanner
21 | import com.sedmelluq.lava.extensions.youtuberotator.planner.RotatingIpRoutePlanner
22 | import com.sedmelluq.lava.extensions.youtuberotator.planner.RotatingNanoIpRoutePlanner
23 | import kotlinx.serialization.SerialName
24 | import kotlinx.serialization.Serializable
25 | import java.util.*
26 |
27 | object RoutePlannerUtil {
28 | /**
29 | * Detail information block for an AbstractRoutePlanner
30 | */
31 | fun getDetailBlock(planner: AbstractRoutePlanner): RoutePlannerStatus.IRoutePlannerStatus {
32 | val ipBlock = planner.ipBlock
33 | val ipBlockStatus = IpBlockStatus(ipBlock.type.simpleName, ipBlock.size.toString())
34 |
35 | val failingAddresses = planner.failingAddresses
36 | val failingAddressesStatus = failingAddresses.entries.map {
37 | FailingAddress(it.key, it.value, Date(it.value).toString())
38 | }
39 |
40 | return when (planner) {
41 | is RotatingIpRoutePlanner -> RotatingIpRoutePlannerStatus(
42 | ipBlockStatus,
43 | failingAddressesStatus,
44 | planner.rotateIndex.toString(),
45 | planner.index.toString(),
46 | planner.currentAddress.toString()
47 | )
48 |
49 | is NanoIpRoutePlanner -> NanoIpRoutePlannerStatus(
50 | ipBlockStatus,
51 | failingAddressesStatus,
52 | planner.currentAddress.toString()
53 | )
54 |
55 | is RotatingNanoIpRoutePlanner -> RotatingNanoIpRoutePlannerStatus(
56 | ipBlockStatus,
57 | failingAddressesStatus,
58 | planner.currentBlock.toString(),
59 | planner.addressIndexInBlock.toString()
60 | )
61 |
62 | else -> GenericRoutePlannerStatus(ipBlockStatus, failingAddressesStatus)
63 | }
64 | }
65 | }
66 |
67 | data class RoutePlannerStatus(
68 | val `class`: String?,
69 | val details: IRoutePlannerStatus?
70 | ) {
71 | interface IRoutePlannerStatus
72 | }
73 |
74 | @Serializable
75 | data class GenericRoutePlannerStatus(
76 | @SerialName("ip_block")
77 | val ipBlock: IpBlockStatus,
78 |
79 | @SerialName("failing_addresses")
80 | val failingAddresses: List
81 | ) : RoutePlannerStatus.IRoutePlannerStatus
82 |
83 | @Serializable
84 | data class RotatingIpRoutePlannerStatus(
85 | @SerialName("ip_block")
86 | val ipBlock: IpBlockStatus,
87 |
88 | @SerialName("failing_addresses")
89 | val failingAddresses: List,
90 |
91 | @SerialName("rotate_index")
92 | val rotateIndex: String,
93 |
94 | @SerialName("ip_index")
95 | val ipIndex: String,
96 |
97 | @SerialName("current_address")
98 | val currentAddress: String
99 | ) : RoutePlannerStatus.IRoutePlannerStatus
100 |
101 | @Serializable
102 | data class FailingAddress(
103 | @SerialName("failing_address")
104 | val failingAddress: String,
105 |
106 | @SerialName("failing_timestamp")
107 | val failingTimestamp: Long,
108 |
109 | @SerialName("failing_time")
110 | val failingTime: String
111 | ) : RoutePlannerStatus.IRoutePlannerStatus
112 |
113 | @Serializable
114 | data class NanoIpRoutePlannerStatus(
115 | @SerialName("ip_block")
116 | val ipBlock: IpBlockStatus,
117 |
118 | @SerialName("failing_addresses")
119 | val failingAddresses: List,
120 |
121 | @SerialName("current_address_index")
122 | val currentAddressIndex: String
123 | ) : RoutePlannerStatus.IRoutePlannerStatus
124 |
125 | @Serializable
126 | data class RotatingNanoIpRoutePlannerStatus(
127 | @SerialName("ip_block")
128 | val ipBlock: IpBlockStatus,
129 |
130 | @SerialName("failing_addresses")
131 | val failingAddresses: List,
132 |
133 | @SerialName("block_index")
134 | val blockIndex: String,
135 |
136 | @SerialName("current_address_index")
137 | val currentAddressIndex: String
138 | ) : RoutePlannerStatus.IRoutePlannerStatus
139 |
140 | @Serializable
141 | data class IpBlockStatus(val type: String, val size: String)
142 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/server/io/StatsBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.server.io
18 |
19 | import obsidian.server.util.CpuTimer
20 | import java.lang.management.ManagementFactory
21 |
22 | object StatsBuilder {
23 | private val cpuTimer = CpuTimer()
24 | private var OS_BEAN_CLASS: Class<*>? = null
25 |
26 | init {
27 | try {
28 | OS_BEAN_CLASS = Class.forName("com.sun.management.OperatingSystemMXBean")
29 | } catch (ex: Exception) {
30 |
31 | }
32 | }
33 |
34 | fun build(client: MagmaClient? = null): Stats {
35 |
36 | /* memory stats. */
37 | val memory = ManagementFactory.getMemoryMXBean().let { bean ->
38 | val heapUsed = bean.heapMemoryUsage.let {
39 | Stats.Memory.Usage(committed = it.committed, max = it.max, init = it.init, used = it.used)
40 | }
41 |
42 | val nonHeapUsed = bean.nonHeapMemoryUsage.let {
43 | Stats.Memory.Usage(committed = it.committed, max = it.max, init = it.init, used = it.used)
44 | }
45 |
46 | Stats.Memory(heapUsed = heapUsed, nonHeapUsed = nonHeapUsed)
47 | }
48 |
49 | /* cpu stats */
50 | val os = ManagementFactory.getOperatingSystemMXBean()
51 | val cpu = Stats.CPU(
52 | cores = os.availableProcessors,
53 | processLoad = cpuTimer.systemRecentCpuUsage,
54 | systemLoad = cpuTimer.processRecentCpuUsage
55 | )
56 |
57 | /* threads */
58 | val threads = with(ManagementFactory.getThreadMXBean()) {
59 | Stats.Threads(
60 | running = threadCount,
61 | daemon = daemonThreadCount,
62 | peak = peakThreadCount,
63 | totalStarted = totalStartedThreadCount
64 | )
65 | }
66 |
67 | /* player count */
68 | val players: Stats.Players? = client?.let {
69 | Stats.Players(
70 | active = client.links.count { (_, l) -> l.playing },
71 | total = client.links.size
72 | )
73 | }
74 |
75 | /* frames */
76 | val frames: List = client?.let {
77 | it.links.map { (_, link) ->
78 | Stats.FrameStats(
79 | usable = link.frameCounter.dataUsable,
80 | guildId = link.guildId,
81 | sent = link.frameCounter.success.sum(),
82 | lost = link.frameCounter.loss.sum(),
83 | )
84 | }
85 | } ?: emptyList()
86 |
87 | return Stats(cpu = cpu, memory = memory, threads = threads, frames = frames, players = players)
88 | }
89 | }
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/server/io/controllers/RoutePlanner.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.server.io.controllers
18 |
19 | import io.ktor.auth.*
20 | import io.ktor.http.*
21 | import io.ktor.locations.*
22 | import io.ktor.request.*
23 | import io.ktor.response.*
24 | import io.ktor.routing.*
25 | import kotlinx.serialization.Serializable
26 | import obsidian.server.Obsidian.playerManager
27 | import obsidian.server.io.RoutePlannerStatus
28 | import obsidian.server.io.RoutePlannerUtil.getDetailBlock
29 | import java.net.InetAddress
30 | import java.util.*
31 |
32 | fun Routing.routePlanner() {
33 | val routePlanner = playerManager.routePlanner
34 |
35 | route("/routeplanner") {
36 | authenticate {
37 | get("/status") {
38 | routePlanner
39 | ?: return@get context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled())
40 |
41 | /* respond with route planner status */
42 | val status = RoutePlannerStatus(
43 | playerManager::class.simpleName,
44 | getDetailBlock(playerManager.routePlanner!!)
45 | )
46 |
47 | context.respond(status)
48 | }
49 |
50 | route("/free") {
51 |
52 | post("/address") {
53 | routePlanner
54 | ?: return@post context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled())
55 |
56 | /* free address. */
57 | val body = context.receive()
58 | val address = InetAddress.getByName(body.address)
59 | routePlanner.freeAddress(address)
60 |
61 | /* respond with 204 */
62 | context.respond(HttpStatusCode.NoContent)
63 | }
64 |
65 | post("/all") {
66 | /* free all addresses. */
67 | routePlanner ?: return@post context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled())
68 | routePlanner.freeAllAddresses()
69 |
70 | /* respond with 204 */
71 | context.respond(HttpStatusCode.NoContent)
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
78 | @Serializable
79 | data class FreeAddress(val address: String)
80 |
81 | @Serializable
82 | data class RoutePlannerDisabled(val message: String = "The route planner is disabled, restrain from making requests to this endpoint.")
83 |
--------------------------------------------------------------------------------
/Server/src/main/kotlin/obsidian/server/io/controllers/Tracks.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 MixtapeBot and Contributors
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package obsidian.server.io.controllers
18 |
19 | import com.sedmelluq.discord.lavaplayer.tools.FriendlyException
20 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack
21 | import io.ktor.application.*
22 | import io.ktor.auth.*
23 | import io.ktor.locations.*
24 | import io.ktor.request.*
25 | import io.ktor.response.*
26 | import io.ktor.routing.*
27 | import kotlinx.coroutines.future.await
28 | import kotlinx.serialization.SerialName
29 | import kotlinx.serialization.Serializable
30 | import kotlinx.serialization.builtins.ListSerializer
31 | import kotlinx.serialization.json.JsonArray
32 | import kotlinx.serialization.json.JsonElement
33 | import kotlinx.serialization.json.JsonTransformingSerializer
34 | import obsidian.server.Obsidian
35 | import obsidian.server.io.search.AudioLoader
36 | import obsidian.server.io.search.LoadType
37 | import obsidian.server.util.TrackUtil
38 | import obsidian.server.util.kxs.AudioTrackSerializer
39 | import org.slf4j.Logger
40 | import org.slf4j.LoggerFactory
41 |
42 | private val logger: Logger = LoggerFactory.getLogger("TracksController")
43 |
44 | fun Routing.tracks() {
45 | authenticate {
46 | @Location("/loadtracks")
47 | data class LoadTracks(val identifier: String)
48 |
49 | get { data ->
50 | val result = AudioLoader(Obsidian.playerManager)
51 | .load(data.identifier)
52 | .await()
53 |
54 | if (result.exception != null) {
55 | logger.error("Track loading failed", result.exception)
56 | }
57 |
58 | val playlist = result.playlistName?.let {
59 | LoadTracksResponse.PlaylistInfo(name = it, selectedTrack = result.selectedTrack)
60 | }
61 |
62 | val exception = if (result.loadResultType == LoadType.LOAD_FAILED && result.exception != null) {
63 | LoadTracksResponse.Exception(
64 | message = result.exception!!.localizedMessage,
65 | severity = result.exception!!.severity
66 | )
67 | } else {
68 | null
69 | }
70 |
71 | val response = LoadTracksResponse(
72 | tracks = result.tracks.map(::getTrack),
73 | type = result.loadResultType,
74 | playlistInfo = playlist,
75 | exception = exception
76 | )
77 |
78 | context.respond(response)
79 | }
80 |
81 | @Location("/decodetrack")
82 | data class DecodeTrack(val track: String)
83 |
84 | get {
85 | val track = TrackUtil.decode(it.track)
86 | context.respond(getTrackInfo(track))
87 | }
88 |
89 | post("/decodetracks") {
90 | val body = call.receive()
91 | context.respond(body.tracks.map(::getTrackInfo))
92 | }
93 | }
94 | }
95 |
96 | private fun getTrack(audioTrack: AudioTrack): Track =
97 | Track(track = audioTrack, info = getTrackInfo(audioTrack))
98 |
99 | private fun getTrackInfo(audioTrack: AudioTrack): Track.Info =
100 | Track.Info(
101 | title = audioTrack.info.title,
102 | uri = audioTrack.info.uri,
103 | identifier = audioTrack.info.identifier,
104 | author = audioTrack.info.author,
105 | length = audioTrack.duration,
106 | isSeekable = audioTrack.isSeekable,
107 | isStream = audioTrack.info.isStream,
108 | position = audioTrack.position
109 | )
110 |
111 | @Serializable
112 | data class DecodeTracksBody(@Serializable(with = AudioTrackListSerializer::class) val tracks: List)
113 |
114 | @Serializable
115 | data class LoadTracksResponse(
116 | @SerialName("load_type")
117 | val type: LoadType,
118 | @SerialName("playlist_info")
119 | val playlistInfo: PlaylistInfo?,
120 | val tracks: List