├── .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, 121 | val exception: Exception? 122 | ) { 123 | @Serializable 124 | data class Exception( 125 | val message: String, 126 | val severity: FriendlyException.Severity 127 | ) 128 | 129 | @Serializable 130 | data class PlaylistInfo( 131 | val name: String, 132 | @SerialName("selected_track") 133 | val selectedTrack: Int? 134 | ) 135 | } 136 | 137 | @Serializable 138 | data class Track( 139 | @Serializable(with = AudioTrackSerializer::class) 140 | val track: AudioTrack, 141 | val info: Info 142 | ) { 143 | @Serializable 144 | data class Info( 145 | val title: String, 146 | val author: String, 147 | val uri: String, 148 | val identifier: String, 149 | val length: Long, 150 | val position: Long, 151 | @SerialName("is_stream") 152 | val isStream: Boolean, 153 | @SerialName("is_seekable") 154 | val isSeekable: Boolean, 155 | ) 156 | } 157 | 158 | // taken from docs lmao 159 | object AudioTrackListSerializer : JsonTransformingSerializer>(ListSerializer(AudioTrackSerializer)) { 160 | override fun transformDeserialize(element: JsonElement): JsonElement = 161 | if (element !is JsonArray) { 162 | JsonArray(listOf(element)) 163 | } else { 164 | element 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/io/search/AudioLoader.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.search 18 | 19 | import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler 20 | import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager 21 | import com.sedmelluq.discord.lavaplayer.tools.FriendlyException 22 | import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist 23 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 24 | import org.slf4j.LoggerFactory 25 | import java.util.* 26 | import java.util.concurrent.CompletableFuture 27 | import java.util.concurrent.CompletionStage 28 | import java.util.concurrent.atomic.AtomicBoolean 29 | 30 | class AudioLoader(private val audioPlayerManager: AudioPlayerManager) : AudioLoadResultHandler { 31 | private val loadResult: CompletableFuture = CompletableFuture() 32 | private val used = AtomicBoolean(false) 33 | 34 | fun load(identifier: String?): CompletionStage { 35 | val isUsed = used.getAndSet(true) 36 | check(!isUsed) { 37 | "This loader can only be used once per instance" 38 | } 39 | 40 | logger.trace("Loading item with identifier $identifier") 41 | audioPlayerManager.loadItem(identifier, this) 42 | 43 | return loadResult 44 | } 45 | 46 | override fun trackLoaded(audioTrack: AudioTrack) { 47 | logger.info("Loaded track ${audioTrack.info.title}") 48 | 49 | val result = ArrayList() 50 | result.add(audioTrack) 51 | loadResult.complete(LoadResult(LoadType.TRACK_LOADED, result, null, null)) 52 | } 53 | 54 | override fun playlistLoaded(audioPlaylist: AudioPlaylist) { 55 | logger.info("Loaded playlist ${audioPlaylist.name}") 56 | 57 | var playlistName: String? = null 58 | var selectedTrack: Int? = null 59 | 60 | if (!audioPlaylist.isSearchResult) { 61 | playlistName = audioPlaylist.name 62 | selectedTrack = audioPlaylist.tracks.indexOf(audioPlaylist.selectedTrack) 63 | } 64 | 65 | val status: LoadType = if (audioPlaylist.isSearchResult) { 66 | LoadType.SEARCH_RESULT 67 | } else { 68 | LoadType.PLAYLIST_LOADED 69 | } 70 | 71 | val loadedItems = audioPlaylist.tracks 72 | loadResult.complete(LoadResult(status, loadedItems, playlistName, selectedTrack)) 73 | } 74 | 75 | override fun noMatches() { 76 | logger.info("No matches found") 77 | 78 | loadResult.complete(NO_MATCHES) 79 | } 80 | 81 | override fun loadFailed(e: FriendlyException) { 82 | logger.error("Load failed", e) 83 | 84 | loadResult.complete(LoadResult(e)) 85 | } 86 | 87 | companion object { 88 | private val logger = LoggerFactory.getLogger(AudioLoader::class.java) 89 | private val NO_MATCHES: LoadResult = LoadResult(LoadType.NO_MATCHES, emptyList(), null, null) 90 | } 91 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/io/search/LoadResult.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.search 18 | 19 | import com.sedmelluq.discord.lavaplayer.tools.FriendlyException 20 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 21 | 22 | class LoadResult { 23 | var loadResultType: LoadType 24 | private set 25 | 26 | var tracks: List 27 | private set 28 | 29 | var playlistName: String? 30 | private set 31 | 32 | var selectedTrack: Int? 33 | private set 34 | 35 | var exception: FriendlyException? 36 | private set 37 | 38 | constructor(loadResultType: LoadType, tracks: List, playlistName: String?, selectedTrack: Int?) { 39 | this.loadResultType = loadResultType 40 | this.tracks = tracks 41 | this.playlistName = playlistName 42 | this.selectedTrack = selectedTrack 43 | exception = null 44 | } 45 | 46 | constructor(exception: FriendlyException?) { 47 | loadResultType = LoadType.LOAD_FAILED 48 | tracks = emptyList() 49 | playlistName = null 50 | selectedTrack = null 51 | this.exception = exception 52 | } 53 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/io/search/LoadType.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.search 18 | 19 | enum class LoadType { 20 | TRACK_LOADED, 21 | PLAYLIST_LOADED, 22 | SEARCH_RESULT, 23 | NO_MATCHES, 24 | LOAD_FAILED 25 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.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.player 18 | 19 | import com.sedmelluq.discord.lavaplayer.player.AudioPlayer 20 | import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter 21 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 22 | import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason 23 | import obsidian.server.util.ByteRingBuffer 24 | import java.util.concurrent.TimeUnit 25 | 26 | class FrameLossTracker : AudioEventAdapter() { 27 | var success = ByteRingBuffer(60) 28 | var loss = ByteRingBuffer(60) 29 | val dataUsable: Boolean 30 | get() { 31 | if (lastTrackStarted - lastTrackEnded > ACCEPTABLE_TRACK_SWITCH_TIME && lastTrackEnded != Long.MAX_VALUE) { 32 | return false 33 | } 34 | 35 | return TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - playingSince) >= 60 36 | } 37 | 38 | private var curSuccess: Byte = 0 39 | private var curLoss: Byte = 0 40 | 41 | private var lastUpdate: Long = 0 42 | private var playingSince = Long.MAX_VALUE 43 | 44 | private var lastTrackEnded = Long.MAX_VALUE 45 | private var lastTrackStarted = Long.MAX_VALUE / 2 46 | 47 | /** 48 | * Increments the amount of successful frames. 49 | */ 50 | fun success() { 51 | checkTime() 52 | curSuccess++ 53 | } 54 | 55 | /** 56 | * Increments the amount of frame losses. 57 | */ 58 | fun loss() { 59 | checkTime() 60 | curLoss++ 61 | } 62 | 63 | private fun checkTime() { 64 | val now = System.nanoTime() 65 | if (now - lastUpdate > ONE_SECOND) { 66 | lastUpdate = now 67 | 68 | /* update success & loss buffers */ 69 | success.put(curSuccess) 70 | loss.put(curLoss) 71 | 72 | /* reset current success & loss */ 73 | curSuccess = 0 74 | curLoss = 0 75 | } 76 | } 77 | 78 | private fun start() { 79 | lastTrackStarted = System.nanoTime() 80 | if (lastTrackStarted - playingSince > ACCEPTABLE_TRACK_SWITCH_TIME || playingSince == Long.MAX_VALUE) { 81 | playingSince = lastTrackStarted 82 | 83 | /* clear success & loss buffers */ 84 | success.clear() 85 | loss.clear() 86 | } 87 | } 88 | 89 | private fun end() { 90 | lastTrackEnded = System.nanoTime() 91 | } 92 | 93 | /* listeners */ 94 | override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack?, reason: AudioTrackEndReason?) = end() 95 | override fun onTrackStart(player: AudioPlayer?, track: AudioTrack?) = start() 96 | override fun onPlayerPause(player: AudioPlayer?) = end() 97 | override fun onPlayerResume(player: AudioPlayer?) = start() 98 | 99 | companion object { 100 | const val ONE_SECOND = 1e9 101 | const val ACCEPTABLE_TRACK_SWITCH_TIME = 1e8 102 | 103 | /** 104 | * Number of packets expected to be sent over one minute. 105 | * *3000* packets with *20ms* of audio each 106 | */ 107 | const val EXPECTED_PACKET_COUNT_PER_MIN = 60 * 1000 / 20 108 | } 109 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/Link.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.player 18 | 19 | import com.sedmelluq.discord.lavaplayer.format.StandardAudioDataFormats 20 | import com.sedmelluq.discord.lavaplayer.player.AudioPlayer 21 | import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener 22 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 23 | import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame 24 | import io.netty.buffer.ByteBuf 25 | import obsidian.bedrock.MediaConnection 26 | import obsidian.bedrock.media.OpusAudioFrameProvider 27 | import obsidian.server.Obsidian.playerManager 28 | import obsidian.server.io.MagmaClient 29 | import obsidian.server.player.filter.FilterChain 30 | import java.nio.ByteBuffer 31 | 32 | class Link( 33 | val client: MagmaClient, 34 | val guildId: Long 35 | ) { 36 | /** 37 | * Handles sending of player updates 38 | */ 39 | val playerUpdates = PlayerUpdates(this) 40 | 41 | /** 42 | * The frame counter. 43 | */ 44 | val frameCounter = FrameLossTracker() 45 | 46 | /** 47 | * The lavaplayer filter. 48 | */ 49 | val audioPlayer: AudioPlayer = playerManager.createPlayer() 50 | .registerListener(playerUpdates) 51 | .registerListener(frameCounter) 52 | .registerListener(PlayerEvents(this)) 53 | 54 | /** 55 | * Whether the player is currently playing a track. 56 | */ 57 | val playing: Boolean 58 | get() = audioPlayer.playingTrack != null && !audioPlayer.isPaused 59 | 60 | /** 61 | * The current filter chain. 62 | */ 63 | var filters: FilterChain = FilterChain(this) 64 | set(value) { 65 | field = value 66 | value.apply() 67 | } 68 | 69 | /** 70 | * Plays the provided [track] and dispatches a Player Update 71 | */ 72 | suspend fun play(track: AudioTrack) { 73 | audioPlayer.playTrack(track) 74 | playerUpdates.sendUpdate() 75 | } 76 | 77 | /** 78 | * Used to seek 79 | */ 80 | fun seekTo(position: Long) { 81 | require(audioPlayer.playingTrack != null) { 82 | "A track must be playing in order to seek." 83 | } 84 | 85 | require(audioPlayer.playingTrack.isSeekable) { 86 | "The playing track is not seekable." 87 | } 88 | 89 | require(position in 0..audioPlayer.playingTrack.duration) { 90 | "The given position must be within 0 and the current playing track's duration." 91 | } 92 | 93 | audioPlayer.playingTrack.position = position 94 | } 95 | 96 | /** 97 | * Provides frames to the provided [MediaConnection] 98 | * 99 | * @param mediaConnection 100 | */ 101 | fun provideTo(mediaConnection: MediaConnection) { 102 | mediaConnection.frameProvider = LinkFrameProvider(mediaConnection) 103 | } 104 | 105 | inner class LinkFrameProvider(mediaConnection: MediaConnection) : OpusAudioFrameProvider(mediaConnection) { 106 | private val lastFrame = MutableAudioFrame().apply { 107 | val frameBuffer = ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize()) 108 | setBuffer(frameBuffer) 109 | } 110 | 111 | override fun canProvide(): Boolean { 112 | val frame = audioPlayer.provide(lastFrame) 113 | if (!frame) { 114 | frameCounter.loss() 115 | } 116 | 117 | return frame 118 | } 119 | 120 | override fun retrieveOpusFrame(targetBuffer: ByteBuf) { 121 | frameCounter.success() 122 | targetBuffer.writeBytes(lastFrame.data) 123 | } 124 | } 125 | 126 | companion object { 127 | fun AudioPlayer.registerListener(listener: AudioEventListener): AudioPlayer { 128 | addListener(listener) 129 | return this 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/PlayerEvents.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.player 18 | 19 | import com.sedmelluq.discord.lavaplayer.player.AudioPlayer 20 | import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter 21 | import com.sedmelluq.discord.lavaplayer.tools.FriendlyException 22 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 23 | import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason 24 | import kotlinx.coroutines.launch 25 | import obsidian.server.io.TrackEndEvent 26 | import obsidian.server.io.TrackExceptionEvent 27 | import obsidian.server.io.TrackStartEvent 28 | import obsidian.server.io.TrackStuckEvent 29 | import org.slf4j.Logger 30 | import org.slf4j.LoggerFactory 31 | 32 | class PlayerEvents(private val link: Link) : AudioEventAdapter() { 33 | override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack?, endReason: AudioTrackEndReason) { 34 | link.client.launch { 35 | val event = TrackEndEvent( 36 | guildId = link.guildId, 37 | track = track, 38 | endReason = endReason 39 | ) 40 | 41 | link.client.send(event) 42 | } 43 | } 44 | 45 | override fun onTrackStart(player: AudioPlayer?, track: AudioTrack) { 46 | link.client.launch { 47 | val event = TrackStartEvent( 48 | guildId = link.guildId, 49 | track = track 50 | ) 51 | 52 | link.client.send(event) 53 | } 54 | } 55 | 56 | override fun onTrackStuck(player: AudioPlayer?, track: AudioTrack?, thresholdMs: Long) { 57 | link.client.launch { 58 | logger.warn("${track?.info?.title} got stuck! Threshold surpassed: $thresholdMs"); 59 | 60 | val event = TrackStuckEvent( 61 | guildId = link.guildId, 62 | track = track, 63 | thresholdMs = thresholdMs 64 | ) 65 | 66 | link.client.send(event) 67 | } 68 | } 69 | 70 | override fun onTrackException(player: AudioPlayer?, track: AudioTrack?, exception: FriendlyException) { 71 | link.client.launch { 72 | val event = TrackExceptionEvent( 73 | guildId = link.guildId, 74 | track = track, 75 | exception = TrackExceptionEvent.Exception( 76 | message = exception.message, 77 | severity = exception.severity, 78 | cause = exception.rootCause.message 79 | ) 80 | ) 81 | 82 | link.client.send(event) 83 | } 84 | } 85 | 86 | companion object { 87 | private val logger: Logger = LoggerFactory.getLogger(PlayerEvents::class.java) 88 | 89 | val Throwable.rootCause: Throwable 90 | get() { 91 | var rootCause: Throwable? = this 92 | while (rootCause!!.cause != null) { 93 | rootCause = rootCause.cause 94 | } 95 | 96 | return rootCause 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.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.player 18 | 19 | import com.sedmelluq.discord.lavaplayer.player.AudioPlayer 20 | import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter 21 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 22 | import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason 23 | import kotlinx.coroutines.launch 24 | import obsidian.bedrock.util.Interval 25 | import obsidian.server.Obsidian.config 26 | import obsidian.server.io.CurrentTrack 27 | import obsidian.server.io.Frames 28 | import obsidian.server.io.PlayerUpdate 29 | import obsidian.server.util.TrackUtil 30 | import obsidian.server.util.config.ObsidianConfig 31 | 32 | class PlayerUpdates(val link: Link) : AudioEventAdapter() { 33 | /** 34 | * Whether player updates should be sent. 35 | */ 36 | var enabled: Boolean = true 37 | set(value) { 38 | field = value 39 | 40 | link.client.launch { 41 | if (value) start() else stop() 42 | } 43 | } 44 | 45 | /** 46 | * Whether a track is currently being played. 47 | */ 48 | val playing: Boolean 49 | get() = link.playing 50 | 51 | private val interval = Interval() 52 | 53 | /** 54 | * Starts sending player updates 55 | */ 56 | suspend fun start() { 57 | if (!interval.started && enabled) { 58 | interval.start(config[ObsidianConfig.PlayerUpdates.Interval], ::sendUpdate) 59 | } 60 | } 61 | 62 | /** 63 | * Stops player updates from being sent 64 | */ 65 | suspend fun stop() { 66 | if (interval.started) { 67 | interval.stop() 68 | } 69 | } 70 | 71 | suspend fun sendUpdate() { 72 | val currentTrack = CurrentTrack( 73 | track = TrackUtil.encode(link.audioPlayer.playingTrack), 74 | paused = link.audioPlayer.isPaused, 75 | position = link.audioPlayer.playingTrack.position 76 | ) 77 | 78 | val frames = Frames( 79 | sent = link.frameCounter.success.sum(), 80 | lost = link.frameCounter.loss.sum(), 81 | usable = link.frameCounter.dataUsable 82 | ) 83 | 84 | link.client.send( 85 | PlayerUpdate( 86 | guildId = link.guildId, 87 | currentTrack = currentTrack, 88 | frames = frames 89 | ) 90 | ) 91 | } 92 | 93 | override fun onTrackStart(player: AudioPlayer?, track: AudioTrack?) { 94 | link.client.launch { start() } 95 | } 96 | 97 | override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack?, endReason: AudioTrackEndReason?) { 98 | link.client.launch { stop() } 99 | } 100 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.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.player 18 | 19 | import com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler 20 | 21 | class TrackEndMarkerHandler(private val link: Link) : TrackMarkerHandler { 22 | override fun handle(state: TrackMarkerHandler.MarkerState) { 23 | if (state == TrackMarkerHandler.MarkerState.REACHED || state == TrackMarkerHandler.MarkerState.BYPASSED) { 24 | link.audioPlayer.stopTrack() 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/Filter.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.player.filter 18 | 19 | import com.sedmelluq.discord.lavaplayer.filter.AudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.DeserializationStrategy 23 | import kotlinx.serialization.descriptors.SerialDescriptor 24 | import kotlinx.serialization.encoding.Decoder 25 | import kotlin.math.abs 26 | 27 | interface Filter { 28 | /** 29 | * Whether this filter is enabled. 30 | */ 31 | val enabled: Boolean 32 | 33 | /** 34 | * Builds this filter's respective [AudioFilter] 35 | * 36 | * @param format The audio data format. 37 | * @param downstream The audio filter used as the downstream. 38 | * 39 | * @return null, if this filter isn't compatible with the provided format or if this filter isn't enabled. 40 | */ 41 | fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? 42 | 43 | companion object { 44 | /** 45 | * Minimum absolute difference for floating point values. Values whose difference to the default 46 | * value are smaller than this are considered equal to the default. 47 | */ 48 | const val MINIMUM_FP_DIFF = 0.01f 49 | 50 | /** 51 | * Returns true if the difference between [value] and [default] 52 | * is greater or equal to [MINIMUM_FP_DIFF] 53 | * 54 | * @param value The value to check 55 | * @param default Default value. 56 | * 57 | * @return true if the difference is greater or equal to the minimum. 58 | */ 59 | fun isSet(value: Float, default: Float): Boolean = 60 | abs(value - default) >= MINIMUM_FP_DIFF 61 | } 62 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/FilterChain.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.player.filter 18 | 19 | import com.sedmelluq.discord.lavaplayer.filter.AudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.filter.PcmFilterFactory 22 | import com.sedmelluq.discord.lavaplayer.filter.UniversalPcmAudioFilter 23 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 24 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 25 | import obsidian.server.io.Filters 26 | import obsidian.server.player.Link 27 | import obsidian.server.player.filter.impl.* 28 | 29 | class FilterChain(val link: Link) { 30 | var channelMix: ChannelMixFilter? = null 31 | var equalizer: EqualizerFilter? = null 32 | var karaoke: KaraokeFilter? = null 33 | var lowPass: LowPassFilter? = null 34 | var rotation: RotationFilter? = null 35 | var timescale: TimescaleFilter? = null 36 | var tremolo: TremoloFilter? = null 37 | var vibrato: VibratoFilter? = null 38 | var volume: VolumeFilter? = null 39 | 40 | 41 | /** 42 | * All enabled filters. 43 | */ 44 | val enabled: List 45 | get() = listOfNotNull(channelMix, equalizer, karaoke, lowPass, rotation, timescale, tremolo, vibrato, volume) 46 | 47 | /** 48 | * Get the filter factory. 49 | */ 50 | fun getFilterFactory(): FilterFactory { 51 | return FilterFactory() 52 | } 53 | 54 | /** 55 | * Applies all enabled filters to the player. 56 | */ 57 | fun apply() { 58 | link.audioPlayer.setFilterFactory(getFilterFactory()) 59 | } 60 | 61 | inner class FilterFactory : PcmFilterFactory { 62 | override fun buildChain( 63 | audioTrack: AudioTrack?, 64 | format: AudioDataFormat, 65 | output: UniversalPcmAudioFilter 66 | ): MutableList { 67 | val list: MutableList = mutableListOf() 68 | 69 | for (filter in enabled) { 70 | val audioFilter = filter.build(format, list.removeLastOrNull() ?: output) 71 | ?: continue 72 | 73 | list.add(audioFilter) 74 | } 75 | 76 | @Suppress("UNCHECKED_CAST") 77 | return list as MutableList 78 | } 79 | } 80 | 81 | companion object { 82 | fun from(link: Link, filters: Filters): FilterChain { 83 | return FilterChain(link).apply { 84 | channelMix = filters.channelMix 85 | equalizer = filters.equalizer 86 | karaoke = filters.karaoke 87 | lowPass = filters.lowPass 88 | rotation = filters.rotation 89 | timescale = filters.timescale 90 | tremolo = filters.tremolo 91 | vibrato = filters.vibrato 92 | 93 | filters.volume?.let { 94 | volume = VolumeFilter(it) 95 | } 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.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.player.filter.impl 18 | 19 | import com.github.natanbc.lavadsp.channelmix.ChannelMixPcmAudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.Serializable 23 | import obsidian.server.player.filter.Filter 24 | 25 | @Serializable 26 | data class ChannelMixFilter( 27 | val leftToLeft: Float = 1f, 28 | val leftToRight: Float = 0f, 29 | val rightToRight: Float = 0f, 30 | val rightToLeft: Float = 1f, 31 | ) : Filter { 32 | override val enabled: Boolean 33 | get() = Filter.isSet(leftToLeft, 1.0f) || Filter.isSet(leftToRight, 0.0f) || 34 | Filter.isSet(rightToLeft, 0.0f) || Filter.isSet(rightToRight, 1.0f); 35 | 36 | override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = 37 | ChannelMixPcmAudioFilter(downstream).also { 38 | it.leftToLeft = leftToLeft 39 | it.leftToRight = leftToRight 40 | it.rightToRight = rightToRight 41 | it.rightToLeft = rightToLeft 42 | } 43 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.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.player.filter.impl 18 | 19 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.equalizer.Equalizer 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.Serializable 23 | import obsidian.server.player.filter.Filter 24 | import obsidian.server.player.filter.Filter.Companion.isSet 25 | 26 | @Serializable 27 | data class EqualizerFilter(val bands: List) : Filter { 28 | override val enabled: Boolean 29 | get() = bands.any { isSet(it.gain, 0f) } 30 | 31 | override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? { 32 | if (!Equalizer.isCompatible(format)) { 33 | return null 34 | } 35 | 36 | val bands = FloatArray(15) { band -> 37 | bands.find { it.band == band }?.gain ?: 0f 38 | } 39 | 40 | return Equalizer(format.channelCount, downstream, bands) 41 | } 42 | 43 | @Serializable 44 | data class Band(val band: Int, val gain: Float) 45 | } 46 | -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/impl/KaraokeFilter.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.player.filter.impl 18 | 19 | import com.github.natanbc.lavadsp.karaoke.KaraokePcmAudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.SerialName 23 | import kotlinx.serialization.Serializable 24 | import obsidian.server.player.filter.Filter 25 | 26 | @Serializable 27 | data class KaraokeFilter( 28 | val level: Float, 29 | @SerialName("mono_level") 30 | val monoLevel: Float, 31 | @SerialName("filter_band") 32 | val filterBand: Float, 33 | @SerialName("filter_width") 34 | val filterWidth: Float, 35 | ) : Filter { 36 | override val enabled: Boolean 37 | get() = Filter.isSet(level, 1f) || Filter.isSet(monoLevel, 1f) 38 | || Filter.isSet(filterBand, 220f) || Filter.isSet(filterWidth, 100f) 39 | 40 | override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = 41 | KaraokePcmAudioFilter(downstream, format.channelCount, format.sampleRate) 42 | .setLevel(level) 43 | .setMonoLevel(monoLevel) 44 | .setFilterBand(filterBand) 45 | .setFilterWidth(filterWidth) 46 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.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.player.filter.impl 18 | 19 | import com.github.natanbc.lavadsp.lowpass.LowPassPcmAudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.Serializable 23 | import obsidian.server.player.filter.Filter 24 | 25 | @Serializable 26 | data class LowPassFilter( 27 | val smoothing: Float = 20f 28 | ) : Filter { 29 | override val enabled: Boolean 30 | get() = Filter.isSet(smoothing, 20f) 31 | 32 | override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = 33 | LowPassPcmAudioFilter(downstream, format.channelCount, 0) 34 | .setSmoothing(smoothing) 35 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.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.player.filter.impl 18 | 19 | import com.github.natanbc.lavadsp.rotation.RotationPcmAudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.SerialName 23 | import kotlinx.serialization.Serializable 24 | import obsidian.server.player.filter.Filter 25 | 26 | @Serializable 27 | data class RotationFilter( 28 | @SerialName("rotation_hz") 29 | val rotationHz: Float = 5f 30 | ) : Filter { 31 | override val enabled: Boolean 32 | get() = Filter.isSet(rotationHz, 5f) 33 | 34 | override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = 35 | RotationPcmAudioFilter(downstream, format.sampleRate) 36 | .setRotationSpeed(rotationHz.toDouble() /* seems like a bad idea idk. */) 37 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.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.player.filter.impl 18 | 19 | import com.github.natanbc.lavadsp.timescale.TimescalePcmAudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.Serializable 23 | import obsidian.server.player.filter.Filter 24 | import obsidian.server.player.filter.Filter.Companion.isSet 25 | import obsidian.server.util.NativeUtil 26 | 27 | @Serializable 28 | data class TimescaleFilter( 29 | val pitch: Float = 1f, 30 | val pitchOctaves: Float? = null, 31 | val pitchSemiTones: Float? = null, 32 | val speed: Float = 1f, 33 | val speedChange: Float? = null, 34 | val rate: Float = 1f, 35 | val rateChange: Float? = null, 36 | ) : Filter { 37 | override val enabled: Boolean 38 | get() = 39 | NativeUtil.timescaleAvailable 40 | && (isSet(pitch, 1f) 41 | || isSet(speed, 1f) 42 | || isSet(rate, 1f)) 43 | 44 | init { 45 | require(speed > 0) { 46 | "'speed' must be greater than 0" 47 | } 48 | 49 | require(rate > 0) { 50 | "'rate' must be greater than 0" 51 | } 52 | 53 | require(pitch > 0) { 54 | "'pitch' must be greater than 0" 55 | } 56 | 57 | if (pitchOctaves != null) { 58 | require(!isSet(pitch, 1.0F) && pitchSemiTones == null) { 59 | "'pitchOctaves' cannot be used in conjunction with 'pitch' and 'pitchSemiTones'" 60 | } 61 | } 62 | 63 | if (pitchSemiTones != null) { 64 | require(!isSet(pitch, 1.0F) && pitchOctaves == null) { 65 | "'pitchOctaves' cannot be used in conjunction with 'pitch' and 'pitchSemiTones'" 66 | } 67 | } 68 | 69 | if (speedChange != null) { 70 | require(!isSet(speed, 1.0F)) { 71 | "'speedChange' cannot be used in conjunction with 'speed'" 72 | } 73 | } 74 | 75 | if (rateChange != null) { 76 | require(!isSet(rate, 1.0F)) { 77 | "'rateChange' cannot be used in conjunction with 'rate'" 78 | } 79 | } 80 | } 81 | 82 | override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = 83 | TimescalePcmAudioFilter(downstream, format.channelCount, format.sampleRate).also { af -> 84 | af.pitch = pitch.toDouble() 85 | af.rate = rate.toDouble() 86 | af.speed = speed.toDouble() 87 | 88 | this.pitchOctaves?.let { af.setPitchOctaves(it.toDouble()) } 89 | this.pitchSemiTones?.let { af.setPitchSemiTones(it.toDouble()) } 90 | this.speedChange?.let { af.setSpeedChange(it.toDouble()) } 91 | this.rateChange?.let { af.setRateChange(it.toDouble()) } 92 | } 93 | 94 | } 95 | 96 | -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/impl/TremoloFilter.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.player.filter.impl 18 | 19 | import com.github.natanbc.lavadsp.tremolo.TremoloPcmAudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.Serializable 23 | import obsidian.server.player.filter.Filter 24 | import obsidian.server.player.filter.Filter.Companion.isSet 25 | 26 | @Serializable 27 | data class TremoloFilter( 28 | val frequency: Float = 2f, 29 | val depth: Float = 0f 30 | ) : Filter { 31 | override val enabled: Boolean 32 | get() = isSet(frequency, 2f) || isSet(depth, 0.5f); 33 | 34 | init { 35 | require(depth <= 1 && depth > 0) { 36 | "'depth' must be greater than 0 and less than 1" 37 | } 38 | 39 | require(frequency > 0) { 40 | "'frequency' must be greater than 0" 41 | } 42 | } 43 | 44 | override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = 45 | TremoloPcmAudioFilter(downstream, format.channelCount, format.sampleRate) 46 | .setDepth(depth) 47 | .setFrequency(frequency) 48 | } 49 | -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/impl/VibratoFilter.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.player.filter.impl 18 | 19 | import com.github.natanbc.lavadsp.vibrato.VibratoPcmAudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.Serializable 23 | import obsidian.server.player.filter.Filter 24 | 25 | @Serializable 26 | data class VibratoFilter( 27 | val frequency: Float = 2f, 28 | val depth: Float = .5f 29 | ) : Filter { 30 | override val enabled: Boolean 31 | get() = Filter.isSet(frequency, 2f) || Filter.isSet(depth, 0.5f) 32 | 33 | init { 34 | require(depth > 0 && depth < 1) { 35 | "'depth' must be greater than 0 and less than 1." 36 | } 37 | 38 | require(frequency > 0 && frequency < VIBRATO_FREQUENCY_MAX_HZ) { 39 | "'frequency' must be greater than 0 and less than $VIBRATO_FREQUENCY_MAX_HZ" 40 | } 41 | } 42 | 43 | override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = 44 | VibratoPcmAudioFilter(downstream, format.channelCount, format.sampleRate) 45 | .setFrequency(frequency) 46 | .setDepth(depth) 47 | 48 | companion object { 49 | private const val VIBRATO_FREQUENCY_MAX_HZ = 14f 50 | } 51 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.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.player.filter.impl 18 | 19 | import com.github.natanbc.lavadsp.volume.VolumePcmAudioFilter 20 | import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter 21 | import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat 22 | import kotlinx.serialization.Serializable 23 | import obsidian.server.player.filter.Filter 24 | import obsidian.server.player.filter.Filter.Companion.isSet 25 | 26 | @Serializable 27 | data class VolumeFilter( 28 | val volume: Float 29 | ) : Filter { 30 | override val enabled: Boolean 31 | get() = isSet(volume, 1f) 32 | 33 | init { 34 | require(volume in 0.0..5.0) { 35 | "'volume' must be >= 0 and <= 5." 36 | } 37 | } 38 | 39 | override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = 40 | VolumePcmAudioFilter(downstream) 41 | .setVolume(volume) 42 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/util/ByteRingBuffer.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.util 18 | 19 | import kotlin.math.min 20 | 21 | /** 22 | * Based off 23 | * https://github.com/natanbc/andesite/blob/master/api/src/main/java/andesite/util/ByteRingBuffer.java 24 | */ 25 | 26 | class ByteRingBuffer(private var size: Int) : Iterable { 27 | private val arr: ByteArray 28 | private var pos = 0 29 | 30 | init { 31 | require(size >= 1) { 32 | "Size must be greater or equal to 1." 33 | } 34 | 35 | arr = ByteArray(size) 36 | } 37 | 38 | /** 39 | * Returns the size of this buffer 40 | */ 41 | fun size() = size 42 | 43 | /** 44 | * Stores a value in this buffer. If the buffer is full, the oldest value is removed. 45 | * 46 | * @param value Value to store 47 | */ 48 | fun put(value: Byte) { 49 | arr[pos] = value 50 | pos = (pos + 1) % arr.size 51 | size = min(size + 1, arr.size) 52 | } 53 | 54 | /** 55 | * Clears this buffer 56 | */ 57 | fun clear() { 58 | pos = 0 59 | size = 0 60 | arr.fill(0) 61 | } 62 | 63 | /** 64 | * Returns the sum of all values in this buffer. 65 | */ 66 | fun sum(): Int { 67 | var sum = 0 68 | for (v in arr) sum += v 69 | return sum 70 | } 71 | 72 | /** 73 | * Returns the [n]th element of this buffer. 74 | * An index of 0 returns the oldest, 75 | * an index of `size() - 1` returns the newest 76 | * 77 | * @param n Index of the wanted element 78 | * 79 | * @throws NoSuchElementException If [n] >= [size] 80 | */ 81 | fun get(n: Int): Byte { 82 | if (n >= size) { 83 | throw NoSuchElementException() 84 | } 85 | 86 | return arr[sub(pos, size - n, arr.size)] 87 | } 88 | 89 | /** 90 | * Returns the last element of this buffer. 91 | * Equivalent to `getLast(0)` 92 | * 93 | * @throws NoSuchElementException If this buffer is empty. 94 | */ 95 | fun getLast() = getLast(0) 96 | 97 | /** 98 | * Returns the [n]th last element of this buffer. 99 | * An index of 0 returns the newest, 100 | * an index of `size() - 1` returns the oldest. 101 | * 102 | * @param n Index of the wanted element. 103 | * 104 | * @throws NoSuchElementException If [n] >= [size] 105 | */ 106 | fun getLast(n: Int): Byte { 107 | if (n >= size) { 108 | throw NoSuchElementException() 109 | } 110 | 111 | return arr[sub(pos, n + 1, arr.size)] 112 | } 113 | 114 | override fun iterator(): Iterator = 115 | object : Iterator { 116 | var cursor = pos 117 | var remaining = size 118 | 119 | override fun hasNext(): Boolean = 120 | remaining > 0 121 | 122 | override fun next(): Byte { 123 | val v = arr[cursor] 124 | cursor = inc(cursor, arr.size) 125 | remaining-- 126 | 127 | return v 128 | } 129 | } 130 | 131 | companion object { 132 | fun sub(i: Int, j: Int, mod: Int) = 133 | (i - j).takeIf { mod < it } 134 | ?: 0 135 | 136 | fun inc(i: Int, mod: Int): Int = 137 | i.inc().takeIf { mod < it } 138 | ?: 0 139 | } 140 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/util/CpuTimer.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.util 18 | 19 | import org.slf4j.Logger 20 | import org.slf4j.LoggerFactory 21 | import java.lang.management.ManagementFactory 22 | import java.lang.management.OperatingSystemMXBean 23 | import java.lang.reflect.Method 24 | 25 | class CpuTimer { 26 | val processRecentCpuUsage: Double 27 | get() = try { 28 | //between 0.0 and 1.0, 1.0 meaning all CPU cores were running threads of this JVM 29 | // see com.sun.management.OperatingSystemMXBean#getProcessCpuLoad and https://www.ibm.com/support/knowledgecenter/en/SSYKE2_7.1.0/com.ibm.java.api.71.doc/com.ibm.lang.management/com/ibm/lang/management/OperatingSystemMXBean.html#getProcessCpuLoad() 30 | val cpuLoad = callDoubleGetter("getProcessCpuLoad", osBean) 31 | cpuLoad ?: ERROR 32 | } catch (ex: Throwable) { 33 | logger.debug("Couldn't access process cpu time", ex) 34 | ERROR 35 | } 36 | 37 | val systemRecentCpuUsage: Double 38 | get() = try { 39 | //between 0.0 and 1.0, 1.0 meaning all CPU cores were running threads of this JVM 40 | // see com.sun.management.OperatingSystemMXBean#getProcessCpuLoad and https://www.ibm.com/support/knowledgecenter/en/SSYKE2_7.1.0/com.ibm.java.api.71.doc/com.ibm.lang.management/com/ibm/lang/management/OperatingSystemMXBean.html#getProcessCpuLoad() 41 | val cpuLoad = callDoubleGetter("getSystemCpuLoad", osBean) 42 | cpuLoad ?: ERROR 43 | } catch (ex: Throwable) { 44 | logger.debug("Couldn't access system cpu time", ex) 45 | ERROR 46 | } 47 | 48 | 49 | /** 50 | * The operating system bean used to get statistics. 51 | */ 52 | private val osBean: OperatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean() 53 | 54 | // Code below copied from Prometheus's StandardExports (Apache 2.0) with slight modifications 55 | private fun callDoubleGetter(getterName: String, obj: Any): Double? { 56 | return callDoubleGetter(obj.javaClass.getMethod(getterName), obj) 57 | } 58 | 59 | /** 60 | * Attempts to call a method either directly or via one of the implemented interfaces. 61 | * 62 | * 63 | * A Method object refers to a specific method declared in a specific class. The first invocation 64 | * might happen with method == SomeConcreteClass.publicLongGetter() and will fail if 65 | * SomeConcreteClass is not public. We then recurse over all interfaces implemented by 66 | * SomeConcreteClass (or extended by those interfaces and so on) until we eventually invoke 67 | * callMethod() with method == SomePublicInterface.publicLongGetter(), which will then succeed. 68 | * 69 | * 70 | * There is a built-in assumption that the method will never return null (or, equivalently, that 71 | * it returns the primitive data type, i.e. `long` rather than `Long`). If this 72 | * assumption doesn't hold, the method might be called repeatedly and the returned value will be 73 | * the one produced by the last call. 74 | */ 75 | private fun callDoubleGetter(method: Method, obj: Any): Double? { 76 | try { 77 | return method.invoke(obj) as Double 78 | } catch (e: IllegalAccessException) { 79 | // Expected, the declaring class or interface might not be public. 80 | } 81 | 82 | // Iterate over all implemented/extended interfaces and attempt invoking the method with the 83 | // same name and parameters on each. 84 | for (clazz in method.declaringClass.interfaces) { 85 | try { 86 | val interfaceMethod: Method = clazz.getMethod(method.name, * method.parameterTypes) 87 | val result = callDoubleGetter(interfaceMethod, obj) 88 | 89 | if (result != null) { 90 | return result 91 | } 92 | } catch (e: NoSuchMethodException) { 93 | // Expected, class might implement multiple, unrelated interfaces. 94 | } 95 | } 96 | 97 | return null 98 | } 99 | 100 | companion object { 101 | private const val ERROR = -1.0 102 | private val logger: Logger = LoggerFactory.getLogger(CpuTimer::class.java) 103 | } 104 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.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.util 18 | 19 | import ch.qos.logback.classic.Level 20 | import ch.qos.logback.classic.spi.ILoggingEvent 21 | import ch.qos.logback.core.pattern.CompositeConverter 22 | import com.github.ajalt.mordant.rendering.TextColors.* 23 | import com.github.ajalt.mordant.rendering.TextStyles.* 24 | 25 | class LogbackColorConverter : CompositeConverter() { 26 | override fun transform(event: ILoggingEvent, element: String): String { 27 | val option = ANSI_COLORS[firstOption] 28 | ?: ANSI_COLORS[LEVELS[event.level.toInt()]] 29 | ?: ANSI_COLORS["green"] 30 | 31 | return option!!(element) 32 | } 33 | 34 | companion object { 35 | private val ANSI_COLORS = mapOf String>( 36 | "red" to { t -> red(t) }, 37 | "green" to { t -> green(t) }, 38 | "yellow" to { t -> yellow(t) }, 39 | "blue" to { t -> blue(t) }, 40 | "magenta" to { t -> magenta(t) }, 41 | "cyan" to { t -> cyan(t) }, 42 | "gray" to { t -> gray(t) }, 43 | "faint" to { t -> dim(t) } 44 | ) 45 | 46 | private val LEVELS = mapOf( 47 | Level.ERROR_INTEGER to "red", 48 | Level.WARN_INTEGER to "yellow", 49 | Level.DEBUG_INTEGER to "blue", 50 | Level.INFO_INTEGER to "faint", 51 | Level.TRACE_INTEGER to "magenta" 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/util/NativeUtil.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.util 18 | 19 | import com.github.natanbc.lavadsp.natives.TimescaleNativeLibLoader 20 | import com.github.natanbc.nativeloader.NativeLibLoader 21 | import org.slf4j.Logger 22 | import org.slf4j.LoggerFactory 23 | import com.sedmelluq.discord.lavaplayer.natives.ConnectorNativeLibLoader 24 | 25 | /** 26 | * Based on https://github.com/natanbc/andesite/blob/master/src/main/java/andesite/util/NativeUtils.java 27 | * 28 | * i want native stuff too :'( 29 | */ 30 | 31 | object NativeUtil { 32 | var timescaleAvailable: Boolean = false 33 | 34 | /* private shit */ 35 | private val logger: Logger = LoggerFactory.getLogger(NativeUtil::class.java) 36 | 37 | // loaders 38 | private val CONNECTOR_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "connector") 39 | 40 | // class names 41 | private const val LOAD_RESULT_NAME = "com.sedmelluq.lava.common.natives.NativeLibraryLoader\$LoadResult" 42 | 43 | private var LOAD_RESULT: Any? = try { 44 | val ctor = Class.forName(LOAD_RESULT_NAME) 45 | .getDeclaredConstructor(Boolean::class.javaPrimitiveType, RuntimeException::class.java) 46 | 47 | ctor.isAccessible = true 48 | ctor.newInstance(true, null) 49 | } catch (e: ReflectiveOperationException) { 50 | logger.error("Unable to create successful load result"); 51 | null; 52 | } 53 | 54 | /** 55 | * Loads native library shit 56 | */ 57 | fun load() { 58 | loadConnector() 59 | timescaleAvailable = loadTimescale() 60 | } 61 | 62 | /** 63 | * Loads the timescale libraries 64 | */ 65 | fun loadTimescale(): Boolean = try { 66 | TimescaleNativeLibLoader.loadTimescaleLibrary() 67 | logger.info("Timescale loaded") 68 | true 69 | } catch (ex: Exception) { 70 | logger.warn("Timescale failed to load", ex) 71 | false 72 | } 73 | 74 | /** 75 | * Loads the lp-cross version of lavaplayer's loader 76 | */ 77 | private fun loadConnector() { 78 | try { 79 | CONNECTOR_LOADER.load() 80 | 81 | val loadersField = ConnectorNativeLibLoader::class.java.getDeclaredField("loaders") 82 | loadersField.isAccessible = true 83 | 84 | for (i in 0 until 2) { 85 | // wtf natan 86 | markLoaded(java.lang.reflect.Array.get(loadersField.get(null), i)) 87 | } 88 | 89 | logger.info("Connector loaded") 90 | } catch (ex: Exception) { 91 | logger.error("Connected failed to load", ex) 92 | } 93 | } 94 | 95 | private fun markLoaded(loader: Any) { 96 | val previousResultField = loader.javaClass.getDeclaredField("previousResult") 97 | previousResultField.isAccessible = true 98 | previousResultField[loader] = LOAD_RESULT 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/util/ThreadFactory.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.util 18 | 19 | import java.util.* 20 | import java.util.concurrent.ThreadFactory 21 | import java.util.concurrent.atomic.AtomicInteger 22 | 23 | val counter = AtomicInteger() 24 | fun threadFactory( 25 | name: String, 26 | daemon: Boolean? = null, 27 | priority: Int? = null, 28 | exceptionHandler: Thread.UncaughtExceptionHandler? = null 29 | ) = 30 | ThreadFactory { runnable -> 31 | Thread(runnable, name.format(Locale.ROOT, counter.getAndIncrement())).apply { 32 | daemon?.let { this.isDaemon = it } 33 | priority?.let { this.priority = priority } 34 | exceptionHandler?.let { this.uncaughtExceptionHandler = it } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/util/TrackUtil.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.util 18 | 19 | import com.sedmelluq.discord.lavaplayer.tools.io.MessageInput 20 | import com.sedmelluq.discord.lavaplayer.tools.io.MessageOutput 21 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 22 | import obsidian.server.Obsidian.playerManager 23 | import java.io.ByteArrayInputStream 24 | import java.io.ByteArrayOutputStream 25 | import java.util.* 26 | 27 | object TrackUtil { 28 | /** 29 | * Decodes a base64 encoded string into a usable [AudioTrack] 30 | * 31 | * @param encodedTrack The base64 encoded string. 32 | * 33 | * @return The decoded [AudioTrack] 34 | */ 35 | fun decode(encodedTrack: String): AudioTrack { 36 | val decoded = Base64.getDecoder() 37 | .decode(encodedTrack) 38 | 39 | val inputStream = ByteArrayInputStream(decoded) 40 | val track = playerManager.decodeTrack(MessageInput(inputStream))!!.decodedTrack 41 | 42 | inputStream.close() 43 | return track 44 | } 45 | 46 | /** 47 | * Encodes a [AudioTrack] into a base64 encoded string. 48 | * 49 | * @param track The audio track to encode. 50 | * 51 | * @return The base64 encoded string 52 | */ 53 | fun encode(track: AudioTrack): String { 54 | val outputStream = ByteArrayOutputStream() 55 | playerManager.encodeTrack(MessageOutput(outputStream), track) 56 | 57 | val encoded = Base64.getEncoder() 58 | .encodeToString(outputStream.toByteArray()) 59 | 60 | outputStream.close() 61 | return encoded 62 | } 63 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/util/config/LoggingConfig.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.util.config 18 | 19 | import com.uchuhimo.konf.ConfigSpec 20 | 21 | object LoggingConfig : ConfigSpec("logging") { 22 | 23 | object Level : ConfigSpec("level") { 24 | /** 25 | * Root logging level 26 | */ 27 | val Root by optional("INFO") 28 | 29 | /** 30 | * Obsidian logging level 31 | */ 32 | val Obsidian by optional("INFO") 33 | } 34 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/util/config/ObsidianConfig.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.util.config 18 | 19 | import com.uchuhimo.konf.ConfigSpec 20 | import obsidian.server.Obsidian.config 21 | 22 | object ObsidianConfig : ConfigSpec("obsidian") { 23 | /** 24 | * The address that the server will bind to. 25 | * 26 | * `obsidian.host` 27 | */ 28 | val Host by optional("0.0.0.0") 29 | 30 | /** 31 | * The server port. 32 | * 33 | * `obsidian.port` 34 | */ 35 | val Port by optional(3030) 36 | 37 | /** 38 | * The server password. 39 | * 40 | * `obsidian.password` 41 | */ 42 | val Password by optional("") 43 | 44 | /** 45 | * Whether obsidian should immediately start providing frames after connecting to the voice server. 46 | * 47 | * `immediately-provide` 48 | */ 49 | val ImmediatelyProvide by optional(true, "immediately-provide") 50 | 51 | /** 52 | * Whether the `Client-Name` header is required for Clients. 53 | * 54 | * `require-client-name` 55 | */ 56 | val RequireClientName by optional(false, "require-client-name") 57 | 58 | /** 59 | * Used to validate a string given as authorization. 60 | * 61 | * @param given The given authorization string. 62 | * 63 | * @return true, if the given authorization matches the configured password. 64 | */ 65 | fun validateAuth(given: String?): Boolean = when { 66 | config[Password].isEmpty() -> true 67 | else -> given == config[Password] 68 | } 69 | 70 | object PlayerUpdates : ConfigSpec("player-updates") { 71 | /** 72 | * The delay (in milliseconds) between each player update. 73 | * 74 | * `obsidian.player-updates.interval` 75 | */ 76 | val Interval by optional(5000L, "interval") 77 | 78 | /** 79 | * Whether the filters object should be sent with Player Updates 80 | * 81 | * `obsidian.player-updates.send-filters` 82 | */ 83 | val SendFilters by optional(true, "send-filters") 84 | } 85 | 86 | object Lavaplayer : ConfigSpec("lavaplayer") { 87 | /** 88 | * Whether garbage collection should be monitored. 89 | * 90 | * `obsidian.lavaplayer.gc-monitoring` 91 | */ 92 | val GcMonitoring by optional(false, "gc-monitoring") 93 | 94 | /** 95 | * Whether lavaplayer shouldn't allocate audio frames 96 | * 97 | * `obsidian.lavaplayer.non-allocating` 98 | */ 99 | val NonAllocating by optional(false, "non-allocating") 100 | 101 | /** 102 | * Names of sources that will be enabled. 103 | * 104 | * `obsidian.lavaplayer.enabled-sources` 105 | */ 106 | val EnabledSources by optional( 107 | setOf( 108 | "youtube", 109 | "yarn", 110 | "bandcamp", 111 | "twitch", 112 | "vimeo", 113 | "nico", 114 | "soundcloud", 115 | "local", 116 | "http" 117 | ), "enabled-sources" 118 | ) 119 | 120 | /** 121 | * Whether `scsearch:` should be allowed. 122 | * 123 | * `obsidian.lavaplayer.allow-scsearch` 124 | */ 125 | val AllowScSearch by optional(true, "allow-scsearch") 126 | 127 | object RateLimit : ConfigSpec("rate-limit") { 128 | /** 129 | * Ip blocks to use. 130 | * 131 | * `obsidian.lavaplayer.rate-limit.ip-blocks` 132 | */ 133 | val IpBlocks by optional(emptyList(), "ip-blocks") 134 | 135 | /** 136 | * IPs which should be excluded from usage by the route planner 137 | * 138 | * `obsidian.lavaplayer.rate-limit.excluded-ips` 139 | */ 140 | val ExcludedIps by optional(emptyList(), "excluded-ips") 141 | 142 | /** 143 | * The route planner strategy to use. 144 | * 145 | * `obsidian.lavaplayer.rate-limit.strategy` 146 | */ 147 | val Strategy by optional("rotate-on-ban") 148 | 149 | /** 150 | * Whether a search 429 should trigger marking the ip as failing. 151 | * 152 | * `obsidian.lavaplayer.rate-limit.search-triggers-fail` 153 | */ 154 | val SearchTriggersFail by optional(true, "search-triggers-fail") 155 | 156 | /** 157 | * -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times 158 | * 159 | * `obsidian.lavaplayer.rate-limit.retry-limit` 160 | */ 161 | val RetryLimit by optional(-1, "retry-limit") 162 | } 163 | 164 | object Nico : ConfigSpec("nico") { 165 | /** 166 | * The email to use for the Nico Source. 167 | * 168 | * `obsidian.lavaplayer.nico.email` 169 | */ 170 | val Email by optional("") 171 | 172 | /** 173 | * The password to use for the Nico Source. 174 | * 175 | * `obsidian.lavaplayer.nico.password` 176 | */ 177 | val Password by optional("") 178 | } 179 | 180 | object YouTube : ConfigSpec("youtube") { 181 | /** 182 | * Whether `ytsearch:` should be allowed. 183 | * 184 | * `obsidian.lavaplayer.youtube.allow-ytsearch` 185 | */ 186 | val AllowSearch by optional(true, "allow-search") 187 | 188 | /** 189 | * Total number of pages (100 tracks per page) to load 190 | * 191 | * `obsidian.lavaplayer.youtube.playlist-page-limit` 192 | */ 193 | val PlaylistPageLimit by optional(6, "playlist-page-limit") 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /Server/src/main/kotlin/obsidian/server/util/kxs/AudioTrackSerializer.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.util.kxs 18 | 19 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 20 | import kotlinx.serialization.KSerializer 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 | import obsidian.server.util.TrackUtil 27 | 28 | object AudioTrackSerializer : KSerializer { 29 | override val descriptor: SerialDescriptor = 30 | PrimitiveSerialDescriptor("AudioTrack", PrimitiveKind.STRING) 31 | 32 | override fun serialize(encoder: Encoder, value: AudioTrack) { 33 | encoder.encodeString(TrackUtil.encode(value)) 34 | } 35 | 36 | override fun deserialize(decoder: Decoder): AudioTrack { 37 | return TrackUtil.decode(decoder.decodeString()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Server/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){gray} %clr([%35.-35thread]){magenta} %clr(%-35.35logger{39}){cyan} %highlight(%-6level) %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Server/src/test/resources/routePlanner.http: -------------------------------------------------------------------------------- 1 | ### route planner status 2 | GET http://localhost:3030/routeplanner/status 3 | Accept: application/json 4 | 5 | ### free single 6 | POST http://localhost:3030/routeplanner/free/address 7 | Accept: application/json 8 | Content-Type: application/json 9 | 10 | { 11 | "address": "" 12 | } 13 | 14 | ### 15 | -------------------------------------------------------------------------------- /Server/src/test/resources/tracks.http: -------------------------------------------------------------------------------- 1 | ### /decodetracks multiple 2 | 3 | POST http://localhost:3030/decodetracks 4 | Content-Type: application/json 5 | 6 | { 7 | "tracks": [ 8 | "QAAAigIAKkhhbHNleSwgRG9taW5pYyBGaWtlIC0gRG9taW5pYydzIEludGVybHVkZQAGSGFsc2V5AAAAAAABZ2AAC1BiS05sOHBPVlRFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9UGJLTmw4cE9WVEUAB3lvdXR1YmUAAAAAAAAAAA==", 9 | "QAAAagIABGRhcmsADGJsb29keSB3aGl0ZQAAAAAAAW8wAAthSjFOS0NST1pDcwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWFKMU5LQ1JPWkNzAAd5b3V0dWJlAAAAAAAAAAA=" 10 | ] 11 | } 12 | 13 | ### /decodetracks single 14 | 15 | POST http://localhost:3030/decodetracks 16 | Content-Type: application/json 17 | 18 | { 19 | "tracks": "QAAAagIABGRhcmsADGJsb29keSB3aGl0ZQAAAAAAAW8wAAthSjFOS0NST1pDcwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWFKMU5LQ1JPWkNzAAd5b3V0dWJlAAAAAAAAAAA=" 20 | } 21 | 22 | ### /loadtracks 23 | 24 | GET http://localhost:3030/loadtracks?identifier=ytsearch:zacari%20mood 25 | Authorization: cockandballs -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | jcenter() 4 | 5 | maven { url "https://jitpack.io" } 6 | maven { url "https://dl.bintray.com/natanbc/maven" } 7 | maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } 8 | maven { url "https://m2.dv8tion.net/releases" } 9 | } 10 | 11 | apply plugin: 'idea' 12 | group = "gg.mixtape.obsidian" 13 | } 14 | 15 | subprojects { 16 | buildscript { 17 | ext { 18 | shadow_version = "6.1.0" 19 | kotlin_version = "1.4.32" 20 | } 21 | 22 | repositories { 23 | maven { url "https://plugins.gradle.org/m2/" } 24 | mavenCentral() 25 | } 26 | 27 | dependencies { 28 | classpath "com.github.jengelman.gradle.plugins:shadow:${shadow_version}" 29 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 30 | classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" 31 | classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" 32 | } 33 | } 34 | 35 | 36 | apply plugin: "java" 37 | 38 | sourceCompatibility = 11 39 | targetCompatibility = 11 40 | 41 | compileJava.options.encoding = 'UTF-8' 42 | compileJava.options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 43 | 44 | tasks.withType(JavaCompile) { 45 | options.encoding = 'UTF-8' 46 | } 47 | 48 | ext { 49 | // kotlin 50 | kotlinx_coroutines_version = "1.4.3" 51 | 52 | // audio 53 | lavaplayer_version = "1.3.76" 54 | lavadsp_version = "0.7.7" 55 | netty_version = "4.1.63.Final" 56 | lavaplayer_ip_rotator_config = "0.2.3" 57 | native_loader_version = "0.7.0" 58 | lpcross_version = "0.1.1" 59 | 60 | // logging 61 | logback_version = "1.2.3" 62 | mordant_version = "2.0.0-beta1" 63 | 64 | // serialization 65 | serialization_json_version = "1.1.0" 66 | 67 | // config 68 | konf_version = "1.1.2" 69 | 70 | // ktor 71 | ktor_version = "1.5.3" 72 | } 73 | } 74 | 75 | ext { 76 | moduleName = "Obsidian-Root" 77 | } 78 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dimensional-fun/obsidian/892ec1c302ed2a5015d82ce34cbd9557354e2823/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /obsidian.yml: -------------------------------------------------------------------------------- 1 | obsidian: 2 | port: 3030 3 | password: "" 4 | require-client-name: false 5 | player-updates: 6 | interval: 5000 7 | send-filters: false 8 | 9 | lavaplayer: 10 | gc-monitoring: true 11 | non-allocating: false 12 | enabled-sources: [ "youtube", "yarn", "bandcamp", "twitch", "vimeo", "nico", "soundcloud", "local", "http" ] 13 | allow-scsearch: true 14 | rate-limit: 15 | ip-blocks: [] 16 | excluded-ips: [] 17 | strategy: "rotate-on-ban" # rotate-on-ban | load-balance | nano-switch | rotating-nano-switch 18 | search-triggers-fail: true # Whether a search 429 should trigger marking the ip as failing. 19 | retry-limit: -1 20 | youtube: 21 | allow-search: true 22 | playlist-page-limit: 6 23 | 24 | logging: 25 | level: 26 | root: INFO 27 | obsidian: INFO 28 | 29 | file: 30 | max-history: 30 31 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'Server' 2 | 3 | --------------------------------------------------------------------------------