From 0c6b31c0ae3f42a936eddfc195274d7fe9ef1b0b Mon Sep 17 00:00:00 2001 From: melike2d Date: Sat, 1 May 2021 23:22:35 -0700 Subject: [PATCH 01/46] :sparkles: implement filters --- .../UnsupportedEncryptionModeException.kt | 19 ---- .../obsidian/server/player/filter/Filter.kt | 6 +- .../server/player/filter/FilterChain.kt | 99 ------------------- .../obsidian/server/player/filter/Filters.kt | 97 ++++++++++++++++++ .../player/filter/impl/ChannelMixFilter.kt | 2 +- .../player/filter/impl/DistortionFilter.kt | 49 +++++++++ .../player/filter/impl/EqualizerFilter.kt | 7 +- .../player/filter/impl/KaraokeFilter.kt | 2 +- .../player/filter/impl/LowPassFilter.kt | 6 +- .../player/filter/impl/RotationFilter.kt | 2 +- .../player/filter/impl/TimescaleFilter.kt | 17 ++-- .../player/filter/impl/VibratoFilter.kt | 2 +- .../server/player/filter/impl/VolumeFilter.kt | 16 ++- 13 files changed, 172 insertions(+), 152 deletions(-) delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/crypto/UnsupportedEncryptionModeException.kt delete mode 100644 Server/src/main/kotlin/obsidian/server/player/filter/FilterChain.kt create mode 100644 Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt create mode 100644 Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt diff --git a/Server/src/main/kotlin/obsidian/bedrock/crypto/UnsupportedEncryptionModeException.kt b/Server/src/main/kotlin/obsidian/bedrock/crypto/UnsupportedEncryptionModeException.kt deleted file mode 100644 index abccc9e..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/crypto/UnsupportedEncryptionModeException.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.crypto - -class UnsupportedEncryptionModeException(message: String) : IllegalArgumentException(message) diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/Filter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/Filter.kt index 72f7dc7..7e2fa88 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/Filter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/Filter.kt @@ -16,12 +16,8 @@ package obsidian.server.player.filter -import com.sedmelluq.discord.lavaplayer.filter.AudioFilter import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder import kotlin.math.abs interface Filter { @@ -59,4 +55,4 @@ interface Filter { fun isSet(value: Float, default: Float): Boolean = abs(value - default) >= MINIMUM_FP_DIFF } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/FilterChain.kt b/Server/src/main/kotlin/obsidian/server/player/filter/FilterChain.kt deleted file mode 100644 index a64a320..0000000 --- a/Server/src/main/kotlin/obsidian/server/player/filter/FilterChain.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.server.player.filter - -import com.sedmelluq.discord.lavaplayer.filter.AudioFilter -import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter -import com.sedmelluq.discord.lavaplayer.filter.PcmFilterFactory -import com.sedmelluq.discord.lavaplayer.filter.UniversalPcmAudioFilter -import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat -import com.sedmelluq.discord.lavaplayer.track.AudioTrack -import obsidian.server.io.Filters -import obsidian.server.player.Link -import obsidian.server.player.filter.impl.* - -class FilterChain(val link: Link) { - var channelMix: ChannelMixFilter? = null - var equalizer: EqualizerFilter? = null - var karaoke: KaraokeFilter? = null - var lowPass: LowPassFilter? = null - var rotation: RotationFilter? = null - var timescale: TimescaleFilter? = null - var tremolo: TremoloFilter? = null - var vibrato: VibratoFilter? = null - var volume: VolumeFilter? = null - - - /** - * All enabled filters. - */ - val enabled: List - get() = listOfNotNull(channelMix, equalizer, karaoke, lowPass, rotation, timescale, tremolo, vibrato, volume) - - /** - * Get the filter factory. - */ - fun getFilterFactory(): FilterFactory { - return FilterFactory() - } - - /** - * Applies all enabled filters to the player. - */ - fun apply() { - link.audioPlayer.setFilterFactory(getFilterFactory()) - } - - inner class FilterFactory : PcmFilterFactory { - override fun buildChain( - audioTrack: AudioTrack?, - format: AudioDataFormat, - output: UniversalPcmAudioFilter - ): MutableList { - val list: MutableList = mutableListOf() - - for (filter in enabled) { - val audioFilter = filter.build(format, list.removeLastOrNull() ?: output) - ?: continue - - list.add(audioFilter) - } - - @Suppress("UNCHECKED_CAST") - return list as MutableList - } - } - - companion object { - fun from(link: Link, filters: Filters): FilterChain { - return FilterChain(link).apply { - channelMix = filters.channelMix - equalizer = filters.equalizer - karaoke = filters.karaoke - lowPass = filters.lowPass - rotation = filters.rotation - timescale = filters.timescale - tremolo = filters.tremolo - vibrato = filters.vibrato - - filters.volume?.let { - volume = VolumeFilter(it) - } - } - } - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt b/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt new file mode 100644 index 0000000..3f5d9f7 --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.player.filter + +import com.sedmelluq.discord.lavaplayer.filter.AudioFilter +import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter +import com.sedmelluq.discord.lavaplayer.filter.PcmFilterFactory +import com.sedmelluq.discord.lavaplayer.filter.UniversalPcmAudioFilter +import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat +import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import obsidian.server.player.Player +import obsidian.server.player.filter.impl.* + +@Serializable +data class Filters( + val volume: VolumeFilter?, + val equalizer: EqualizerFilter?, + val karaoke: KaraokeFilter?, + val rotation: RotationFilter?, + val tremolo: TremoloFilter?, + val vibrato: VibratoFilter?, + val distortion: DistortionFilter?, + val timescale: TimescaleFilter?, + @SerialName("low_pass") + val lowPass: LowPassFilter?, + @SerialName("channel_mix") + val channelMix: ChannelMixFilter?, +) { + /** + * All filters + */ + val asList: List + get() = listOfNotNull( + volume, + equalizer, + karaoke, + rotation, + tremolo, + vibrato, + distortion, + timescale, + lowPass, + channelMix + ) + + /** + * List of all enabled filters. + */ + val enabled + get() = asList.filter { + it.enabled + } + + /** + * Applies all enabled filters to the audio player declared at [Player.audioPlayer]. + */ + fun applyTo(player: Player) { + val factory = FilterFactory(this) + player.audioPlayer.setFilterFactory(factory) + } + + class FilterFactory(private val filters: Filters) : PcmFilterFactory { + override fun buildChain( + track: AudioTrack?, + format: AudioDataFormat, + output: UniversalPcmAudioFilter + ): MutableList { + // dont remove explicit type declaration + val list = buildList { + for (filter in filters.enabled) { + val audioFilter = filter.build(format, lastOrNull() ?: output) + ?: continue + + add(audioFilter) + } + } + + return list.toMutableList() + } + } +} diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt index 6437c1d..6e299b0 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt @@ -40,4 +40,4 @@ data class ChannelMixFilter( it.rightToRight = rightToRight it.rightToLeft = rightToLeft } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt new file mode 100644 index 0000000..95bbc76 --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.player.filter.impl + +import com.github.natanbc.lavadsp.distortion.DistortionPcmAudioFilter +import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter +import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat +import kotlinx.serialization.Serializable +import obsidian.server.player.filter.Filter + +@Serializable +data class DistortionFilter( + val sinOffset: Float = 0f, + val sinScale: Float = 1f, + val cosOffset: Float = 0f, + val cosScale: Float = 1f, + val offset: Float = 0f, + val scale: Float = 1f +) : Filter { + override val enabled: Boolean + get() = + (Filter.isSet(sinOffset, 0f) && Filter.isSet(sinScale, 1f)) && + (Filter.isSet(cosOffset, 0f) && Filter.isSet(cosScale, 1f)) && + (Filter.isSet(offset, 0f) && Filter.isSet(scale, 1f)) + + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? { + return DistortionPcmAudioFilter(downstream, format.channelCount) + .setSinOffset(sinOffset) + .setSinScale(sinScale) + .setCosOffset(cosOffset) + .setCosScale(cosScale) + .setOffset(offset) + .setScale(scale) + } +} diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt index 66ea6b1..fe072e6 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt @@ -19,14 +19,15 @@ package obsidian.server.player.filter.impl import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter import com.sedmelluq.discord.lavaplayer.filter.equalizer.Equalizer import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat -import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter -import obsidian.server.player.filter.Filter.Companion.isSet +import kotlinx.serialization.Serializable @Serializable data class EqualizerFilter(val bands: List) : Filter { override val enabled: Boolean - get() = bands.any { isSet(it.gain, 0f) } + get() = bands.any { + Filter.isSet(it.gain, 0f) + } override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? { if (!Equalizer.isCompatible(format)) { diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/KaraokeFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/KaraokeFilter.kt index 029216f..7217c64 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/KaraokeFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/KaraokeFilter.kt @@ -43,4 +43,4 @@ data class KaraokeFilter( .setMonoLevel(monoLevel) .setFilterBand(filterBand) .setFilterWidth(filterWidth) -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt index d05b6c7..53291f7 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt @@ -23,13 +23,11 @@ import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter @Serializable -data class LowPassFilter( - val smoothing: Float = 20f -) : Filter { +data class LowPassFilter(val smoothing: Float = 20f) : Filter { override val enabled: Boolean get() = Filter.isSet(smoothing, 20f) override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = LowPassPcmAudioFilter(downstream, format.channelCount, 0) .setSmoothing(smoothing) -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt index 55aa255..9318b20 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt @@ -34,4 +34,4 @@ data class RotationFilter( override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = RotationPcmAudioFilter(downstream, format.sampleRate) .setRotationSpeed(rotationHz.toDouble() /* seems like a bad idea idk. */) -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt index fa7cbd9..af1e9a2 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt @@ -19,19 +19,23 @@ package obsidian.server.player.filter.impl import com.github.natanbc.lavadsp.timescale.TimescalePcmAudioFilter import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter import obsidian.server.player.filter.Filter.Companion.isSet -import obsidian.server.util.NativeUtil @Serializable data class TimescaleFilter( val pitch: Float = 1f, + @SerialName("pitch_octaves") val pitchOctaves: Float? = null, + @SerialName("pitch_semi_tones") val pitchSemiTones: Float? = null, val speed: Float = 1f, + @SerialName("speed_change") val speedChange: Float? = null, val rate: Float = 1f, + @SerialName("rate_change") val rateChange: Float? = null, ) : Filter { override val enabled: Boolean @@ -56,25 +60,25 @@ data class TimescaleFilter( if (pitchOctaves != null) { require(!isSet(pitch, 1.0F) && pitchSemiTones == null) { - "'pitchOctaves' cannot be used in conjunction with 'pitch' and 'pitchSemiTones'" + "'pitch_octaves' cannot be used in conjunction with 'pitch' and 'pitch_semi_tones'" } } if (pitchSemiTones != null) { require(!isSet(pitch, 1.0F) && pitchOctaves == null) { - "'pitchOctaves' cannot be used in conjunction with 'pitch' and 'pitchSemiTones'" + "'pitch_semi_tones' cannot be used in conjunction with 'pitch' and 'pitch_octaves'" } } if (speedChange != null) { require(!isSet(speed, 1.0F)) { - "'speedChange' cannot be used in conjunction with 'speed'" + "'speed_change' cannot be used in conjunction with 'speed'" } } if (rateChange != null) { require(!isSet(rate, 1.0F)) { - "'rateChange' cannot be used in conjunction with 'rate'" + "'rate_change' cannot be used in conjunction with 'rate'" } } } @@ -84,12 +88,9 @@ data class TimescaleFilter( af.pitch = pitch.toDouble() af.rate = rate.toDouble() af.speed = speed.toDouble() - this.pitchOctaves?.let { af.setPitchOctaves(it.toDouble()) } this.pitchSemiTones?.let { af.setPitchSemiTones(it.toDouble()) } this.speedChange?.let { af.setSpeedChange(it.toDouble()) } this.rateChange?.let { af.setRateChange(it.toDouble()) } } - } - diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VibratoFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VibratoFilter.kt index 82b5687..d4c0409 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VibratoFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VibratoFilter.kt @@ -48,4 +48,4 @@ data class VibratoFilter( companion object { private const val VIBRATO_FREQUENCY_MAX_HZ = 14f } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt index b477ec4..6fa8d0d 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt @@ -21,22 +21,18 @@ import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter -import obsidian.server.player.filter.Filter.Companion.isSet @Serializable -data class VolumeFilter( - val volume: Float -) : Filter { +data class VolumeFilter(val volume: Float) : Filter { override val enabled: Boolean - get() = isSet(volume, 1f) + get() = Filter.isSet(volume, 1f) init { require(volume in 0.0..5.0) { - "'volume' must be >= 0 and <= 5." + "'volume' must be 0 <= x <= 5" } } - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = - VolumePcmAudioFilter(downstream) - .setVolume(volume) -} \ No newline at end of file + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? = + VolumePcmAudioFilter(downstream).setVolume(volume) +} From 754460946782425e3b8186a34c9db8711629fce4 Mon Sep 17 00:00:00 2001 From: melike2d Date: Mon, 3 May 2021 17:06:54 -0700 Subject: [PATCH 02/46] :boom: push current v2 code --- .gitignore | 11 +- Server/build.gradle | 96 -- Server/build.gradle.kts | 107 ++ .../kotlin/me/uport/knacl/NaClLowLevel.kt | 1193 ----------------- .../koe/codec/udpqueue/QueueManagerPool.kt | 76 ++ .../koe/codec/udpqueue/UdpQueueFramePoller.kt | 99 ++ .../udpqueue}/UdpQueueFramePollerFactory.kt | 27 +- .../main/kotlin/obsidian/bedrock/Bedrock.kt | 92 -- .../kotlin/obsidian/bedrock/BedrockClient.kt | 70 - .../src/main/kotlin/obsidian/bedrock/Event.kt | 66 - .../obsidian/bedrock/MediaConnection.kt | 189 --- .../kotlin/obsidian/bedrock/codec/Codec.kt | 93 -- .../obsidian/bedrock/codec/CodecType.kt | 26 - .../obsidian/bedrock/codec/OpusCodec.kt | 56 - .../codec/framePoller/AbstractFramePoller.kt | 46 - .../codec/framePoller/FramePollerFactory.kt | 27 - .../codec/framePoller/QueueManagerPool.kt | 85 -- .../framePoller/UdpQueueOpusFramePoller.kt | 114 -- .../obsidian/bedrock/crypto/EncryptionMode.kt | 41 - .../bedrock/crypto/PlainEncryptionMode.kt | 30 - .../crypto/XSalsa20Poly1305EncryptionMode.kt | 50 - .../XSalsa20Poly1305LiteEncryptionMode.kt | 57 - .../XSalsa20Poly1305SuffixEncryptionMode.kt | 50 - .../gateway/AbstractMediaGatewayConnection.kt | 227 ---- .../bedrock/gateway/GatewayVersion.kt | 35 - .../bedrock/gateway/MediaGatewayConnection.kt | 46 - .../gateway/MediaGatewayV4Connection.kt | 183 --- .../obsidian/bedrock/gateway/event/Command.kt | 151 --- .../obsidian/bedrock/gateway/event/Event.kt | 131 -- .../bedrock/handler/ConnectionHandler.kt | 46 - .../bedrock/handler/DiscordUDPConnection.kt | 146 -- .../bedrock/handler/HolepunchHandler.kt | 92 -- .../obsidian/bedrock/media/IntReference.kt | 37 - .../bedrock/media/MediaFrameProvider.kt | 58 - .../bedrock/media/OpusAudioFrameProvider.kt | 139 -- .../bedrock/util/NettyBootstrapFactory.kt | 40 - .../kotlin/obsidian/server/Application.kt | 166 +++ .../main/kotlin/obsidian/server/Obsidian.kt | 151 --- .../kotlin/obsidian/server/io/Handlers.kt | 101 ++ .../main/kotlin/obsidian/server/io/Magma.kt | 227 ++-- .../kotlin/obsidian/server/io/MagmaClient.kt | 402 +----- .../obsidian/server/io/MagmaCloseReason.kt | 26 - .../src/main/kotlin/obsidian/server/io/Op.kt | 69 - .../RoutePlanner.kt => routes/planner.kt} | 14 +- .../obsidian/server/io/routes/players.kt | 159 +++ .../Tracks.kt => routes/tracks.kt} | 89 +- .../io/ws/CloseReasons.kt} | 19 +- .../obsidian/server/io/{ => ws}/Dispatch.kt | 23 +- .../gateway/event => server/io/ws}/Op.kt | 56 +- .../obsidian/server/io/{ => ws}/Operation.kt | 45 +- .../io/{StatsBuilder.kt => ws/StatsTask.kt} | 53 +- .../obsidian/server/io/ws/WebSocketHandler.kt | 273 ++++ .../server/player/FrameLossTracker.kt | 24 +- ...bsidianPlayerManager.kt => ObsidianAPM.kt} | 36 +- .../server/player/{Link.kt => Player.kt} | 87 +- .../obsidian/server/player/PlayerEvents.kt | 99 -- .../obsidian/server/player/PlayerUpdates.kt | 66 +- .../server/player/TrackEndMarkerHandler.kt | 6 +- .../player/filter/impl/TimescaleFilter.kt | 1 + .../server/util/AuthorizationPipeline.kt | 54 + .../obsidian/server/util/ByteRingBuffer.kt | 4 +- .../kotlin/obsidian/server/util/CpuTimer.kt | 2 +- .../{bedrock => server}/util/Interval.kt | 4 +- .../kotlin/obsidian/server/util/KoeUtil.kt | 100 ++ .../kotlin/obsidian/server/util/NativeUtil.kt | 27 +- .../kotlin/obsidian/server/util/Obsidian.kt | 200 +++ .../obsidian/server/util/ThreadFactory.kt | 20 +- .../kotlin/obsidian/server/util/TrackUtil.kt | 41 +- .../util/VersionInfo.kt} | 22 +- .../server/util/config/LoggingConfig.kt | 34 - .../server/util/config/ObsidianConfig.kt | 196 --- .../server/{io => util}/search/AudioLoader.kt | 5 +- .../server/{io => util}/search/LoadResult.kt | 5 +- .../server/{io => util}/search/LoadType.kt | 4 +- Server/src/main/resources/logback.xml | 4 +- Server/src/main/resources/version.txt | 2 + build.gradle | 77 -- build.gradle.kts | 27 + .../build.gradle.kts | 12 +- .../src/main/kotlin/Compiler.kt | 16 +- buildSrc/src/main/kotlin/Dependencies.kt | 61 + .../src/main/kotlin/Project.kt | 11 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 83 files changed, 2041 insertions(+), 5113 deletions(-) delete mode 100644 Server/build.gradle create mode 100644 Server/build.gradle.kts delete mode 100644 Server/src/main/kotlin/me/uport/knacl/NaClLowLevel.kt create mode 100644 Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt create mode 100644 Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt rename Server/src/main/kotlin/{obsidian/bedrock/codec/framePoller => moe/kyokobot/koe/codec/udpqueue}/UdpQueueFramePollerFactory.kt (68%) delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/Bedrock.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/BedrockClient.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/Event.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/MediaConnection.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/codec/Codec.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/codec/CodecType.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/codec/OpusCodec.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/AbstractFramePoller.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/FramePollerFactory.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/QueueManagerPool.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/UdpQueueOpusFramePoller.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/crypto/EncryptionMode.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/crypto/PlainEncryptionMode.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305EncryptionMode.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305LiteEncryptionMode.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305SuffixEncryptionMode.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/gateway/AbstractMediaGatewayConnection.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/gateway/GatewayVersion.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayConnection.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayV4Connection.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/gateway/event/Command.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/gateway/event/Event.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/handler/ConnectionHandler.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/handler/DiscordUDPConnection.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/handler/HolepunchHandler.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/media/IntReference.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/media/MediaFrameProvider.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/media/OpusAudioFrameProvider.kt delete mode 100644 Server/src/main/kotlin/obsidian/bedrock/util/NettyBootstrapFactory.kt create mode 100644 Server/src/main/kotlin/obsidian/server/Application.kt delete mode 100644 Server/src/main/kotlin/obsidian/server/Obsidian.kt create mode 100644 Server/src/main/kotlin/obsidian/server/io/Handlers.kt delete mode 100644 Server/src/main/kotlin/obsidian/server/io/MagmaCloseReason.kt delete mode 100644 Server/src/main/kotlin/obsidian/server/io/Op.kt rename Server/src/main/kotlin/obsidian/server/io/{controllers/RoutePlanner.kt => routes/planner.kt} (87%) create mode 100644 Server/src/main/kotlin/obsidian/server/io/routes/players.kt rename Server/src/main/kotlin/obsidian/server/io/{controllers/Tracks.kt => routes/tracks.kt} (74%) rename Server/src/main/kotlin/obsidian/{bedrock/crypto/DefaultEncryptionModes.kt => server/io/ws/CloseReasons.kt} (55%) rename Server/src/main/kotlin/obsidian/server/io/{ => ws}/Dispatch.kt (90%) rename Server/src/main/kotlin/obsidian/{bedrock/gateway/event => server/io/ws}/Op.kt (60%) rename Server/src/main/kotlin/obsidian/server/io/{ => ws}/Operation.kt (82%) rename Server/src/main/kotlin/obsidian/server/io/{StatsBuilder.kt => ws/StatsTask.kt} (66%) create mode 100644 Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt rename Server/src/main/kotlin/obsidian/server/player/{ObsidianPlayerManager.kt => ObsidianAPM.kt} (82%) rename Server/src/main/kotlin/obsidian/server/player/{Link.kt => Player.kt} (57%) delete mode 100644 Server/src/main/kotlin/obsidian/server/player/PlayerEvents.kt create mode 100644 Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt rename Server/src/main/kotlin/obsidian/{bedrock => server}/util/Interval.kt (98%) create mode 100644 Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt create mode 100644 Server/src/main/kotlin/obsidian/server/util/Obsidian.kt rename Server/src/main/kotlin/obsidian/{bedrock/codec/framePoller/FramePoller.kt => server/util/VersionInfo.kt} (62%) delete mode 100644 Server/src/main/kotlin/obsidian/server/util/config/LoggingConfig.kt delete mode 100644 Server/src/main/kotlin/obsidian/server/util/config/ObsidianConfig.kt rename Server/src/main/kotlin/obsidian/server/{io => util}/search/AudioLoader.kt (98%) rename Server/src/main/kotlin/obsidian/server/{io => util}/search/LoadResult.kt (97%) rename Server/src/main/kotlin/obsidian/server/{io => util}/search/LoadType.kt (94%) create mode 100644 Server/src/main/resources/version.txt delete mode 100644 build.gradle create mode 100644 build.gradle.kts rename Server/src/main/kotlin/obsidian/bedrock/VoiceServerInfo.kt => buildSrc/build.gradle.kts (83%) rename Server/src/main/kotlin/obsidian/bedrock/util/RTPHeaderWriter.kt => buildSrc/src/main/kotlin/Compiler.kt (61%) create mode 100644 buildSrc/src/main/kotlin/Dependencies.kt rename Server/src/main/kotlin/obsidian/bedrock/gateway/SpeakingFlags.kt => buildSrc/src/main/kotlin/Project.kt (80%) diff --git a/.gitignore b/.gitignore index 3537dfb..229c211 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,11 @@ .idea/ .settings/ -/build/ -/logs/ -/test/ -/target/ -/Server/bin -/Server/build +build/ +bin/ +logs/ +test/ +target/ .obsidianrc *.iml diff --git a/Server/build.gradle b/Server/build.gradle deleted file mode 100644 index cf5b124..0000000 --- a/Server/build.gradle +++ /dev/null @@ -1,96 +0,0 @@ -apply plugin: "kotlin" -apply plugin: "kotlinx-serialization" -apply plugin: "application" -apply plugin: "com.github.johnrengelman.shadow" - -description = "A robust and performant audio sending node meant for Discord Bots." -mainClassName = "obsidian.server.Obsidian" -version "1.0.0" - -ext { - moduleName = "Server" -} - -dependencies { - /* kotlin shit */ - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" - - /* audio */ - implementation ("com.sedmelluq:lavaplayer:$lavaplayer_version") { - exclude group: "com.sedmelluq", module: "lavaplayer-natives" - } - - implementation("com.sedmelluq:lavaplayer-ext-youtube-rotator:$lavaplayer_ip_rotator_config") { - exclude group: "com.sedmelluq", module: "lavaplayer" - } - - // native library loading - implementation "com.github.natanbc:native-loader:$native_loader_version" - implementation "com.github.natanbc:lp-cross:$lpcross_version" - - // filters - implementation "com.github.natanbc:lavadsp:$lavadsp_version" - - /* logging */ - implementation "ch.qos.logback:logback-classic:$logback_version" - implementation "com.github.ajalt.mordant:mordant:$mordant_version" - - /* config */ - implementation "com.uchuhimo:konf-core:$konf_version" - implementation "com.uchuhimo:konf-yaml:$konf_version" - - /* serialization */ - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_json_version" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.1.0" - - /* netty */ - implementation "io.netty:netty-transport:$netty_version" - implementation "io.netty:netty-transport-native-epoll:$netty_version:linux-x86_64" - - /* ktor */ - - // Serialization - implementation "io.ktor:ktor-serialization:$ktor_version" - - // Server - implementation "io.ktor:ktor-locations:$ktor_version" - implementation "io.ktor:ktor-websockets:$ktor_version" - implementation "io.ktor:ktor-server-cio:$ktor_version" - implementation "io.ktor:ktor-server-core:$ktor_version" - - // Client - implementation "io.ktor:ktor-client-core:$ktor_version" - implementation "io.ktor:ktor-client-okhttp:$ktor_version" - implementation "io.ktor:ktor-client-websockets:$ktor_version" -} - -shadowJar { - archiveBaseName.set("Obsidian") - archiveClassifier.set("") - archiveVersion.set("") -} - -jar { - manifest { - attributes "Main-Class": mainClassName - } -} - -compileJava.options.encoding = "UTF-8" - -compileKotlin { - sourceCompatibility = JavaVersion.VERSION_13 - targetCompatibility = JavaVersion.VERSION_13 - - kotlinOptions { - jvmTarget = "11" - incremental = true - freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalStdlibApi" - freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi" - freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" - freeCompilerArgs += "-Xopt-in=io.ktor.locations.KtorExperimentalLocationsAPI" - freeCompilerArgs += "-Xinline-classes" - } -} - diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts new file mode 100644 index 0000000..30de894 --- /dev/null +++ b/Server/build.gradle.kts @@ -0,0 +1,107 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import java.io.ByteArrayOutputStream + +plugins { + application + id("com.github.johnrengelman.shadow") version Versions.shadow +} + +apply(plugin = "kotlin") +apply(plugin = "kotlinx-serialization") + +description = "A robust and performant audio sending node meant for Discord Bots." +version = "2.0.0" + +application { + mainClass.set(Project.mainClassName) +} + +repositories { + jcenter() +} + +dependencies { + /* kotlin */ + implementation(Dependencies.kotlin) + implementation(Dependencies.kotlinxCoroutines) + implementation(Dependencies.kotlinxCoroutinesJdk8) + implementation(Dependencies.kotlinxSerialization) + + /* ktor, server related */ + implementation(Dependencies.ktorServerCore) + implementation(Dependencies.ktorServerCio) + implementation(Dependencies.ktorLocations) + implementation(Dependencies.ktorWebSockets) + implementation(Dependencies.ktorSerialization) + + /* media library */ + implementation(Dependencies.koeCore) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + + /* */ + implementation(Dependencies.lavaplayer)/*{ + exclude(group = "com.sedmelluq", module = "lavaplayer-natives") + } */ + + implementation(Dependencies.lavaplayerIpRotator) { + exclude(group = "com.sedmelluq", module = "lavaplayer") + } + + /* audio filters */ + implementation(Dependencies.lavadsp) + + /* native libraries */ + implementation(Dependencies.nativeLoader) +// implementation(Dependencies.lpCross) + + /* logging */ + implementation(Dependencies.logback) + implementation(Dependencies.mordant) + + /* configuration */ + implementation(Dependencies.konfCore) + implementation(Dependencies.konfYaml) +} + +tasks.withType { + archiveBaseName.set("Obsidian") + archiveClassifier.set("") +} + +tasks.withType { + sourceCompatibility = Project.jvmTarget + targetCompatibility = Project.jvmTarget + + kotlinOptions { + jvmTarget = Project.jvmTarget + incremental = true + freeCompilerArgs = listOf( + CompilerArgs.experimentalCoroutinesApi, + CompilerArgs.experimentalLocationsApi, + CompilerArgs.experimentalStdlibApi, + CompilerArgs.obsoleteCoroutinesApi + ) + } +} + +/* version info task */ +fun getVersionInfo(): String { + val gitVersion = ByteArrayOutputStream() + exec { + commandLine("git", "rev-parse", "--short", "HEAD") + standardOutput = gitVersion + } + + return "$version\n${gitVersion.toString().trim()}" +} + +tasks.create("writeVersion") { + val resourcePath = sourceSets["main"].resources.srcDirs.first() + if (!file(resourcePath).exists()) { + resourcePath.mkdirs() + } + + file("$resourcePath/version.txt").writeText(getVersionInfo()) +} diff --git a/Server/src/main/kotlin/me/uport/knacl/NaClLowLevel.kt b/Server/src/main/kotlin/me/uport/knacl/NaClLowLevel.kt deleted file mode 100644 index 779e89a..0000000 --- a/Server/src/main/kotlin/me/uport/knacl/NaClLowLevel.kt +++ /dev/null @@ -1,1193 +0,0 @@ -@file:Suppress("ObjectPropertyName", "FunctionName") - -package me.uport.knacl - -import java.security.SecureRandom -import kotlin.experimental.and -import kotlin.experimental.or -import kotlin.experimental.xor -import kotlin.math.floor -import kotlin.math.roundToLong - -/** - * This is a port of the TweetNaCl library - * Ported from the original C by Mircea Nistor - * - * **DISCLAIMER: - * This port is not complete and has not gone through a complete audit. - * Use at your own risk.** - */ -object NaClLowLevel { - - private val _0: ByteArray = ByteArray(16) { - 0 - } - - private val _9: ByteArray = ByteArray(32).apply { - this[0] = 9 - } - - private val gf0: LongArray = LongArray(16) { - 0 - } - - private val gf1: LongArray = LongArray(16).apply { - this[0] = 1 - } - - private val _121665: LongArray = longArrayOf(0xDB41, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - - private val D: LongArray = longArrayOf( - 0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, - 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203 - ) - - private val D2: LongArray = longArrayOf( - 0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, - 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406 - ) - - private val X: LongArray = longArrayOf( - 0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, - 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169 - ) - - private val Y: LongArray = longArrayOf( - 0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, - 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666 - ) - - private val I: LongArray = longArrayOf( - 0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, - 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83 - ) - - private fun L32(x: Int, c: Int): Int = ((x shl c) or (x ushr (32 - c))) - - private fun randomBytes(x: ByteArray, size: Int) { - require(x.size >= size) { - "array must be of size>=`$size` but it is of size=${x.size}" - } - - SecureRandom().nextBytes(x) - } - - private fun ld32(x: ByteArray, off: Int = 0): Int { - var u: Int = x[off + 3].toInt() and 0xff - - u = u shl 8 or (x[off + 2].toInt() and 0xff) - u = u shl 8 or (x[off + 1].toInt() and 0xff) - - return u shl 8 or (x[off + 0].toInt() and 0xff) - } - - private fun dl64(x: ByteArray, xi: Int): Long { - require(x.size >= 8 + xi) { - "Array must have at least 8 elements for `Byte`s to `Long` conversion" - } - - var u: Long = 0 - for (i in 0 until 8) { - u = (u shl 8) or x[i + xi].toLong() - } - - return u - } - - private fun st32(x: ByteArray, off: Int = 0, u: Int) { - require(x.size >= 4 + off) { - "`x` output array is too small to fit 4 bytes starting from $off" - } - - var uu = u - for (i in 0 until 4) { - x[i + off] = uu.toByte() - uu = uu shr 8 - } - } - - private fun ts64(x: ByteArray, xi: Int = 0, u: Long) { - var uu = u - for (i in 7 downTo 0) { - x[i + xi] = (uu and 0xff).toByte() - uu = uu shr 8 - } - } - - private fun vn(x: ByteArray, xi: Int = 0, y: ByteArray, yi: Int, n: Int): Int { - var d = 0 - for (i in 0 until n) { - d = d or (0xff and (x[i + xi] xor y[i + yi]).toInt()) - } - - return ((1 and ((d - 1) shr 8)) - 1) - } - - private fun crypto_verify_16(x: ByteArray, xi: Int = 0, y: ByteArray, yi: Int = 0): Int { - return vn(x, xi, y, yi, 16) - } - - private fun crypto_verify_32(x: ByteArray, xi: Int = 0, y: ByteArray, yi: Int = 0): Int { - return vn(x, xi, y, yi, 32) - } - - private fun core(outArr: ByteArray, inArr: ByteArray, k: ByteArray, c: ByteArray, h: Int) { - val w = IntArray(16) - val x = IntArray(16) - val y = IntArray(16) - val t = IntArray(4) - - for (i in 0 until 4) { - x[5 * i] = ld32(c, 4 * i) - x[1 + i] = ld32(k, 4 * i) - x[6 + i] = ld32(inArr, 4 * i) - x[11 + i] = ld32(k, 16 + 4 * i) - } - - for (i in 0 until 16) { - y[i] = x[i] - } - - for (i in 0 until 20) { - for (j in 0 until 4) { - for (m in 0 until 4) { - t[m] = x[(5 * j + 4 * m) % 16] - } - - t[1] = t[1] xor L32(t[0] + t[3], 7) - t[2] = t[2] xor L32(t[1] + t[0], 9) - t[3] = t[3] xor L32(t[2] + t[1], 13) - t[0] = t[0] xor L32(t[3] + t[2], 18) - - for (m in 0 until 4) { - w[4 * j + (j + m) % 4] = t[m] - } - } - - for (m in 0 until 16) { - x[m] = w[m] - } - } - - if (h != 0) { - for (i in 0 until 16) { - x[i] += y[i] - } - - for (i in 0 until 4) { - x[5 * i] -= ld32(c, 4 * i) - x[6 + i] -= ld32(inArr, 4 * i) - } - - for (i in 0 until 4) { - st32(outArr, 4 * i, x[5 * i]) - st32(outArr, 16 + 4 * i, x[6 + i]) - } - } else { - for (i in 0 until 16) { - st32(outArr, 4 * i, x[i] + y[i]) - } - } - } - - fun crypto_core_salsa20(outArr: ByteArray, inArr: ByteArray, k: ByteArray, c: ByteArray): Int { - core(outArr, inArr, k, c, 0) - - return 0 - } - - fun crypto_core_hsalsa20(outArr: ByteArray, inArr: ByteArray, k: ByteArray, c: ByteArray): Int { - core(outArr, inArr, k, c, 1) - - return 0 - } - - private val sigma: ByteArray = "expand 32-byte k".toByteArray(Charsets.UTF_8) - - fun crypto_stream_salsa20_xor( - c: ByteArray, - m: ByteArray?, - bIn: Long, - n: ByteArray, - nOff: Int = 0, - k: ByteArray - ): Int { - val z = ByteArray(16) { 0 } - val x = ByteArray(64) - - var u: Int - if (bIn == 0L) { - return 0 - } - - for (i in 0 until 8) { - z[i] = n[i + nOff] - } - - var b = bIn - var cOff = 0 - var mOff = 0 - while (b >= 64) { - crypto_core_salsa20(x, z, k, sigma) - for (i in 0 until 64) { - c[cOff + i] = (if (m != null) m[mOff + i] else 0) xor x[i] - } - - u = 1 - for (i in 8 until 16) { - u += 0xff and z[i].toInt() - z[i] = u.toByte() - u = u shr 8 - } - - b -= 64 - cOff += 64 - - if (m != null) { - mOff += 64 - } - } - - if (b != 0L) { - crypto_core_salsa20(x, z, k, sigma) - for (i in 0 until b.toInt()) { - c[cOff + i] = (if (m != null) m[mOff + i] else 0) xor x[i] - } - } - - return 0 - } - - fun crypto_stream_salsa20(c: ByteArray, d: Long, n: ByteArray, k: ByteArray, nStart: Int = 0): Int { - return crypto_stream_salsa20_xor(c, null, d, n, nStart, k) - } - - fun crypto_stream(c: ByteArray, d: Long, n: ByteArray, k: ByteArray): Int { - val s = ByteArray(32) - crypto_core_hsalsa20(s, n, k, sigma) - return crypto_stream_salsa20(c, d, n, s, 16) - } - - fun crypto_stream_xor(c: ByteArray, m: ByteArray, d: Long, n: ByteArray, k: ByteArray): Int { - val s = ByteArray(32) - crypto_core_hsalsa20(s, n, k, sigma) - - return crypto_stream_salsa20_xor(c, m, d, n, 16, s) - } - - private fun add1305(h: IntArray, c: IntArray) { - var u = 0 - - for (j in 0 until 17) { - u += h[j] + c[j] - h[j] = u and 255 - u = u shr 8 - } - } - - private val minusp: IntArray = intArrayOf(5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252) - - fun crypto_onetimeauth(out: ByteArray, outStart: Int, m: ByteArray, mStart: Int, n: Long, k: ByteArray): Int { - var mpos = mStart - var nn = n - val x = IntArray(17) - val r = IntArray(17) - val h = IntArray(17) - val c = IntArray(17) - val g = IntArray(17) - - for (j in 0 until 17) { - r[j] = 0; h[j] = 0 - } - - for (j in 0 until 16) { - r[j] = (0xff and k[j].toInt()) - } - - r[3] = r[3] and 15 - r[4] = r[4] and 252 - r[7] = r[7] and 15 - r[8] = r[8] and 252 - r[11] = r[11] and 15 - r[12] = r[12] and 252 - r[15] = r[15] and 15 - - while (nn > 0) { - for (j in 0 until 17) { - c[j] = 0 - } - - var jj = 0 - while (jj < 16 && jj < nn.toInt()) { - c[jj] = 0xff and m[mpos + jj].toInt() - jj++ - } - - c[jj] = 1 - mpos += jj - nn -= jj - - add1305(h, c) - for (i in 0 until 17) { - x[i] = 0 - for (j in 0 until 17) { - x[i] += h[j] * if (j <= i) r[i - j] else (320 * r[i + 17 - j]) - } - } - - for (i in 0 until 17) { - h[i] = x[i] - } - - var u = 0 - for (j in 0 until 16) { - u += h[j] - h[j] = u and 255 - u = u shr 8 - } - - u += h[16] - h[16] = u and 3 - u = (5 * (u shr 2)) - - for (j in 0 until 16) { - u += h[j] - h[j] = u and 255 - u = u shr 8 - } - - u += h[16] - h[16] = u - } - - for (j in 0 until 17) { - g[j] = h[j] - } - - add1305(h, minusp) - - val s = 0 - (h[16] shr 7) - for (j in 0 until 17) { - h[j] = h[j] xor (s and (g[j] xor h[j])) - } - - for (j in 0 until 16) { - c[j] = 0xff and k[j + 16].toInt() - } - - c[16] = 0 - - add1305(h, c) - for (j in 0 until 16) { - out[outStart + j] = h[j].toByte() - } - - return 0 - } - - private fun crypto_onetimeauth_verify(h: ByteArray, hi: Int, m: ByteArray, mi: Int, n: Long, k: ByteArray): Int { - val x = ByteArray(16) - crypto_onetimeauth(x, 0, m, mi, n, k) - - return crypto_verify_16(h, hi, x, 0) - } - - fun crypto_secretbox(c: ByteArray, m: ByteArray, d: Long, n: ByteArray, k: ByteArray): Int { - if (d < 32) { - return -1 - } - - crypto_stream_xor(c, m, d, n, k) - crypto_onetimeauth(c, 16, c, 32, d - 32, c) - -// for (i in 0 until 16) { -// c[i] = 0 -// } - - return 0 - } - - fun crypto_secretbox_open(m: ByteArray, c: ByteArray, d: Long, n: ByteArray, k: ByteArray): Int { - val x = ByteArray(32) - if (d < 32) { - return -1 - } - - crypto_stream(x, 32, n, k) - if (crypto_onetimeauth_verify(c, 16, c, 32, d - 32, x) != 0) { - return -1 - } - - crypto_stream_xor(m, c, d, n, k) - for (i in 0 until 32) { - m[i] = 0 - } - - return 0 - } - -/////////////////////////////////////////////////////////////////// -// curve 25519 -/////////////////////////////////////////////////////////////////// - - private fun set25519(r: LongArray, a: LongArray) { - for (i in 0 until 16) { - r[i] = a[i] - } - } - - private fun car25519(/*gf*/ o: LongArray, oOff: Int = 0) { - for (i in 0 until 16) { - o[oOff + i] += (1 shl 16).toLong() - - val c = o[oOff + i] shr 16 - o[oOff + (i + 1) * (if (i < 15) 1 else 0)] += c - 1 + 37 * (c - 1) * (if (i == 15) 1 else 0).toLong() - o[oOff + i] -= c shl 16 - } - } - - private fun sel25519(p: LongArray, q: LongArray, b: Int) { - var t: Long - val c = (b - 1).inv().toLong() - - for (i in 0 until 16) { - t = c and (p[i] xor q[i]) - p[i] = p[i] xor t - q[i] = q[i] xor t - } - } - - private fun pack25519(o: ByteArray, n: LongArray, nOff: Int = 0) { - var b: Int - val m = LongArray(16) - val t = LongArray(16) - - for (i in 0 until 16) { - t[i] = n[i + nOff] - } - - car25519(t) - car25519(t) - car25519(t) - - for (j in 0 until 2) { - m[0] = t[0] - 0xffed - - for (i in 1 until 15) { - m[i] = t[i] - 0xffff - ((m[i - 1] shr 16) and 1) - m[i - 1] = m[i - 1] and 0xffff - } - - m[15] = t[15] - 0x7fff - ((m[14] shr 16) and 1) - b = ((m[15] shr 16) and 1).toInt() - m[14] = m[14] and 0xffff - - sel25519(t, m, 1 - b) - } - - for (i in 0 until 16) { - o[2 * i] = t[i].toByte() - o[2 * i + 1] = (t[i] shr 8).toByte() - } - } - - private fun neq25519(a: LongArray, b: LongArray): Int { - val c = ByteArray(32) - pack25519(c, a) - - val d = ByteArray(32) - pack25519(d, b) - - return crypto_verify_32(c, 0, d, 0) - } - - private fun par25519(a: LongArray): Byte { - val d = ByteArray(32) - pack25519(d, a) - - return (d[0] and 1) - } - - private fun unpack25519(o: LongArray, n: ByteArray) { - for (i in 0 until 16) { - o[i] = (0xff and n[2 * i].toInt()) + (0xffL and n[2 * i + 1].toLong() shl 8) - } - - o[15] = o[15] and 0x7fff - } - - private fun A(o: LongArray, a: LongArray, b: LongArray) { - for (i in 0 until 16) { - o[i] = a[i] + b[i] - } - } - - private fun Z(o: LongArray, a: LongArray, b: LongArray) { - for (i in 0 until 16) { - o[i] = a[i] - b[i] - } - } - - private fun M(o: LongArray, a: LongArray, b: LongArray) { - val t = LongArray(31) - for (i in 0 until 16) { - for (j in 0 until 16) { - t[i + j] += a[i] * b[j] - } - } - - for (i in 0 until 15) { - t[i] += 38 * t[i + 16] - } - - for (i in 0 until 16) { - o[i] = t[i] - } - - car25519(o) - car25519(o) - } - - private fun S(o: LongArray, a: LongArray) = - M(o, a, a) - - private fun inv25519(o: LongArray, i: LongArray) { - val c = LongArray(16) - - for (a in 0 until 16) { - c[a] = i[a] - } - - for (a in 253 downTo 0) { - S(c, c) - if (a != 2 && a != 4) { - M(c, c, i) - } - } - - for (a in 0 until 16) { - o[a] = c[a] - } - } - - private fun pow2523(o: LongArray, i: LongArray) { - val c = LongArray(16) - - for (a in 0 until 16) { - c[a] = i[a] - } - - for (a in 250 downTo 0) { - S(c, c) - if (a != 1) { - M(c, c, i) - } - } - - for (a in 0 until 16) { - o[a] = c[a] - } - } - - fun crypto_scalarmult(q: ByteArray, n: ByteArray, p: ByteArray): Int { - val z = ByteArray(32) - val x = LongArray(80) - val a = LongArray(16) - val b = LongArray(16) - val c = LongArray(16) - val d = LongArray(16) - val e = LongArray(16) - val f = LongArray(16) - - for (i in 0 until 31) { - z[i] = n[i] - } - - z[31] = (n[31] and 127) or 64 - z[0] = z[0] and 248.toByte() - - unpack25519(x, p) - for (i in 0 until 16) { - b[i] = x[i] - d[i] = 0 - a[i] = 0 - c[i] = 0 - } - - a[0] = 1 - d[0] = 1 - - var r: Int - for (i in 254 downTo 0) { - r = ((z[i shr 3].toLong() shr (i and 7)) and 1).toInt() - sel25519(a, b, r) - sel25519(c, d, r) - A(e, a, c) - Z(a, a, c) - A(c, b, d) - Z(b, b, d) - S(d, e) - S(f, a) - M(a, c, a) - M(c, b, e) - A(e, a, c) - Z(a, a, c) - S(b, a) - Z(c, d, f) - M(a, c, _121665) - A(a, a, d) - M(c, c, a) - M(a, d, f) - M(d, b, x) - S(b, e) - sel25519(a, b, r) - sel25519(c, d, r) - } - - for (i in 0 until 16) { - x[i + 16] = a[i] - x[i + 32] = c[i] - x[i + 48] = b[i] - x[i + 64] = d[i] - } - - - val x32 = x.copyOfRange(32, x.size) - inv25519(x32, x32) - - val x16 = x.copyOfRange(16, x.size) - M(x16, x16, x32) - pack25519(q, x16) - - return 0 - } - - fun crypto_scalarmult_base(q: ByteArray, n: ByteArray): Int { - return crypto_scalarmult(q, n, _9) - } - - fun crypto_box_keypair(y: ByteArray, x: ByteArray): Int { - randomBytes(x, 32) - return crypto_scalarmult_base(y, x) - } - - fun crypto_box_beforenm(k: ByteArray, y: ByteArray, x: ByteArray): Int { - val s = ByteArray(32) - crypto_scalarmult(s, x, y) - return crypto_core_hsalsa20(k, _0, s, sigma) - } - - private fun crypto_box_afternm(c: ByteArray, m: ByteArray, d: Long, n: ByteArray, k: ByteArray): Int { - return crypto_secretbox(c, m, d, n, k) - } - - private fun crypto_box_open_afternm(m: ByteArray, c: ByteArray, d: Long, n: ByteArray, k: ByteArray): Int { - return crypto_secretbox_open(m, c, d, n, k) - } - - fun crypto_box(c: ByteArray, m: ByteArray, d: Long, n: ByteArray, y: ByteArray, x: ByteArray): Int { - val k = ByteArray(32) - crypto_box_beforenm(k, y, x) - return crypto_box_afternm(c, m, d, n, k) - } - - fun crypto_box_open(m: ByteArray, c: ByteArray, d: Long, n: ByteArray, y: ByteArray, x: ByteArray): Int { - val k = ByteArray(32) - crypto_box_beforenm(k, y, x) - return crypto_box_open_afternm(m, c, d, n, k) - } - - - ///////////////////////////////////////////////////// - // hash - ///////////////////////////////////////////////////// - - - private fun R(x: Long, c: Int): Long = ((x shr c) or (x shl (64 - c))) - - private fun Ch(x: Long, y: Long, z: Long): Long = (x and y) xor (x.inv() and z) - - private fun Maj(x: Long, y: Long, z: Long): Long = ((x and y) xor (x xor z) xor (y and z)) - - private fun Sigma0(x: Long): Long = (R(x, 28) xor R(x, 34) xor R(x, 39)) - - private fun Sigma1(x: Long): Long = (R(x, 14) xor R(x, 18) xor R(x, 41)) - - private fun sigma0(x: Long): Long = (R(x, 1) xor R(x, 8) xor (x shr 7)) - - private fun sigma1(x: Long): Long = (R(x, 19) xor R(x, 61) xor (x shr 6)) - - // private val K = ulongArrayOf( -// 0x428a2f98d728ae22UL, 0x7137449123ef65cdUL, 0xb5c0fbcfec4d3b2fUL, 0xe9b5dba58189dbbcUL, -// 0x3956c25bf348b538UL, 0x59f111f1b605d019UL, 0x923f82a4af194f9bUL, 0xab1c5ed5da6d8118UL, -// 0xd807aa98a3030242UL, 0x12835b0145706fbeUL, 0x243185be4ee4b28cUL, 0x550c7dc3d5ffb4e2UL, -// 0x72be5d74f27b896fUL, 0x80deb1fe3b1696b1UL, 0x9bdc06a725c71235UL, 0xc19bf174cf692694UL, -// 0xe49b69c19ef14ad2UL, 0xefbe4786384f25e3UL, 0x0fc19dc68b8cd5b5UL, 0x240ca1cc77ac9c65UL, -// 0x2de92c6f592b0275UL, 0x4a7484aa6ea6e483UL, 0x5cb0a9dcbd41fbd4UL, 0x76f988da831153b5UL, -// 0x983e5152ee66dfabUL, 0xa831c66d2db43210UL, 0xb00327c898fb213fUL, 0xbf597fc7beef0ee4UL, -// 0xc6e00bf33da88fc2UL, 0xd5a79147930aa725UL, 0x06ca6351e003826fUL, 0x142929670a0e6e70UL, -// 0x27b70a8546d22ffcUL, 0x2e1b21385c26c926UL, 0x4d2c6dfc5ac42aedUL, 0x53380d139d95b3dfUL, -// 0x650a73548baf63deUL, 0x766a0abb3c77b2a8UL, 0x81c2c92e47edaee6UL, 0x92722c851482353bUL, -// 0xa2bfe8a14cf10364UL, 0xa81a664bbc423001UL, 0xc24b8b70d0f89791UL, 0xc76c51a30654be30UL, -// 0xd192e819d6ef5218UL, 0xd69906245565a910UL, 0xf40e35855771202aUL, 0x106aa07032bbd1b8UL, -// 0x19a4c116b8d2d0c8UL, 0x1e376c085141ab53UL, 0x2748774cdf8eeb99UL, 0x34b0bcb5e19b48a8UL, -// 0x391c0cb3c5c95a63UL, 0x4ed8aa4ae3418acbUL, 0x5b9cca4f7763e373UL, 0x682e6ff3d6b2b8a3UL, -// 0x748f82ee5defb2fcUL, 0x78a5636f43172f60UL, 0x84c87814a1f0ab72UL, 0x8cc702081a6439ecUL, -// 0x90befffa23631e28UL, 0xa4506cebde82bde9UL, 0xbef9a3f7b2c67915UL, 0xc67178f2e372532bUL, -// 0xca273eceea26619cUL, 0xd186b8c721c0c207UL, 0xeada7dd6cde0eb1eUL, 0xf57d4f7fee6ed178UL, -// 0x06f067aa72176fbaUL, 0x0a637dc5a2c898a6UL, 0x113f9804bef90daeUL, 0x1b710b35131c471bUL, -// 0x28db77f523047d84UL, 0x32caab7b40c72493UL, 0x3c9ebe0a15c9bebcUL, 0x431d67c49c100d4cUL, -// 0x4cc5d4becb3e42b6UL, 0x597f299cfc657e2aUL, 0x5fcb6fab3ad6faecUL, 0x6c44198c4a475817UL -// ).asLongArray() - private val K = longArrayOf( - 4794697086780616226, 8158064640168781261, -5349999486874862801, -1606136188198331460, - 4131703408338449720, 6480981068601479193, -7908458776815382629, -6116909921290321640, - -2880145864133508542, 1334009975649890238, 2608012711638119052, 6128411473006802146, - 8268148722764581231, -9160688886553864527, -7215885187991268811, -4495734319001033068, - -1973867731355612462, -1171420211273849373, 1135362057144423861, 2597628984639134821, - 3308224258029322869, 5365058923640841347, 6679025012923562964, 8573033837759648693, - -7476448914759557205, -6327057829258317296, -5763719355590565569, -4658551843659510044, - -4116276920077217854, -3051310485924567259, 489312712824947311, 1452737877330783856, - 2861767655752347644, 3322285676063803686, 5560940570517711597, 5996557281743188959, - 7280758554555802590, 8532644243296465576, -9096487096722542874, -7894198246740708037, - -6719396339535248540, -6333637450476146687, -4446306890439682159, -4076793802049405392, - -3345356375505022440, -2983346525034927856, -860691631967231958, 1182934255886127544, - 1847814050463011016, 2177327727835720531, 2830643537854262169, 3796741975233480872, - 4115178125766777443, 5681478168544905931, 6601373596472566643, 7507060721942968483, - 8399075790359081724, 8693463985226723168, -8878714635349349518, -8302665154208450068, - -8016688836872298968, -6606660893046293015, -4685533653050689259, -4147400797238176981, - -3880063495543823972, -3348786107499101689, -1523767162380948706, -757361751448694408, - 500013540394364858, 748580250866718886, 1242879168328830382, 1977374033974150939, - 2944078676154940804, 3659926193048069267, 4368137639120453308, 4836135668995329356, - 5532061633213252278, 6448918945643986474, 6902733635092675308, 7801388544844847127 - ) - - private fun crypto_hashblocks(x: ByteArray, m: ByteArray, n: Long): Int { - val z = LongArray(8) - val b = LongArray(8) - val a = LongArray(8) - val w = LongArray(16) - - var t: Long - - for (i in 0 until 8) { - z[i] = dl64(x, 8 * i) - a[i] = dl64(x, 8 * i) - } - var nn = n - var mi = 0 - - while (nn >= 128) { - for (i in 0 until 16) { - w[i] = dl64(m, mi + 8 * i) - } - - for (i in 0 until 80) { - for (j in 0 until 8) { - b[j] = a[j] - } - t = a[7] + Sigma1(a[4]) + Ch(a[4], a[5], a[6]) + K[i] + w[i % 16] - b[7] = t + Sigma0(a[0]) + Maj(a[0], a[1], a[2]) - b[3] += t - for (j in 0 until 8) { - a[(j + 1) % 8] = b[j] - } - if (i % 16 == 15) { - for (j in 0 until 16) { - w[j] += w[(j + 9) % 16] + sigma0(w[(j + 1) % 16]) + sigma1(w[(j + 14) % 16]) - } - } - } - - - for (i in 0 until 8) { - a[i] += z[i]; z[i] = a[i]; } - - mi += 128 - nn -= 128 - } - - for (i in 0 until 8) ts64(x, 8 * i, z[i]) - - return n.toInt() - } - - private val iv = byteArrayOf( - 0x6a.toByte(), - 0x09.toByte(), - 0xe6.toByte(), - 0x67.toByte(), - 0xf3.toByte(), - 0xbc.toByte(), - 0xc9.toByte(), - 0x08.toByte(), - 0xbb.toByte(), - 0x67.toByte(), - 0xae.toByte(), - 0x85.toByte(), - 0x84.toByte(), - 0xca.toByte(), - 0xa7.toByte(), - 0x3b.toByte(), - 0x3c.toByte(), - 0x6e.toByte(), - 0xf3.toByte(), - 0x72.toByte(), - 0xfe.toByte(), - 0x94.toByte(), - 0xf8.toByte(), - 0x2b.toByte(), - 0xa5.toByte(), - 0x4f.toByte(), - 0xf5.toByte(), - 0x3a.toByte(), - 0x5f.toByte(), - 0x1d.toByte(), - 0x36.toByte(), - 0xf1.toByte(), - 0x51.toByte(), - 0x0e.toByte(), - 0x52.toByte(), - 0x7f.toByte(), - 0xad.toByte(), - 0xe6.toByte(), - 0x82.toByte(), - 0xd1.toByte(), - 0x9b.toByte(), - 0x05.toByte(), - 0x68.toByte(), - 0x8c.toByte(), - 0x2b.toByte(), - 0x3e.toByte(), - 0x6c.toByte(), - 0x1f.toByte(), - 0x1f.toByte(), - 0x83.toByte(), - 0xd9.toByte(), - 0xab.toByte(), - 0xfb.toByte(), - 0x41.toByte(), - 0xbd.toByte(), - 0x6b.toByte(), - 0x5b.toByte(), - 0xe0.toByte(), - 0xcd.toByte(), - 0x19.toByte(), - 0x13.toByte(), - 0x7e.toByte(), - 0x21.toByte(), - 0x79.toByte() - ) - - private fun crypto_hash(outArr: ByteArray, m: ByteArray, n: Long): Int { - require(outArr.size >= 64) { "outArr size(${outArr.size}) needs to be at least 64" } - val h = iv.copyOf() - val x = ByteArray(256) - val b: Long = n - - crypto_hashblocks(h, m, n) - - var mi = 0 - var nn = n.toInt() - - mi += nn - nn = nn and 127 - mi -= nn - - for (i in 0 until nn) { - x[i] = m[i + mi] - } - x[nn] = 128.toByte() - - nn = 256 - 128 * (if (nn < 112) 1 else 0) - x[nn - 9] = ((b shr 61) and 0xff).toByte() - ts64(x, nn - 8, b shl 3) - crypto_hashblocks(h, x, nn.toLong()) - - for (i in 0 until 64) outArr[i] = h[i] - - return 0 - } - - private fun add(p: Array, q: Array) { - val a = LongArray(16) - val b = LongArray(16) - val c = LongArray(16) - val d = LongArray(16) - val e = LongArray(16) - val f = LongArray(16) - val g = LongArray(16) - val h = LongArray(16) - val t = LongArray(16) - - Z(a, p[1], p[0]) - Z(t, q[1], q[0]) - M(a, a, t) - A(b, p[0], p[1]) - A(t, q[0], q[1]) - M(b, b, t) - M(c, p[3], q[3]) - M(c, c, D2) - M(d, p[2], q[2]) - A(d, d, d) - Z(e, b, a) - Z(f, d, c) - A(g, d, c) - A(h, b, a) - - M(p[0], e, f) - M(p[1], h, g) - M(p[2], g, f) - M(p[3], e, h) - } - - private fun cswap(p: Array, q: Array, b: Byte) { - for (i in 0 until 4) { - sel25519(p[i], q[i], b.toInt()) - } - } - - private fun pack(r: ByteArray, p: Array) { - val tx = LongArray(16) - val ty = LongArray(16) - val zi = LongArray(16) - - inv25519(zi, p[2]) - - M(tx, p[0], zi) - M(ty, p[1], zi) - pack25519(r, ty) - - r[31] = r[31] xor ((par25519(tx).toInt() shl 7) and 0xff).toByte() - } - - private fun scalarmult(p: Array, q: Array, s: ByteArray) { - set25519(p[0], gf0) - set25519(p[1], gf1) - set25519(p[2], gf1) - set25519(p[3], gf0) - for (i in 255 downTo 0) { - val b: Byte = ((s[i / 8].toInt() shr (i and 7)) and 1).toByte() - cswap(p, q, b) - add(q, p) - add(p, p) - cswap(p, q, b) - } - } - - private fun scalarbase(p: Array, s: ByteArray) { - val q = Array(4) { LongArray(16) } - set25519(q[0], X) - set25519(q[1], Y) - set25519(q[2], gf1) - M(q[3], X, Y) - scalarmult(p, q, s) - } - - //XXX: check array sizes (32, 64)? - private fun crypto_sign_keypair(pk: ByteArray, sk: ByteArray): Int { - val d = ByteArray(64) - val p = Array(4) { LongArray(16) } - - randomBytes(sk, 32) - crypto_hash(d, sk, 32) - d[0] = d[0] and 248.toByte() - d[31] = d[31] and 127 - d[31] = d[31] or 64 - - scalarbase(p, d) - pack(pk, p) - - for (i in 0 until 32) sk[32 + i] = pk[i] - return 0 - } - - private val L = longArrayOf( - 0xed, 0xd3, 0xf5, 0x5c, - 0x1a, 0x63, 0x12, 0x58, - 0xd6, 0x9c, 0xf7, 0xa2, - 0xde, 0xf9, 0xde, 0x14, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0x10 - ) - - private fun modL(r: ByteArray, x: LongArray, ri: Int = 0) { - var carry: Long - for (i in 63 downTo 32) { - carry = 0 - - for (j in (i - 32) until (i - 12)) { - x[j] += carry - 16 * x[i] * L[j - (i - 32)] - carry = floor((x[j].toDouble() + 128.0) / 256.0).roundToLong() - x[j] -= carry shl 8 - } - - ///XXX: check index arithmetic - x[i - 12 - 1] += carry - x[i] = 0 - } - - carry = 0 - for (j in 0 until 32) { - x[j] += carry - (x[31] shr 4) * L[j] - carry = x[j] shr 8 - x[j] = x[j] and 255 - } - - for (j in 0 until 32) { - x[j] -= carry * L[j] - } - - for (i in 0 until 32) { - x[i + 1] += x[i] shr 8 - r[i + ri] = (x[i] and 255).toByte() - } - } - - private fun reduce(r: ByteArray) { - val x = LongArray(64) - for (i in 0 until 64) { - x[i] = r[i].toLong() - r[i] = 0 - //xxx: check result - } - modL(r, x) - } - - private fun crypto_sign(sm: ByteArray, m: ByteArray, n: Long, sk: ByteArray): Int { - require(sm.size >= n + 64) { - "resulting array sm(size=${sm.size}) must be able to fit n+64 bytes (${n + 64})" - } - - val d = ByteArray(64) - val h = ByteArray(64) - val r = ByteArray(64) - val x = LongArray(64) - val p = Array(4) { LongArray(16) } - - crypto_hash(d, sk, 32) - d[0] = d[0] and 248.toByte() - d[31] = d[31] and 127 - d[31] = d[31] or 64 - - for (i in 0 until n.toInt()) { - sm[64 + i] = m[i] - } - for (i in 0 until 32) { - sm[32 + i] = d[32 + i] - } - - crypto_hash(r, sm.copyOfRange(32, sm.size), n + 32) - reduce(r) - scalarbase(p, r) - pack(sm, p) - - for (i in 0 until 32) { - sm[i + 32] = sk[i + 32] - } - crypto_hash(h, sm, n + 64) - reduce(h) - - for (i in 0 until 64) { - x[i] = 0 - } - for (i in 0 until 32) { - x[i] = r[i].toLong() - } - for (i in 0 until 32) for (j in 0 until 32) { - x[i + j] += (h[i] * d[j]).toLong() - } - modL(sm, x, 32) - - return 0 - } - - //check lengths r[4], p[32] - private fun unpackneg(r: Array, p: ByteArray): Int { - val t = LongArray(16) - val chk = LongArray(16) - val num = LongArray(16) - val den = LongArray(16) - val den2 = LongArray(16) - val den4 = LongArray(16) - val den6 = LongArray(16) - set25519(r[2], gf1) - unpack25519(r[1], p) - S(num, r[1]) - M(den, num, D) - Z(num, num, r[2]) - A(den, r[2], den) - - S(den2, den) - S(den4, den2) - M(den6, den4, den2) - M(t, den6, num) - M(t, t, den) - - pow2523(t, t) - M(t, t, num) - M(t, t, den) - M(t, t, den) - M(r[0], t, den) - - S(chk, r[0]) - M(chk, chk, den) - if (neq25519(chk, num) != 0) { - M(r[0], r[0], I) - } - - S(chk, r[0]) - M(chk, chk, den) - if (neq25519(chk, num) != 0) { - return -1 - } - - if (par25519(r[0]) == ((p[31].toInt() shr 7) and 0xff).toByte()) { - Z(r[0], gf0, r[0]) - } - - M(r[3], r[0], r[1]) - return 0 - } - - private fun crypto_sign_open(m: ByteArray, sm: ByteArray, n: Long, pk: ByteArray): Int { - val nn = (n - 64).toInt() - require(m.size >= nn) { "resulting array `m` size must be at least $nn but is ${m.size}" } - val t = ByteArray(32) - val h = ByteArray(64) - val p = Array(4) { LongArray(16) } - val q = Array(4) { LongArray(16) } - - if (n < 64) { - return -1 - } - - if (unpackneg(q, pk) != 0) { - return -1 - } - - for (i in 0 until n.toInt()) { - m[i] = sm[i] - } - for (i in 0 until 32) m[i + 32] = pk[i] - crypto_hash(h, m, n) - reduce(h) - scalarmult(p, q, h) - - scalarbase(q, sm.copyOfRange(32, sm.size)) - add(p, q) - pack(t, p) - - if (crypto_verify_32(sm, 0, t, 0) != 0) { - for (i in 0 until nn) { - m[i] = 0 - } - return -1 - } - - for (i in 0 until nn) m[i] = sm[i + 64] - return 0 - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt new file mode 100644 index 0000000..9d8274e --- /dev/null +++ b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package moe.kyokobot.koe.codec.udpqueue + +import com.sedmelluq.discord.lavaplayer.udpqueue.natives.UdpQueueManager +import moe.kyokobot.koe.codec.OpusCodec.FRAME_DURATION +import moe.kyokobot.koe.codec.udpqueue.UdpQueueFramePollerFactory.Companion.MAXIMUM_PACKET_SIZE +import obsidian.server.util.threadFactory +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong + +class QueueManagerPool(val size: Int, val bufferDuration: Int) { + + private var closed: Boolean = false + + private val threadFactory = + threadFactory("QueueManagerPool %d", priority = (Thread.NORM_PRIORITY + Thread.MAX_PRIORITY) / 2, daemon = true) + + private val queueKeySeq: AtomicLong = + AtomicLong() + + private val managers: List = + List(size) { + val queueManager = UdpQueueManager( + bufferDuration / FRAME_DURATION, + TimeUnit.MILLISECONDS.toNanos(FRAME_DURATION.toLong()), + MAXIMUM_PACKET_SIZE + ) + + threadFactory.newThread(queueManager::process) + queueManager + } + + fun close() { + if (closed) { + return + } + + closed = true + managers.forEach(UdpQueueManager::close) + } + + fun getNextWrapper(): UdpQueueWrapper { + val queueKey = queueKeySeq.getAndIncrement() + return getWrapperForKey(queueKey) + } + + fun getWrapperForKey(queueKey: Long): UdpQueueWrapper { + val manager = managers[(queueKey % managers.size.toLong()).toInt()] + return UdpQueueWrapper(queueKey, manager) + } + + class UdpQueueWrapper(val queueKey: Long, val manager: UdpQueueManager) { + val remainingCapacity: Int + get() = manager.getRemainingCapacity(queueKey) + + fun queuePacket(packet: ByteBuffer, addr: InetSocketAddress) = + this.manager.queuePacket(queueKey, packet, addr) + } +} diff --git a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt new file mode 100644 index 0000000..f6ad353 --- /dev/null +++ b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package moe.kyokobot.koe.codec.udpqueue + +import moe.kyokobot.koe.MediaConnection +import moe.kyokobot.koe.codec.AbstractFramePoller +import moe.kyokobot.koe.codec.OpusCodec +import moe.kyokobot.koe.internal.handler.DiscordUDPConnection +import moe.kyokobot.koe.media.IntReference +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit + +class UdpQueueFramePoller(connection: MediaConnection, private val manager: QueueManagerPool.UdpQueueWrapper) : + AbstractFramePoller(connection) { + + private var lastFrame: Long = 0 + private val timestamp: IntReference = IntReference() + + override fun start() { + check(!polling) { + "Polling has already started." + } + + polling = true + lastFrame = System.currentTimeMillis() + eventLoop.execute(::populateQueue) + } + + override fun stop() { + if (!polling) { + return + } + + polling = false + } + + private fun populateQueue() { + if (!polling) { + return + } + + val remaining = manager.remainingCapacity + val handler = connection.connectionHandler as DiscordUDPConnection + val sender = connection.audioSender + + for (i in 0 until remaining) { + if (sender != null && sender.canSendFrame(OpusCodec.INSTANCE)) { + val buf = allocator.buffer() + + /* retrieve a frame so we can compare */ + val start = buf.writerIndex() + sender.retrieve(OpusCodec.INSTANCE, buf, timestamp) + + /* create a packet */ + val packet = + handler.createPacket(OpusCodec.PAYLOAD_TYPE, timestamp.get(), buf, buf.writerIndex() - start, false) + + if (packet != null) { + manager.queuePacket(packet.nioBuffer(), handler.serverAddress as InetSocketAddress) + packet.release() + } + + buf.release() + } + } + + val frameDelay = 40 - (System.currentTimeMillis() - lastFrame) + if (frameDelay > 0) { + eventLoop.schedule(::loop, frameDelay, TimeUnit.MILLISECONDS) + } else { + loop() + } + } + + private fun loop() { + if (System.currentTimeMillis() < lastFrame + 60) { + lastFrame += 40 + } else { + lastFrame = System.currentTimeMillis() + } + + populateQueue() + } + +} diff --git a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/UdpQueueFramePollerFactory.kt b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePollerFactory.kt similarity index 68% rename from Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/UdpQueueFramePollerFactory.kt rename to Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePollerFactory.kt index 58a617a..021a020 100644 --- a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/UdpQueueFramePollerFactory.kt +++ b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePollerFactory.kt @@ -14,11 +14,13 @@ * limitations under the License. */ -package obsidian.bedrock.codec.framePoller +package moe.kyokobot.koe.codec.udpqueue -import obsidian.bedrock.MediaConnection -import obsidian.bedrock.codec.Codec -import obsidian.bedrock.codec.OpusCodec +import moe.kyokobot.koe.MediaConnection +import moe.kyokobot.koe.codec.Codec +import moe.kyokobot.koe.codec.FramePoller +import moe.kyokobot.koe.codec.FramePollerFactory +import moe.kyokobot.koe.codec.OpusCodec class UdpQueueFramePollerFactory( bufferDuration: Int = DEFAULT_BUFFER_DURATION, @@ -27,22 +29,15 @@ class UdpQueueFramePollerFactory( private val pool = QueueManagerPool(poolSize, bufferDuration) override fun createFramePoller(codec: Codec, connection: MediaConnection): FramePoller? { - if (OpusCodec.INSTANCE == codec) { - return UdpQueueOpusFramePoller(pool.getNextWrapper(), connection) + if (codec !is OpusCodec) { + return null } - return null + return UdpQueueFramePoller(connection, pool.getNextWrapper()) } companion object { - /** - * The default packet size used by Opus frames - */ const val MAXIMUM_PACKET_SIZE = 4096 - - /** - * The default frame buffer duration. - */ - const val DEFAULT_BUFFER_DURATION: Int = 400 + const val DEFAULT_BUFFER_DURATION = 400 } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/bedrock/Bedrock.kt b/Server/src/main/kotlin/obsidian/bedrock/Bedrock.kt deleted file mode 100644 index bd4c996..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/Bedrock.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock - -import com.uchuhimo.konf.ConfigSpec -import io.netty.buffer.ByteBufAllocator -import io.netty.buffer.PooledByteBufAllocator -import io.netty.buffer.UnpooledByteBufAllocator -import io.netty.channel.EventLoopGroup -import io.netty.channel.epoll.EpollDatagramChannel -import io.netty.channel.epoll.EpollEventLoopGroup -import io.netty.channel.epoll.EpollSocketChannel -import io.netty.channel.nio.NioEventLoopGroup -import io.netty.channel.socket.DatagramChannel -import io.netty.channel.socket.SocketChannel -import io.netty.channel.socket.nio.NioDatagramChannel -import io.netty.channel.socket.nio.NioSocketChannel -import obsidian.bedrock.gateway.GatewayVersion -import obsidian.bedrock.codec.framePoller.FramePollerFactory -import obsidian.bedrock.codec.framePoller.UdpQueueFramePollerFactory -import obsidian.server.Obsidian.config -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -object Bedrock { - private val logger: Logger = LoggerFactory.getLogger(Bedrock::class.java) - private val epollAvailable: Boolean by lazy { config[Config.UseEpoll] } - - val highPacketPriority: Boolean - get() = config[Config.HighPacketPriority] - - /** - * The [FramePollerFactory] to use. - */ - val framePollerFactory: FramePollerFactory = UdpQueueFramePollerFactory() - - /** - * The netty [ByteBufAllocator] to use when sending audio frames. - */ - val byteBufAllocator: ByteBufAllocator by lazy { - when (val allocator = config[Config.Allocator]) { - "pooled", "default" -> PooledByteBufAllocator.DEFAULT - "netty" -> ByteBufAllocator.DEFAULT - "unpooled" -> UnpooledByteBufAllocator.DEFAULT - else -> { - logger.warn("Invalid byte buf allocator '$allocator', defaulting to the 'pooled' byte buf allocator.") - PooledByteBufAllocator.DEFAULT - } - } - } - - /** - * The netty [EventLoopGroup] being used. - * Defaults to [NioEventLoopGroup] if Epoll isn't available. - */ - val eventLoopGroup: EventLoopGroup by lazy { - if (epollAvailable) EpollEventLoopGroup() else NioEventLoopGroup() - } - - /** - * The class of the netty [DatagramChannel] being used. - * Defaults to [NioDatagramChannel] if Epoll isn't available - */ - val datagramChannelClass: Class by lazy { - if (epollAvailable) EpollDatagramChannel::class.java else NioDatagramChannel::class.java - } - - /** - * The [GatewayVersion] to use. - */ - val gatewayVersion = GatewayVersion.V4 - - object Config : ConfigSpec("bedrock") { - val UseEpoll by optional(true, "use-epoll") - val Allocator by optional("pooled") - val HighPacketPriority by optional(true, "high-packet-priority") - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/BedrockClient.kt b/Server/src/main/kotlin/obsidian/bedrock/BedrockClient.kt deleted file mode 100644 index 96170dd..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/BedrockClient.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock - -import java.util.concurrent.ConcurrentHashMap - -class BedrockClient(val clientId: Long) { - /** - * All media connections that are currently being handled. - */ - private val connections = ConcurrentHashMap() - - /** - * Creates a new media connection for the provided guild id. - * - * @param guildId The guild id. - */ - fun createConnection(guildId: Long): MediaConnection = - connections.computeIfAbsent(guildId) { MediaConnection(this, guildId) } - - /** - * Get the MediaConnection for the provided guild id. - * - * @param guildId - */ - fun getConnection(guildId: Long): MediaConnection? = - connections[guildId] - - /** - * Destroys the MediaConnection for the provided guild id. - * - * @param guildId - */ - suspend fun destroyConnection(guildId: Long) = - removeConnection(guildId)?.close() - - /** - * Removes the MediaConnection of the provided guild id. - * - * @param guildId - */ - fun removeConnection(guildId: Long): MediaConnection? = - connections.remove(guildId) - - /** - * Closes this BedrockClient. - */ - suspend fun close() { - if (!connections.isEmpty()) { - for ((id, conn) in connections) { - removeConnection(id) - conn.close(); - } - } - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/Event.kt b/Server/src/main/kotlin/obsidian/bedrock/Event.kt deleted file mode 100644 index f13e1cc..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/Event.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock - -import io.ktor.util.network.* -import obsidian.bedrock.gateway.event.ClientConnect - -interface Event { - /** - * Media connection - */ - val mediaConnection: MediaConnection - - /** - * Client that emitted this event - */ - val client: BedrockClient - get() = mediaConnection.bedrockClient - - /** - * ID of the guild - */ - val guildId: Long - get() = mediaConnection.id -} - -data class GatewayClosedEvent( - override val mediaConnection: MediaConnection, - val code: Short, - val reason: String? -) : Event - -data class GatewayReadyEvent( - override val mediaConnection: MediaConnection, - val ssrc: Int, - val target: NetworkAddress -) : Event - -data class HeartbeatSentEvent( - override val mediaConnection: MediaConnection, - val nonce: Long -) : Event - -data class HeartbeatAcknowledgedEvent( - override val mediaConnection: MediaConnection, - val nonce: Long -) : Event - -data class UserConnectedEvent( - override val mediaConnection: MediaConnection, - val event: ClientConnect -) : Event diff --git a/Server/src/main/kotlin/obsidian/bedrock/MediaConnection.kt b/Server/src/main/kotlin/obsidian/bedrock/MediaConnection.kt deleted file mode 100644 index 00710b5..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/MediaConnection.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock - -import io.ktor.util.* -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* -import obsidian.bedrock.codec.Codec -import obsidian.bedrock.codec.OpusCodec -import obsidian.bedrock.codec.framePoller.FramePoller -import obsidian.bedrock.gateway.MediaGatewayConnection -import obsidian.bedrock.handler.ConnectionHandler -import obsidian.bedrock.media.MediaFrameProvider -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import kotlin.coroutines.CoroutineContext - -class MediaConnection( - val bedrockClient: BedrockClient, - val id: Long, - private val dispatcher: CoroutineDispatcher = Dispatchers.Default -) : CoroutineScope { - - /** - * The [ConnectionHandler]. - */ - var connectionHandler: ConnectionHandler? = null - - /** - * The [VoiceServerInfo] provided. - */ - var info: VoiceServerInfo? = null - - /** - * The [MediaFrameProvider]. - */ - var frameProvider: MediaFrameProvider? = null - set(value) { - if (field != null) { - field?.dispose() - } - - field = value - } - - /** - * Event flow - */ - val events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) - - override val coroutineContext: CoroutineContext - get() = dispatcher + SupervisorJob() - - /** - * The [MediaGatewayConnection]. - */ - private var mediaGatewayConnection: MediaGatewayConnection? = null - - /** - * The audio [Codec] to use when sending frames. - */ - private val audioCodec: Codec by lazy { OpusCodec.INSTANCE } - - /** - * The [FramePoller]. - */ - private val framePoller: FramePoller = Bedrock.framePollerFactory.createFramePoller(audioCodec, this)!! - - /** - * Connects to the Discord voice server described in [info] - * - * @param info The voice server info. - */ - suspend fun connect(info: VoiceServerInfo) { - if (mediaGatewayConnection != null) { - disconnect() - } - - val connection = Bedrock.gatewayVersion.createConnection(this, info) - mediaGatewayConnection = connection - connection.start() - } - - /** - * Disconnects from the voice server. - */ - suspend fun disconnect() { - logger.debug("Disconnecting...") - - stopFramePolling() - if (mediaGatewayConnection != null && mediaGatewayConnection?.open == true) { - mediaGatewayConnection?.close(1000, null) - mediaGatewayConnection = null - } - - if (connectionHandler != null) { - withContext(Dispatchers.IO) { - connectionHandler?.close() - } - - connectionHandler = null - } - } - - /** - * Starts the [FramePoller] for this media connection. - */ - suspend fun startFramePolling() { - if (this.framePoller.polling) { - return - } - - this.framePoller.start() - } - - /** - * Stops the [FramePoller] for this media connection - */ - fun stopFramePolling() { - if (!this.framePoller.polling) { - return - } - - this.framePoller.stop() - } - - /** - * Updates the speaking state with the provided [mask] - * - * @param mask The speaking mask to update with - */ - suspend fun updateSpeakingState(mask: Int) = - mediaGatewayConnection?.updateSpeaking(mask) - - /** - * Closes this media connection. - */ - suspend fun close() { - if (frameProvider != null) { - frameProvider?.dispose() - frameProvider = null - } - - disconnect() - coroutineContext.cancel() - bedrockClient.removeConnection(id) - } - - companion object { - val logger: Logger = LoggerFactory.getLogger(MediaConnection::class.java) - } -} - -/** - * Convenience method that calls [block] whenever event [T] is emitted on [MediaConnection.events] - * - * @param scope Scope to launch the job in - * @param block Block to call when [T] is emitted - * - * @return A [Job] that can be used to cancel any further processing of event [T] - */ -inline fun MediaConnection.on( - scope: CoroutineScope = this, - crossinline block: suspend T.() -> Unit -): Job { - return events.buffer(Channel.UNLIMITED) - .filterIsInstance() - .onEach { event -> - event - .runCatching { block() } - .onFailure { MediaConnection.logger.error(it) } - } - .launchIn(scope) -} diff --git a/Server/src/main/kotlin/obsidian/bedrock/codec/Codec.kt b/Server/src/main/kotlin/obsidian/bedrock/codec/Codec.kt deleted file mode 100644 index 2032a20..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/codec/Codec.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.codec - -import obsidian.bedrock.gateway.event.CodecDescription - -abstract class Codec { - /** - * The name of this Codec - */ - abstract val name: String - - /** - * The type of payload this codec provides. - */ - abstract val payloadType: Byte - - /** - * The priority of this codec. - */ - abstract val priority: Int - - /** - * The JSON description of this Codec. - */ - abstract val description: CodecDescription - - /** - * The type of this codec, can only be audio - */ - val codecType: CodecType = CodecType.AUDIO - - /** - * The type of rtx-payload this codec provides. - */ - val rtxPayloadType: Byte = 0 - - override operator fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - - if (other == null || javaClass != other.javaClass) { - return false - } - - return payloadType == (other as Codec).payloadType - } - - override fun hashCode(): Int { - var result = name.hashCode() - result = 31 * result + payloadType - result = 31 * result + priority - result = 31 * result + description.hashCode() - result = 31 * result + codecType.hashCode() - result = 31 * result + rtxPayloadType - - return result - } - - companion object { - /** - * List of all audio codecs available - */ - private val AUDIO_CODECS: List by lazy { - listOf(OpusCodec.INSTANCE) - } - - /** - * Gets audio codec description by name. - * - * @param name the codec name - * @return Codec instance or null if the codec is not found/supported by Bedrock - */ - fun getAudio(name: String): Codec? = AUDIO_CODECS.find { - it.name == name - } - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/codec/CodecType.kt b/Server/src/main/kotlin/obsidian/bedrock/codec/CodecType.kt deleted file mode 100644 index c8246ee..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/codec/CodecType.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.codec - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -enum class CodecType { - @SerialName("audio") - AUDIO -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/codec/OpusCodec.kt b/Server/src/main/kotlin/obsidian/bedrock/codec/OpusCodec.kt deleted file mode 100644 index 1a4c321..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/codec/OpusCodec.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.codec - -import obsidian.bedrock.gateway.event.CodecDescription - -class OpusCodec : Codec() { - override val name = "opus" - - override val priority = 1000 - - override val payloadType: Byte = PAYLOAD_TYPE - - override val description = CodecDescription( - name = name, - payloadType = payloadType, - priority = priority, - type = CodecType.AUDIO - ) - - companion object { - /** - * The payload type of the Opus codec. - */ - const val PAYLOAD_TYPE: Byte = 120 - - /** - * The frame duration for every Opus frame. - */ - const val FRAME_DURATION: Int = 20 - - /** - * Represents a Silence Frame within opus. - */ - val SILENCE_FRAME = byteArrayOf(0xF8.toByte(), 0xFF.toByte(), 0xFE.toByte()) - - /** - * A pre-defined instance of [OpusCodec] - */ - val INSTANCE: OpusCodec by lazy { OpusCodec() } - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/AbstractFramePoller.kt b/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/AbstractFramePoller.kt deleted file mode 100644 index 7b0d3bc..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/AbstractFramePoller.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.codec.framePoller - -import io.netty.buffer.ByteBufAllocator -import io.netty.channel.EventLoopGroup -import kotlinx.coroutines.ExecutorCoroutineDispatcher -import kotlinx.coroutines.asCoroutineDispatcher -import obsidian.bedrock.Bedrock -import obsidian.bedrock.MediaConnection - -abstract class AbstractFramePoller(protected val connection: MediaConnection) : FramePoller { - /** - * Whether we're polling or not. - */ - override var polling = false - - /** - * The [ByteBufAllocator] to use. - */ - protected val allocator: ByteBufAllocator = Bedrock.byteBufAllocator - - /** - * The [EventLoopGroup] being used. - */ - protected val eventLoop: EventLoopGroup = Bedrock.eventLoopGroup - - /** - * The [eventLoop] as a [ExecutorCoroutineDispatcher] - */ - protected val eventLoopDispatcher = eventLoop.asCoroutineDispatcher() -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/FramePollerFactory.kt b/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/FramePollerFactory.kt deleted file mode 100644 index a87a181..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/FramePollerFactory.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.codec.framePoller - -import obsidian.bedrock.MediaConnection -import obsidian.bedrock.codec.Codec - -interface FramePollerFactory { - /** - * Creates a frame poller using the provided [Codec] and [MediaConnection] - */ - fun createFramePoller(codec: Codec, connection: MediaConnection): FramePoller? -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/QueueManagerPool.kt b/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/QueueManagerPool.kt deleted file mode 100644 index 55c04b0..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/QueueManagerPool.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.codec.framePoller - -import com.sedmelluq.discord.lavaplayer.udpqueue.natives.UdpQueueManager -import obsidian.bedrock.codec.OpusCodec -import java.net.InetSocketAddress -import java.nio.ByteBuffer -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicLong -import kotlin.concurrent.thread - -class QueueManagerPool( - size: Int, - bufferDuration: Int -) { - private val queueKeySeq = AtomicLong() - private val managers: List - private var closed = false - - init { - require(size > 0) { - "Pool size must be higher or equal to 1." - } - - managers = List(size) { - val queueManager = UdpQueueManager( - bufferDuration / OpusCodec.FRAME_DURATION, - TimeUnit.MILLISECONDS.toNanos(OpusCodec.FRAME_DURATION.toLong()), - UdpQueueFramePollerFactory.MAXIMUM_PACKET_SIZE - ) - - /* create thread */ - thread( - name = "Queue Manager Pool $it", - isDaemon = true, - priority = (Thread.NORM_PRIORITY + Thread.MAX_PRIORITY) / 2, - block = queueManager::process - ) - - /* return queue manager */ - queueManager - } - } - - fun close() { - if (closed) { - return - } - - closed = true - managers.forEach(UdpQueueManager::close) - } - - fun getNextWrapper(): UdpQueueWrapper = - getWrapperForKey(this.queueKeySeq.getAndIncrement()) - - fun getWrapperForKey(queueKey: Long): UdpQueueWrapper = - UdpQueueWrapper( - managers[(queueKey % managers.size.toLong()).toInt()], - queueKey - ) - - class UdpQueueWrapper(val manager: UdpQueueManager, val queueKey: Long) { - val remainingCapacity: Int - get() = manager.getRemainingCapacity(this.queueKey) - - fun queuePacket(packet: ByteBuffer, address: InetSocketAddress): Boolean = - manager.queuePacket(this.queueKey, packet, address) - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/UdpQueueOpusFramePoller.kt b/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/UdpQueueOpusFramePoller.kt deleted file mode 100644 index dd9ac5b..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/UdpQueueOpusFramePoller.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.codec.framePoller - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.runBlocking -import obsidian.bedrock.MediaConnection -import obsidian.bedrock.codec.OpusCodec -import obsidian.bedrock.handler.DiscordUDPConnection -import obsidian.bedrock.media.IntReference -import java.net.InetSocketAddress -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit -import kotlin.coroutines.CoroutineContext - -class UdpQueueOpusFramePoller( - private val wrapper: QueueManagerPool.UdpQueueWrapper, - connection: MediaConnection -) : AbstractFramePoller(connection), CoroutineScope { - private val timestamp = IntReference() - private var lastFrame: Long = 0 - - override val coroutineContext: CoroutineContext - get() = eventLoopDispatcher + Job() - - override suspend fun start() { - if (polling) { - throw IllegalStateException("Polling already started!") - } - - polling = true - lastFrame = System.currentTimeMillis() - populateQueue() - } - - override fun stop() { - if (polling) { - polling = false - } - } - - private suspend fun populateQueue() { - if (!polling) { - return - } - - val handler = connection.connectionHandler as DiscordUDPConnection? - val frameProvider = connection.frameProvider - val codec = OpusCodec.INSTANCE - - for (i in 0 until wrapper.remainingCapacity) { - if (frameProvider != null && handler != null && frameProvider.canSendFrame(codec)) { - val buf = allocator.buffer() - val start = buf.writerIndex() - - frameProvider.retrieve(codec, buf, timestamp) - - val packet = - handler.createPacket(OpusCodec.PAYLOAD_TYPE, timestamp.get(), buf, buf.writerIndex() - start, false) - - if (packet != null) { - wrapper.queuePacket(packet.nioBuffer(), handler.serverAddress as InetSocketAddress) - packet.release() - } - - buf.release() - } - } - - val frameDelay = 40 - (System.currentTimeMillis() - lastFrame) - if (frameDelay > 0) { - eventLoop.schedule(frameDelay) { - runBlocking(coroutineContext) { loop() } - } - } else { - loop() - } - } - - private suspend fun loop() { - if (System.currentTimeMillis() < lastFrame + 60) { - lastFrame += 40 - } else { - lastFrame = System.currentTimeMillis() - } - - populateQueue() - } - - companion object { - fun ScheduledExecutorService.schedule( - delay: Long, - timeUnit: TimeUnit = TimeUnit.MILLISECONDS, - block: Runnable - ): ScheduledFuture<*> = - schedule(block, delay, timeUnit) - } -} diff --git a/Server/src/main/kotlin/obsidian/bedrock/crypto/EncryptionMode.kt b/Server/src/main/kotlin/obsidian/bedrock/crypto/EncryptionMode.kt deleted file mode 100644 index 59ba3d9..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/crypto/EncryptionMode.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.crypto - -import io.netty.buffer.ByteBuf - -interface EncryptionMode { - val name: String - - fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean - - companion object { - fun select(modes: List): String { - for (mode in modes) { - val impl = DefaultEncryptionModes.encryptionModes[mode] - if (impl != null) { - return mode - } - } - - throw UnsupportedEncryptionModeException("Cannot find a suitable encryption mode for this connection!") - } - - operator fun get(mode: String): EncryptionMode? = - DefaultEncryptionModes.encryptionModes[mode]?.invoke() - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/crypto/PlainEncryptionMode.kt b/Server/src/main/kotlin/obsidian/bedrock/crypto/PlainEncryptionMode.kt deleted file mode 100644 index 19e6220..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/crypto/PlainEncryptionMode.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.crypto - -import io.netty.buffer.ByteBuf - -class PlainEncryptionMode : EncryptionMode { - override val name: String = "plain" - - override fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean { - opus.readerIndex(start) - output.writeBytes(opus) - - return true - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305EncryptionMode.kt b/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305EncryptionMode.kt deleted file mode 100644 index 86c0a23..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305EncryptionMode.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.crypto - -import io.netty.buffer.ByteBuf -import me.uport.knacl.NaClLowLevel - -class XSalsa20Poly1305EncryptionMode : EncryptionMode { - override val name: String = "xsalsa20_poly1305" - - private val extendedNonce = ByteArray(24) - private val m = ByteArray(984) - private val c = ByteArray(984) - - override fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean { - for (i in c.indices) { - m[i] = 0 - c[i] = 0 - } - - for (i in 0 until start) { - m[(i + 32)] = opus.readByte() - } - - output.getBytes(0, extendedNonce, 0, 12) - if (NaClLowLevel.crypto_secretbox(c, m, (start + 32).toLong(), extendedNonce, secretKey) == 0) { - for (i in 0 until start + 16) { - output.writeByte(c[(i + 16)].toInt()) - } - - return true - } - - return false - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305LiteEncryptionMode.kt b/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305LiteEncryptionMode.kt deleted file mode 100644 index c94e0fd..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305LiteEncryptionMode.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.crypto - -import io.netty.buffer.ByteBuf -import me.uport.knacl.NaClLowLevel - -class XSalsa20Poly1305LiteEncryptionMode : EncryptionMode { - override val name: String = "xsalsa20_poly1305_lite" - - private val extendedNonce = ByteArray(24) - private val m = ByteArray(984) - private val c = ByteArray(984) - private var seq = 0x80000000 - - override fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean { - for (i in c.indices) { - m[i] = 0 - c[i] = 0 - } - - for (i in 0 until start) { - m[(i + 32)] = opus.readByte() - } - - val s = seq++ - extendedNonce[0] = (s and 0xff).toByte() - extendedNonce[1] = ((s shr 8) and 0xff).toByte() - extendedNonce[2] = ((s shr 16) and 0xff).toByte() - extendedNonce[3] = ((s shr 24) and 0xff).toByte() - - if (NaClLowLevel.crypto_secretbox(c, m, (start + 32).toLong(), extendedNonce, secretKey) == 0) { - for (i in 0 until start + 16) { - output.writeByte(c[(i + 16)].toInt()) - } - - output.writeIntLE(s.toInt()) - return true - } - - return false - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305SuffixEncryptionMode.kt b/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305SuffixEncryptionMode.kt deleted file mode 100644 index 52c8926..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/crypto/XSalsa20Poly1305SuffixEncryptionMode.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.crypto - -import io.netty.buffer.ByteBuf -import me.uport.knacl.NaClLowLevel -import java.util.concurrent.ThreadLocalRandom - -class XSalsa20Poly1305SuffixEncryptionMode : EncryptionMode { - override val name: String = "xsalsa20_poly1305_suffix" - - private val extendedNonce = ByteArray(24) - private val m = ByteArray(984) - private val c = ByteArray(984) - - override fun box(opus: ByteBuf, start: Int, output: ByteBuf, secretKey: ByteArray): Boolean { - for (i in c.indices) { - m[i] = 0 - c[i] = 0 - } - - for (i in 0 until start) m[i + 32] = opus.readByte() - - ThreadLocalRandom.current().nextBytes(extendedNonce) - if (NaClLowLevel.crypto_secretbox(c, m, (start + 32).toLong(), extendedNonce, secretKey) == 0) { - for (i in 0 until start + 16) { - output.writeByte(c[i + 16].toInt()) - } - - output.writeBytes(extendedNonce) - return true - } - - return false - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/gateway/AbstractMediaGatewayConnection.kt b/Server/src/main/kotlin/obsidian/bedrock/gateway/AbstractMediaGatewayConnection.kt deleted file mode 100644 index edf89c1..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/gateway/AbstractMediaGatewayConnection.kt +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.gateway - -import io.ktor.client.* -import io.ktor.client.engine.* -import io.ktor.client.engine.okhttp.* -import io.ktor.client.features.* -import io.ktor.client.features.websocket.* -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.http.cio.websocket.* -import io.ktor.util.* -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ClosedSendChannelException -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.flow.* -import obsidian.bedrock.MediaConnection -import obsidian.bedrock.VoiceServerInfo -import obsidian.bedrock.gateway.event.Command -import obsidian.bedrock.gateway.event.Event -import obsidian.server.io.MagmaClient.Companion.jsonParser -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.nio.charset.Charset -import java.util.concurrent.CancellationException -import kotlin.coroutines.CoroutineContext - -abstract class AbstractMediaGatewayConnection( - val mediaConnection: MediaConnection, - val voiceServerInfo: VoiceServerInfo, - version: Int -) : MediaGatewayConnection { - - /** - * Whether the websocket is open - */ - override var open = false - - /** - * Coroutine context - */ - override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + SupervisorJob() - - /** - * Broadcast channel - */ - private val channel = BroadcastChannel(1) - - /** - * Event flow - */ - protected val eventFlow: Flow - get() = channel.openSubscription().asFlow().buffer(Channel.UNLIMITED) - - /** - * Current websocket session - */ - private lateinit var socket: DefaultClientWebSocketSession - - /** - * Websocket url to use - */ - private val websocketUrl: String by lazy { - "wss://${voiceServerInfo.endpoint.replace(":80", "")}/?v=$version" - } - - /** - * Closes the connection to the voice server - * - * @param code The close code. - * @param reason The close reason. - */ - override suspend fun close(code: Short, reason: String?) { - channel.close() - } - - /** - * Creates a websocket connection to the voice server described in [voiceServerInfo] - */ - override suspend fun start() { - if (open) { - close(1000, null) - } - - open = true - while (open) { - try { - socket = client.webSocketSession { - url(websocketUrl) - } - } catch (ex: Exception) { - logger.error("WebSocket closed.", ex) - open = false - break - } - - identify() - handleIncoming() - - open = false - } - - if (::socket.isInitialized) { - socket.close() - } - - val reason = withTimeoutOrNull(1500) { - socket.closeReason.await() - } - - try { - onClose(reason?.code ?: -1, reason?.message ?: "unknown") - } catch (ex: Exception) { - logger.error(ex) - } - } - - /** - * Sends a JSON encoded string to the voice server. - * - * @param command The command to send - */ - suspend fun sendPayload(command: Command) { - if (open) { - try { - val json = jsonParser.encodeToString(Command.Companion, command) - logger.trace("VS <<< $json") - socket.send(json) - } catch (ex: Exception) { - logger.error(ex) - } - } - } - - /** - * Identifies this session - */ - protected abstract suspend fun identify() - - /** - * Called when the websocket connection has closed. - * - * @param code Close code - * @param reason Close reason - */ - protected abstract suspend fun onClose(code: Short, reason: String?) - - /** - * Used to handle specific events that are received - * - * @param block - */ - protected inline fun on(crossinline block: suspend T.() -> Unit) { - eventFlow.filterIsInstance() - .onEach { - try { - block(it) - } catch (ex: Exception) { - logger.error(ex) - } - }.launchIn(this) - } - - /** - * Handles incoming frames from the voice server - */ - private suspend fun handleIncoming() { - val session = this.socket - session.incoming.asFlow().buffer(Channel.UNLIMITED) - .collect { - when (it) { - is Frame.Text, is Frame.Binary -> handleFrame(it) - else -> { /* noop */ - } - } - } - } - - /** - * Handles an incoming frame - * - * @param frame Frame that was received - */ - private fun handleFrame(frame: Frame) { - val json = frame.data.toString(Charset.defaultCharset()) - - try { - logger.trace("VS >>> $json") - jsonParser.decodeFromString(Event.Companion, json)?.let { channel.offer(it) } - } catch (ex: ClosedSendChannelException) { - // fuck off - } catch (ex: Exception) { - logger.error(ex) - } - } - - companion object { - val logger: Logger = LoggerFactory.getLogger(AbstractMediaGatewayConnection::class.java) - val client = HttpClient(OkHttp) { install(WebSockets) } - - internal fun ReceiveChannel.asFlow() = flow { - try { - for (value in this@asFlow) emit(value) - } catch (ignore: CancellationException) { - //reading was stopped from somewhere else, ignore - } - } - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/gateway/GatewayVersion.kt b/Server/src/main/kotlin/obsidian/bedrock/gateway/GatewayVersion.kt deleted file mode 100644 index 2106571..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/gateway/GatewayVersion.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.gateway - -import obsidian.bedrock.MediaConnection -import obsidian.bedrock.VoiceServerInfo - -typealias MediaGatewayConnectionFactory = (MediaConnection, VoiceServerInfo) -> MediaGatewayConnection - -enum class GatewayVersion(private val factory: MediaGatewayConnectionFactory) { - V4({ a, b -> MediaGatewayV4Connection(a, b) }); - - /** - * Creates a new [MediaGatewayConnection] - * - * @param connection The media connection. - * @param voiceServerInfo The voice server information. - */ - fun createConnection(connection: MediaConnection, voiceServerInfo: VoiceServerInfo): MediaGatewayConnection = - factory.invoke(connection, voiceServerInfo) -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayConnection.kt b/Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayConnection.kt deleted file mode 100644 index fa6ebe4..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayConnection.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.gateway - -import kotlinx.coroutines.CoroutineScope - -interface MediaGatewayConnection : CoroutineScope { - /** - * Whether the gateway connection is opened. - */ - val open: Boolean - - /** - * Starts connecting to the gateway. - */ - suspend fun start() - - /** - * Closes the gateway connection. - * - * @param code The close code. - * @param reason The close reason. - */ - suspend fun close(code: Short, reason: String?) - - /** - * Updates the speaking state of the Client. - * - * @param mask The speaking mask. - */ - suspend fun updateSpeaking(mask: Int) -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayV4Connection.kt b/Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayV4Connection.kt deleted file mode 100644 index 555958b..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/gateway/MediaGatewayV4Connection.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.gateway - -import io.ktor.util.network.* -import kotlinx.coroutines.ObsoleteCoroutinesApi -import obsidian.bedrock.MediaConnection -import obsidian.bedrock.VoiceServerInfo -import obsidian.bedrock.codec.OpusCodec -import obsidian.bedrock.crypto.EncryptionMode -import obsidian.bedrock.* -import obsidian.bedrock.gateway.event.* -import obsidian.bedrock.handler.DiscordUDPConnection -import obsidian.bedrock.util.Interval -import java.util.* - -@ObsoleteCoroutinesApi -class MediaGatewayV4Connection( - mediaConnection: MediaConnection, - voiceServerInfo: VoiceServerInfo -) : AbstractMediaGatewayConnection(mediaConnection, voiceServerInfo, 4) { - private var ssrc = 0 - private var address: NetworkAddress? = null - private var rtcConnectionId: UUID? = null - private var interval: Interval = Interval() - - private lateinit var encryptionModes: List - - init { - on { - logger.debug("Received HELLO, heartbeat interval: $heartbeatInterval") - startHeartbeating(heartbeatInterval) - } - - on { - logger.debug("Received READY, ssrc: $ssrc") - - /* update state */ - this@MediaGatewayV4Connection.ssrc = ssrc - address = NetworkAddress(ip, port) - encryptionModes = modes - - /* emit event */ - mediaConnection.events.emit(GatewayReadyEvent(mediaConnection, ssrc, address!!)) - - /* select protocol */ - selectProtocol("udp") - } - - on { - mediaConnection.events.emit(HeartbeatAcknowledgedEvent(mediaConnection, nonce)) - } - - on { - if (mediaConnection.connectionHandler != null) { - mediaConnection.connectionHandler?.handleSessionDescription(this) - } else { - logger.warn("Received session description before protocol selection? (connection id = $rtcConnectionId)") - } - } - - on { - mediaConnection.events.emit(UserConnectedEvent(mediaConnection, this)) - } - } - - override suspend fun close(code: Short, reason: String?) { - interval.stop() - super.close(code, reason) - } - - private suspend fun selectProtocol(protocol: String) { - val mode = EncryptionMode.select(encryptionModes) - logger.debug("Selected preferred encryption mode: $mode") - - rtcConnectionId = UUID.randomUUID() - logger.debug("Generated new connection id: $rtcConnectionId") - - when (protocol.toLowerCase()) { - "udp" -> { - val connection = DiscordUDPConnection(mediaConnection, address!!, ssrc) - val externalAddress = connection.connect() - - logger.debug("Connected, our external address is '$externalAddress'") - - sendPayload(SelectProtocol( - protocol = "udp", - codecs = SUPPORTED_CODECS, - connectionId = rtcConnectionId!!, - data = SelectProtocol.UDPInformation( - address = externalAddress.address.hostAddress, - port = externalAddress.port, - mode = mode - ) - )) - - sendPayload(Command.ClientConnect( - audioSsrc = ssrc, - videoSsrc = 0, - rtxSsrc = 0 - )) - - mediaConnection.connectionHandler = connection - logger.debug("Waiting for session description...") - } - - else -> throw IllegalArgumentException("Protocol \"$protocol\" is not supported by Bedrock.") - } - } - - override suspend fun identify() { - logger.debug("Identifying...") - - sendPayload(Identify( - token = voiceServerInfo.token, - guildId = mediaConnection.id, - userId = mediaConnection.bedrockClient.clientId, - sessionId = voiceServerInfo.sessionId - )) - } - - override suspend fun onClose(code: Short, reason: String?) { - if (interval.started) { - interval.stop() - } - - val event = GatewayClosedEvent(mediaConnection, code, reason) - mediaConnection.events.emit(event) - } - - /** - * Updates the speaking state of the Client. - * - * @param mask The speaking mask. - */ - override suspend fun updateSpeaking(mask: Int) { - sendPayload(Speaking( - speaking = mask, - delay = 0, - ssrc = ssrc - )) - } - - /** - * Starts the heartbeat ticker. - * - * @param delay Delay, in milliseconds, between heart-beats. - */ - @ObsoleteCoroutinesApi - private suspend fun startHeartbeating(delay: Double) { - interval.start(delay.toLong()) { - val nonce = System.currentTimeMillis() - - /* emit event */ - val event = HeartbeatSentEvent(mediaConnection, nonce) - mediaConnection.events.tryEmit(event) - - /* send payload */ - sendPayload(Heartbeat(nonce)) - } - } - - companion object { - /** - * All supported audio codecs. - */ - val SUPPORTED_CODECS = listOf(OpusCodec.INSTANCE.description) - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Command.kt b/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Command.kt deleted file mode 100644 index 13020f4..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Command.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.gateway.event - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonObject -import obsidian.bedrock.codec.CodecType -import java.util.* - -sealed class Command { - @Serializable - data class ClientConnect( - @SerialName("audio_ssrc") val audioSsrc: Int, - @SerialName("video_ssrc") val videoSsrc: Int, - @SerialName("rtx_ssrc") val rtxSsrc: Int, - ) : Command() - - companion object : SerializationStrategy { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Command") { - element("op", Op.descriptor) - element("d", JsonObject.serializer().descriptor) - } - - override fun serialize(encoder: Encoder, value: Command) { - val composite = encoder.beginStructure(descriptor) - when (value) { - is SelectProtocol -> { - composite.encodeSerializableElement(descriptor, 0, Op, Op.SelectProtocol) - composite.encodeSerializableElement(descriptor, 1, SelectProtocol.serializer(), value) - } - - is Heartbeat -> { - composite.encodeSerializableElement(descriptor, 0, Op, Op.Heartbeat) - composite.encodeSerializableElement(descriptor, 1, Heartbeat.serializer(), value) - } - - is ClientConnect -> { - composite.encodeSerializableElement(descriptor, 0, Op, Op.ClientConnect) - composite.encodeSerializableElement(descriptor, 1, ClientConnect.serializer(), value) - } - - is Identify -> { - composite.encodeSerializableElement(descriptor, 0, Op, Op.Identify) - composite.encodeSerializableElement(descriptor, 1, Identify.serializer(), value) - } - - is Speaking -> { - composite.encodeSerializableElement(descriptor, 0, Op, Op.Speaking) - composite.encodeSerializableElement(descriptor, 1, Speaking.serializer(), value) - } - - } - - composite.endStructure(descriptor) - } - } -} - -@Serializable -data class SelectProtocol( - val protocol: String, - val codecs: List, - @Serializable(with = UUIDSerializer::class) - @SerialName("rtc_connection_id") - val connectionId: UUID, - val data: UDPInformation -) : Command() { - @Serializable - data class UDPInformation( - val address: String, - val port: Int, - val mode: String - ) -} - -@Serializable -data class CodecDescription( - val name: String, - @SerialName("payload_type") - val payloadType: Byte, - val priority: Int, - val type: CodecType -) - -@Serializable -data class Heartbeat( - val nonce: Long -) : Command() { - companion object : SerializationStrategy { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("Heartbeat", PrimitiveKind.LONG) - - override fun serialize(encoder: Encoder, value: Heartbeat) { - encoder.encodeLong(value.nonce) - } - } -} - - -@Serializable -data class Identify( - val token: String, - @SerialName("server_id") - val guildId: Long, - @SerialName("user_id") - val userId: Long, - @SerialName("session_id") - val sessionId: String -) : Command() - -@Serializable -data class Speaking( - val speaking: Int, - val delay: Int, - val ssrc: Int -) : Command() - -object UUIDSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: UUID) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): UUID = - UUID.fromString(decoder.decodeString()) -} diff --git a/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Event.kt b/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Event.kt deleted file mode 100644 index f947579..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Event.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.gateway.event - -import kotlinx.serialization.* -import kotlinx.serialization.builtins.nullable -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import obsidian.server.io.Operation - -sealed class Event { - companion object : DeserializationStrategy { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Event") { - element("op", Op.descriptor) - element("d", JsonObject.serializer().descriptor, isOptional = true) - } - - @ExperimentalSerializationApi - override fun deserialize(decoder: Decoder): Event? { - var op: Op? = null - var data: Event? = null - - with(decoder.beginStructure(descriptor)) { - loop@ while (true) { - val idx = decodeElementIndex(descriptor) - fun decode(serializer: DeserializationStrategy) = - decodeSerializableElement(Operation.descriptor, idx, serializer) - - when (idx) { - CompositeDecoder.DECODE_DONE -> break@loop - - 0 -> - op = Op.deserialize(decoder) - - 1 -> data = - when (op) { - Op.Hello -> - decode(Hello.serializer()) - - Op.Ready -> - decode(Ready.serializer()) - - Op.HeartbeatAck -> - decode(HeartbeatAck.serializer()) - - Op.SessionDescription -> - decode(SessionDescription.serializer()) - - Op.ClientConnect -> - decode(ClientConnect.serializer()) - - else -> { - decodeNullableSerializableElement(Operation.descriptor, idx, JsonElement.serializer().nullable) - data - } - } - } - } - - endStructure(descriptor) - return data - } - } - } -} - -@Serializable -data class Hello( - @SerialName("heartbeat_interval") - val heartbeatInterval: Double -) : Event() - -@Serializable -data class Ready( - val ssrc: Int, - val ip: String, - val port: Int, - val modes: List -) : Event() - -@Serializable -data class HeartbeatAck(val nonce: Long) : Event() { - companion object : DeserializationStrategy { - override val descriptor: SerialDescriptor - get() = PrimitiveSerialDescriptor("HeartbeatAck", PrimitiveKind.LONG) - - override fun deserialize(decoder: Decoder): HeartbeatAck = - HeartbeatAck(decoder.decodeLong()) - } -} - -@Serializable -data class SessionDescription( - val mode: String, - @SerialName("audio_codec") - val audioCodec: String, - @SerialName("secret_key") - val secretKey: List -) : Event() - -@Serializable -data class ClientConnect( - @SerialName("user_id") - val userId: String, - @SerialName("audio_ssrc") - val audioSsrc: Int = 0, - @SerialName("video_ssrc") - val videoSsrc: Int = 0, - @SerialName("rtx_ssrc") - val rtxSsrc: Int = 0, -) : Event() diff --git a/Server/src/main/kotlin/obsidian/bedrock/handler/ConnectionHandler.kt b/Server/src/main/kotlin/obsidian/bedrock/handler/ConnectionHandler.kt deleted file mode 100644 index 0589be7..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/handler/ConnectionHandler.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.handler - -import io.ktor.util.network.* -import io.netty.buffer.ByteBuf -import obsidian.bedrock.gateway.event.SessionDescription -import java.io.Closeable - -/** - * This interface specifies Discord voice connection handler, allowing to implement other methods of establishing voice - * connections/transmitting audio packets eg. TCP or browser/WebRTC way via ICE instead of their minimalistic custom - * discovery protocol. - */ -interface ConnectionHandler : Closeable { - - /** - * Handles a session description - * - * @param data The session description data. - */ - suspend fun handleSessionDescription(data: SessionDescription) - - /** - * Connects to the Discord UDP Socket. - * - * @return Our external network address. - */ - suspend fun connect(): NetworkAddress - - suspend fun sendFrame(payloadType: Byte, timestamp: Int, data: ByteBuf, start: Int, extension: Boolean) -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/handler/DiscordUDPConnection.kt b/Server/src/main/kotlin/obsidian/bedrock/handler/DiscordUDPConnection.kt deleted file mode 100644 index 71a4cf2..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/handler/DiscordUDPConnection.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.handler - -import io.netty.buffer.ByteBuf -import io.netty.channel.ChannelInitializer -import io.netty.channel.socket.DatagramChannel -import io.netty.util.internal.ThreadLocalRandom -import kotlinx.coroutines.future.await -import obsidian.bedrock.Bedrock -import obsidian.bedrock.MediaConnection -import obsidian.bedrock.codec.Codec -import obsidian.bedrock.crypto.EncryptionMode -import obsidian.bedrock.gateway.event.SessionDescription -import obsidian.bedrock.util.NettyBootstrapFactory -import obsidian.bedrock.util.writeV2 -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.io.Closeable -import java.net.InetSocketAddress -import java.net.SocketAddress -import java.util.concurrent.CompletableFuture - -class DiscordUDPConnection( - private val connection: MediaConnection, - val serverAddress: SocketAddress, - val ssrc: Int -) : Closeable, ConnectionHandler { - - private var allocator = Bedrock.byteBufAllocator - private var bootstrap = NettyBootstrapFactory.createDatagram() - - private var encryptionMode: EncryptionMode? = null - private var channel: DatagramChannel? = null - private var secretKey: ByteArray? = null - - private var seq = ThreadLocalRandom.current().nextInt() and 0xffff - - override suspend fun connect(): InetSocketAddress { - logger.debug("Connecting to '$serverAddress'...") - - val future = CompletableFuture() - bootstrap.handler(Initializer(this, future)) - .connect(serverAddress) - .addListener { res -> - if (!res.isSuccess) { - future.completeExceptionally(res.cause()) - } - } - - return future.await() - } - - override fun close() { - if (channel != null && channel!!.isOpen) { - channel?.close() - } - } - - override suspend fun handleSessionDescription(data: SessionDescription) { - encryptionMode = EncryptionMode[data.mode] - - val audioCodec = Codec.getAudio(data.audioCodec) - if (audioCodec == null) { - logger.warn("Unsupported audio codec type: {}, no audio data will be polled", data.audioCodec) - } - - checkNotNull(encryptionMode) { - "Encryption mode selected by Discord is not supported by Bedrock or the " + - "protocol changed! Open an issue!" - } - - secretKey = ByteArray(data.secretKey.size) { idx -> - (data.secretKey[idx] and 0xff).toByte() - } - - connection.startFramePolling() - } - - override suspend fun sendFrame(payloadType: Byte, timestamp: Int, data: ByteBuf, start: Int, extension: Boolean) { - val buf = createPacket(payloadType, timestamp, data, start, extension) - if (buf != null) { - channel?.writeAndFlush(buf) - } - } - - fun createPacket(payloadType: Byte, timestamp: Int, data: ByteBuf, len: Int, extension: Boolean): ByteBuf? { - if (secretKey == null) { - return null - } - - val buf = allocator.buffer() - buf.clear() - - writeV2(buf, payloadType, nextSeq(), timestamp, ssrc, extension) - - if (encryptionMode!!.box(data, len, buf, secretKey!!)) { - return buf - } else { - logger.debug("Encryption failed!") - buf.release() - } - - return null - } - - private fun nextSeq(): Int { - if (seq + 1 > 0xffff) { - seq = 0 - } else { - seq++ - } - - return seq - } - - inner class Initializer constructor( - private val connection: DiscordUDPConnection, - private val future: CompletableFuture - ) : ChannelInitializer() { - override fun initChannel(datagramChannel: DatagramChannel) { - connection.channel = datagramChannel - - val handler = HolepunchHandler(future, connection.ssrc) - datagramChannel.pipeline().addFirst("handler", handler) - } - } - - companion object { - private val logger: Logger = LoggerFactory.getLogger(DiscordUDPConnection::class.java) - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/handler/HolepunchHandler.kt b/Server/src/main/kotlin/obsidian/bedrock/handler/HolepunchHandler.kt deleted file mode 100644 index 6ae62c7..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/handler/HolepunchHandler.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.handler - -import io.netty.buffer.ByteBuf -import io.netty.buffer.Unpooled -import io.netty.channel.ChannelHandlerContext -import io.netty.channel.SimpleChannelInboundHandler -import io.netty.channel.socket.DatagramPacket -import org.slf4j.LoggerFactory -import java.net.InetSocketAddress -import java.net.SocketTimeoutException -import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit - -class HolepunchHandler( - private val future: CompletableFuture?, - private val ssrc: Int = 0 -) : SimpleChannelInboundHandler() { - - private var tries = 0 - private var packet: DatagramPacket? = null - - override fun channelActive(ctx: ChannelHandlerContext) { - holepunch(ctx) - } - - override fun channelRead0(ctx: ChannelHandlerContext, packet: DatagramPacket) { - val buf: ByteBuf = packet.content() - if (!future!!.isDone) { - if (buf.readableBytes() != 74) return - - buf.skipBytes(8) - - val stringBuilder = StringBuilder() - var b: Byte - while (buf.readByte().also { b = it }.toInt() != 0) { - stringBuilder.append(b.toChar()) - } - - val ip = stringBuilder.toString() - val port: Int = buf.getUnsignedShort(72) - - ctx.pipeline().remove(this) - future.complete(InetSocketAddress(ip, port)) - } - } - - fun holepunch(ctx: ChannelHandlerContext) { - if (future!!.isDone) { - return - } - - if (tries++ > 10) { - logger.debug("Discovery failed.") - future.completeExceptionally(SocketTimeoutException("Failed to discover external UDP address.")) - return - } - - logger.debug("Holepunch [attempt {}/10, local ip: {}]", tries, ctx.channel().localAddress()) - if (packet == null) { - val buf = Unpooled.buffer(74) - buf.writeShort(1) - buf.writeShort(0x46) - buf.writeInt(ssrc) - buf.writerIndex(74) - packet = DatagramPacket(buf, ctx.channel().remoteAddress() as InetSocketAddress) - } - - packet!!.retain() - ctx.writeAndFlush(packet) - ctx.executor().schedule({ holepunch(ctx) }, 1, TimeUnit.SECONDS) - } - - companion object { - private val logger = LoggerFactory.getLogger(HolepunchHandler::class.java) - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/media/IntReference.kt b/Server/src/main/kotlin/obsidian/bedrock/media/IntReference.kt deleted file mode 100644 index 401b916..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/media/IntReference.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.media - -/** - * Mutable reference to an int value. Provides no atomicity guarantees - * and should not be shared between threads without external synchronization. - */ -class IntReference { - private var value = 0 - - fun get(): Int { - return value - } - - fun set(value: Int) { - this.value = value - } - - fun add(amount: Int) { - value += amount - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/media/MediaFrameProvider.kt b/Server/src/main/kotlin/obsidian/bedrock/media/MediaFrameProvider.kt deleted file mode 100644 index d120bbe..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/media/MediaFrameProvider.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.media - -import io.netty.buffer.ByteBuf -import obsidian.bedrock.codec.Codec - -/** - * Base interface for media frame providers. Note that Bedrock doesn't handle stuff such as speaking state, silent frames - * or etc., these are implemented by codec-specific frame provider classes. - * - * @see OpusAudioFrameProvider for Opus audio codec specific implementation that handles speaking state and etc. - */ -interface MediaFrameProvider { - /** - * Frame interval between polling attempts or sets the delay between polling attempts. - */ - var frameInterval: Int - - /** - * Called when this [MediaFrameProvider] should clean up it's event handlers and etc. - */ - fun dispose() - - /** - * @return If true, Bedrock will request media data for given [Codec] by calling [retrieve] method. - */ - fun canSendFrame(codec: Codec): Boolean - - /** - * 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. - * 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. - * - * 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. - * - * @param codec [Codec] type this handler was registered with. - * @param buf [ByteBuf] the buffer where the media data should be written to. - * @param timestamp [IntReference] reference to current frame timestamp, which must be updated with timestamp of written frame. - * - * @return If true, Bedrock will immediately attempt to poll a next frame, this is meant for video transmissions. - */ - suspend fun retrieve(codec: Codec?, buf: ByteBuf?, timestamp: IntReference?): Boolean -} - diff --git a/Server/src/main/kotlin/obsidian/bedrock/media/OpusAudioFrameProvider.kt b/Server/src/main/kotlin/obsidian/bedrock/media/OpusAudioFrameProvider.kt deleted file mode 100644 index b919a67..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/media/OpusAudioFrameProvider.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.media - -import io.netty.buffer.ByteBuf -import obsidian.bedrock.MediaConnection -import obsidian.bedrock.codec.Codec -import obsidian.bedrock.codec.OpusCodec -import obsidian.bedrock.UserConnectedEvent -import obsidian.bedrock.gateway.SpeakingFlags -import obsidian.bedrock.on - -abstract class OpusAudioFrameProvider(val connection: MediaConnection) : MediaFrameProvider { - override var frameInterval = OpusCodec.FRAME_DURATION - - private var speakingMask = SpeakingFlags.NORMAL - private var counter = 0 - private var lastProvide = false - private var lastSpeaking = false - private var lastFramePolled: Long = 0 - private var speaking = false - - private val userConnectedJob = connection.on { - if (speaking) { - connection.updateSpeakingState(speakingMask) - } - } - - override fun dispose() { - userConnectedJob.cancel() - } - - override fun canSendFrame(codec: Codec): Boolean { - if (codec.payloadType != OpusCodec.PAYLOAD_TYPE) { - return false - } - - if (counter > 0) { - return true - } - - val provide = canProvide() - if (lastProvide != provide) { - lastProvide = provide; - if (!provide) { - counter = SILENCE_FRAME_COUNT; - return true; - } - } - - return provide; - } - - override suspend fun retrieve(codec: Codec?, buf: ByteBuf?, timestamp: IntReference?): Boolean { - if (codec?.payloadType != OpusCodec.PAYLOAD_TYPE) { - return false - } - - if (counter > 0) { - counter-- - buf!!.writeBytes(OpusCodec.SILENCE_FRAME) - if (speaking) { - setSpeaking(false) - } - - timestamp!!.add(960) - return false - } - - val startIndex = buf!!.writerIndex() - retrieveOpusFrame(buf) - - val written = buf.writerIndex() != startIndex - if (written && !speaking) { - setSpeaking(true) - } - - if (!written) { - counter = SILENCE_FRAME_COUNT - } - - val now = System.currentTimeMillis() - val changeTalking = now - lastFramePolled > OpusCodec.FRAME_DURATION - - lastFramePolled = now - if (changeTalking) { - setSpeaking(written) - } - - timestamp!!.add(960) - return false - } - - private suspend fun setSpeaking(state: Boolean) { - speaking = state - if (speaking != lastSpeaking) { - lastSpeaking = state - connection.updateSpeakingState(if (state) speakingMask else 0) - } - } - - - /** - * Called every time Opus frame poller tries to retrieve an Opus audio frame. - * - * @return If this method returns true, Bedrock will attempt to retrieve an Opus audio frame. - */ - abstract fun canProvide(): Boolean - - /** - * If [canProvide] returns true, this method will attempt to retrieve an Opus audio frame. - * - * - * This method must not block, otherwise it might cause severe performance issues, due to event loop thread - * getting blocked, therefore it's recommended to load all data before or in parallel, not when Bedrock frame poller - * calls this method. If no data gets written, the frame won't be sent. - * - * @param targetBuffer the target [ByteBuf] audio data should be written to. - */ - abstract fun retrieveOpusFrame(targetBuffer: ByteBuf) - - companion object { - private const val SILENCE_FRAME_COUNT = 5 - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/bedrock/util/NettyBootstrapFactory.kt b/Server/src/main/kotlin/obsidian/bedrock/util/NettyBootstrapFactory.kt deleted file mode 100644 index 0516831..0000000 --- a/Server/src/main/kotlin/obsidian/bedrock/util/NettyBootstrapFactory.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.bedrock.util - -import io.netty.bootstrap.Bootstrap -import io.netty.channel.ChannelOption -import obsidian.bedrock.Bedrock - -object NettyBootstrapFactory { - /** - * Creates a Datagram [Bootstrap] - */ - fun createDatagram(): Bootstrap { - val bootstrap = Bootstrap() - .group(Bedrock.eventLoopGroup) - .channel(Bedrock.datagramChannelClass) - .option(ChannelOption.SO_REUSEADDR, true) - - if (Bedrock.highPacketPriority) { - // IPTOS_LOWDELAY | IPTOS_THROUGHPUT - bootstrap.option(ChannelOption.IP_TOS, 0x10 or 0x08) - } - - return bootstrap - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/server/Application.kt b/Server/src/main/kotlin/obsidian/server/Application.kt new file mode 100644 index 0000000..77c2a79 --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/Application.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server + +import com.github.natanbc.nativeloader.SystemNativeLibraryProperties +import com.github.natanbc.nativeloader.system.SystemType +import com.uchuhimo.konf.Config +import com.uchuhimo.konf.source.yaml +import io.ktor.application.* +import io.ktor.auth.* +import io.ktor.features.* +import io.ktor.http.HttpStatusCode.Companion.InternalServerError +import io.ktor.locations.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.serialization.* +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.ktor.websocket.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import obsidian.server.io.Magma.magma +import obsidian.server.player.ObsidianAPM +import obsidian.server.util.AuthorizationPipeline.obsidianProvider +import obsidian.server.util.Obsidian +import obsidian.server.util.VersionInfo +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.system.exitProcess + +object Application { + /** + * Configuration instance. + * + * @see Obsidian + */ + val config = Config { addSpec(Obsidian) } + .from.yaml.file("obsidian.yml", optional = true) + .from.env() + + /** + * Custom player manager instance. + */ + val players = ObsidianAPM() + + /** + * Logger + */ + val log: Logger = LoggerFactory.getLogger(Application::class.java) + + /** + * Json parser used by ktor and us. + */ + val json = Json { + isLenient = true + encodeDefaults = true + ignoreUnknownKeys = true + } + + @JvmStatic + fun main(args: Array) { + + /* native library loading lololol */ + try { + val type = SystemType.detect(SystemNativeLibraryProperties(null, "nativeloader.")) + + log.info("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") +// log.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}") + } catch (e: Exception) { + val message = + "Unable to load system info" + if (e is UnsatisfiedLinkError || e is RuntimeException && e.cause is UnsatisfiedLinkError) + ", this isn't an error" else "." + + log.warn(message, e) + } + + try { + log.info("Loading Native Libraries") +// NativeUtil.load() + } catch (ex: Exception) { + log.error("Fatal exception while loading native libraries.", ex) + exitProcess(1) + } + + val server = embeddedServer(CIO, host = config[Obsidian.Server.host], port = config[Obsidian.Server.port]) { + install(WebSockets) + + install(Locations) + + /* use the custom authentication provider */ + install(Authentication) { + obsidianProvider() + } + + /* install status pages. */ + install(StatusPages) { + exception { exc -> + val error = ExceptionResponse.Error( + className = exc::class.simpleName ?: "Throwable", + message = exc.message, + cause = exc.cause?.let { + ExceptionResponse.Error( + it.message, + className = it::class.simpleName ?: "Throwable" + ) + } + ) + + val message = ExceptionResponse(error, exc.stackTraceToString()) + call.respond(InternalServerError, message) + } + } + + /* append version headers. */ + install(DefaultHeaders) { + header("Obsidian-Version", VersionInfo.VERSION) + header("Obsidian-Version-Commit", VersionInfo.GIT_REVISION) + } + + /* use content negotiation for REST endpoints */ + install(ContentNegotiation) { + json(json) + } + + /* install routing */ + install(Routing) { + magma() + } + } + + server.start(wait = true) + shutdown() + } + + fun shutdown() { + + } +} + +@Serializable +data class ExceptionResponse( + val error: Error, + @SerialName("stack_trace") val stackTrace: String +) { + @Serializable + data class Error( + val message: String?, + val cause: Error? = null, + @SerialName("class_name") val className: String + ) +} diff --git a/Server/src/main/kotlin/obsidian/server/Obsidian.kt b/Server/src/main/kotlin/obsidian/server/Obsidian.kt deleted file mode 100644 index f0a9f49..0000000 --- a/Server/src/main/kotlin/obsidian/server/Obsidian.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.server - -import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger -import ch.qos.logback.classic.LoggerContext -import com.github.natanbc.nativeloader.NativeLibLoader -import com.github.natanbc.nativeloader.SystemNativeLibraryProperties -import com.github.natanbc.nativeloader.system.SystemType -import com.uchuhimo.konf.Config -import com.uchuhimo.konf.source.yaml -import io.ktor.application.* -import io.ktor.auth.* -import io.ktor.features.* -import io.ktor.http.* -import io.ktor.locations.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* -import io.ktor.serialization.* -import io.ktor.server.cio.* -import io.ktor.server.engine.* -import io.ktor.websocket.* -import kotlinx.coroutines.runBlocking -import obsidian.bedrock.Bedrock -import obsidian.server.io.Magma.Companion.magma -import obsidian.server.player.ObsidianPlayerManager -import obsidian.server.util.NativeUtil -import obsidian.server.util.config.LoggingConfig -import obsidian.server.util.config.ObsidianConfig -import org.slf4j.LoggerFactory -import kotlin.system.exitProcess - -object Obsidian { - /** - * Configuration - */ - val config = Config { - addSpec(ObsidianConfig) - addSpec(Bedrock.Config) - addSpec(LoggingConfig) - } - .from.yaml.file("obsidian.yml", true) - .from.env() - .from.systemProperties() - - /** - * Player manager - */ - val playerManager = ObsidianPlayerManager() - - /** - * Lol i just like comments - */ - private val logger = LoggerFactory.getLogger(Obsidian::class.java) - - @JvmStatic - fun main(args: Array) { - runBlocking { - /* setup logging */ - configureLogging() - - /* native library loading lololol */ - try { - val type = SystemType.detect(SystemNativeLibraryProperties(null, "nativeloader.")) - - logger.info("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") - logger.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}") - } catch (e: Exception) { - val message = - "Unable to load system info" + if (e is UnsatisfiedLinkError || e is RuntimeException && e.cause is UnsatisfiedLinkError) - ", this isn't an error" else "." - - logger.warn(message, e) - } - - try { - logger.info("Loading Native Libraries") - NativeUtil.load() - } catch (ex: Exception) { - logger.error("Fatal exception while loading native libraries.", ex) - exitProcess(1) - } - - /* setup server */ - val server = embeddedServer(CIO, host = config[ObsidianConfig.Host], port = config[ObsidianConfig.Port]) { - install(Locations) - - install(WebSockets) - - install(ContentNegotiation) { - json() - } - - install(Authentication) { - provider { - pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context -> - val authorization = call.request.authorization() - if (!ObsidianConfig.validateAuth(authorization)) { - val cause = - if (authorization == null) AuthenticationFailedCause.NoCredentials - else AuthenticationFailedCause.InvalidCredentials - - context.challenge("ObsidianAuth", cause) { - call.respond(HttpStatusCode.Unauthorized) - it.complete() - } - } - } - } - } - - routing { - magma.use(this) - } - } - - if (config[ObsidianConfig.Password].isEmpty()) { - logger.warn("No password has been configured, thus allowing no authorization for the websocket server and REST requests.") - } - - server.start(wait = true) - magma.shutdown() - } - } - - private fun configureLogging() { - val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext - - val rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME) as Logger - rootLogger.level = Level.toLevel(config[LoggingConfig.Level.Root], Level.INFO) - - val obsidianLogger = loggerContext.getLogger("obsidian") as Logger - obsidianLogger.level = Level.toLevel(config[LoggingConfig.Level.Obsidian], Level.INFO) - } -} diff --git a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt new file mode 100644 index 0000000..f9d6030 --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.io + +import com.sedmelluq.discord.lavaplayer.track.TrackMarker +import moe.kyokobot.koe.VoiceServerInfo +import obsidian.server.player.TrackEndMarkerHandler +import obsidian.server.player.filter.Filters +import obsidian.server.util.TrackUtil +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +object Handlers { + + val log: Logger = LoggerFactory.getLogger(Handlers::class.java) + + fun submitVoiceServer(client: MagmaClient, guildId: Long, vsi: VoiceServerInfo) { + val connection = client.mediaConnectionFor(guildId) + connection.connect(vsi).whenComplete { _, _ -> + client.playerFor(guildId).provideTo(connection) + } + } + + fun seek(client: MagmaClient, guildId: Long, position: Long) { + val player = client.playerFor(guildId) + player.seekTo(position) + } + + suspend fun destroy(client: MagmaClient, guildId: Long) { + val player = client.players[guildId] + player?.destroy() + client.koe.destroyConnection(guildId) + } + + suspend fun playTrack( + client: MagmaClient, + guildId: Long, + track: String, + startTime: Long?, + endTime: Long?, + noReplace: Boolean = false + ) { + val player = client.playerFor(guildId) + if (player.audioPlayer.playingTrack != null && noReplace) { + log.info("${client.displayName} - skipping PLAY_TRACK operation") + return + } + + val track = TrackUtil.decode(track) + + /* handle start and end times */ + if (startTime != null && startTime in 0..track.duration) { + track.position = startTime + } + + if (endTime != null && endTime in 0..track.duration) { + val handler = TrackEndMarkerHandler(player) + val marker = TrackMarker(endTime, handler) + track.setMarker(marker) + } + + player.play(track) + } + + fun stopTrack(client: MagmaClient, guildId: Long) { + val player = client.playerFor(guildId) + player.audioPlayer.stopTrack() + } + + fun configure( + client: MagmaClient, + guildId: Long, + filters: Filters? = null, + pause: Boolean? = null, + sendPlayerUpdates: Boolean? = null + ) { + if (filters != null && pause != null && sendPlayerUpdates != null) { + return + } + + val player = client.playerFor(guildId) + pause?.let { player.audioPlayer.isPaused = it } + filters?.let { player.filters = it } + sendPlayerUpdates?.let { player.updates.enabled = it } + } + +} diff --git a/Server/src/main/kotlin/obsidian/server/io/Magma.kt b/Server/src/main/kotlin/obsidian/server/io/Magma.kt index 25564c2..0526ced 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Magma.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Magma.kt @@ -16,140 +16,175 @@ package obsidian.server.io +import io.ktor.application.* import io.ktor.auth.* -import io.ktor.features.* -import io.ktor.http.* import io.ktor.http.cio.websocket.* -import io.ktor.locations.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* -import io.ktor.util.* -import io.ktor.util.pipeline.* import io.ktor.websocket.* -import obsidian.server.Obsidian.config -import obsidian.server.io.MagmaCloseReason.CLIENT_EXISTS -import obsidian.server.io.MagmaCloseReason.INVALID_AUTHORIZATION -import obsidian.server.io.MagmaCloseReason.MISSING_CLIENT_NAME -import obsidian.server.io.MagmaCloseReason.NO_USER_ID -import obsidian.server.io.controllers.routePlanner -import obsidian.server.io.controllers.tracks -import obsidian.server.util.config.ObsidianConfig +import kotlinx.coroutines.launch +import obsidian.server.Application.config +import obsidian.server.io.routes.planner +import obsidian.server.io.routes.players +import obsidian.server.io.routes.tracks +import obsidian.server.io.ws.CloseReasons +import obsidian.server.io.ws.StatsTask +import obsidian.server.io.ws.WebSocketHandler +import obsidian.server.util.Obsidian import obsidian.server.util.threadFactory +import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService +import kotlin.text.Typography.mdash + +object Magma { -class Magma private constructor() { /** - * Executor + * All connected clients. */ - val executor: ScheduledExecutorService = - Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Cleanup", daemon = true)) + val clients = ConcurrentHashMap() /** - * All connected clients. - * `Client ID -> MagmaClient` + * Executor used for cleaning up un-resumed sessions. */ - private val clients = ConcurrentHashMap() + val cleanupExecutor = + Executors.newSingleThreadScheduledExecutor(threadFactory("Obsidian Magma-Cleanup")) - fun use(routing: Routing) { - routing { - webSocket("/") { - val request = call.request + val log: Logger = LoggerFactory.getLogger(Magma::class.java) - /* Used within logs to easily identify different clients. */ - val clientName = request.headers["Client-Name"] - ?: request.queryParameters["client-name"] + /** + * Adds REST endpoint routes and websocket route + */ + fun Routing.magma() { + /* rest endpoints */ + tracks() + planner() + players() + + authenticate { + get("/stats") { + val stats = StatsTask.build(null) + call.respond(stats) + } + } - /* check if client names are required, if so check if one is provided. */ - if (config[ObsidianConfig.RequireClientName] && clientName.isNullOrBlank()) { - logger.warn("${request.local.remoteHost} - missing 'Client-Name' header") - return@webSocket close(MISSING_CLIENT_NAME) - } + /* websocket */ + webSocket("/magma") { + val request = call.request - val identification = "${request.local.remoteHost}${if (!clientName.isNullOrEmpty()) "($clientName)" else ""}" + /* check if client names are required, if so check if one was supplied */ + val clientName = request.clientName() + if (config[Obsidian.requireClientName] && clientName.isNullOrBlank()) { + log.warn("${request.local.remoteHost} $mdash missing 'Client-Name' header/query parameter.") + return@webSocket close(CloseReasons.MISSING_CLIENT_NAME) + } - /* validate authorization. */ - val auth = request.authorization() - ?: request.queryParameters["auth"] + /* used within logs to easily identify different clients */ + val display = "${request.local.remoteHost}${if (!clientName.isNullOrEmpty()) "($clientName)" else ""}" - if (!ObsidianConfig.validateAuth(auth)) { - logger.warn("$identification - authentication failed") - return@webSocket close(INVALID_AUTHORIZATION) - } + /* validate authorization */ + val auth = request.authorization() + ?: request.queryParameters["auth"] - logger.info("$identification - incoming connection") + if (!Obsidian.Server.validateAuth(auth)) { + log.warn("$display $mdash authentication failed") + return@webSocket close(CloseReasons.INVALID_AUTHORIZATION) + } - /* check for userId */ - val userId = request.headers["User-Id"]?.toLongOrNull() - ?: request.queryParameters["user-id"]?.toLongOrNull() + log.info("$display $mdash incoming connection") - if (userId == null) { - /* no user id was given, close the connection */ - logger.info("$identification - missing 'User-Id' header") - return@webSocket close(NO_USER_ID) - } + /* check for user id */ + val userId = request.userId() + if (userId == null) { + /* no user-id was given, close the connection */ + log.info("$display $mdash missing 'User-Id' header/query parameter") + return@webSocket close(CloseReasons.MISSING_USER_ID) + } - /* check if a client for the provided userId already exists. */ - var client = clients[userId] - if (client != null) { - /* check for a resume key, if one was given check if the client has the same resume key/ */ - val resumeKey: String? = request.headers["Resume-Key"] - if (resumeKey != null && client.resumeKey == resumeKey) { - /* resume the client session */ - client.resume(this) - return@webSocket - } - - return@webSocket close(CLIENT_EXISTS) + val client = clients[userId] + ?: createClient(userId, clientName) + + val wsh = client.websocket + if (wsh != null) { + /* check for a resume key, if one was given check if the client has the same resume key/ */ + val resumeKey: String? = request.headers["Resume-Key"] + if (resumeKey != null && wsh.resumeKey == resumeKey) { + /* resume the client session */ + wsh.resume(this) + return@webSocket } - /* create client */ - client = MagmaClient(clientName, userId, this) - clients[userId] = client + return@webSocket close(CloseReasons.DUPLICATE_SESSION) + } - /* listen for incoming messages */ - try { - client.listen() - } catch (ex: Throwable) { - logger.error("${client.identification} -", ex) - close(CloseReason(4005, ex.message ?: "unknown exception")) - } + handleWebsocket(client, this) + } + } - client.handleClose() - } + fun getClient(userId: Long, clientName: String? = null): MagmaClient { + return clients[userId] ?: createClient(userId, clientName) + } - authenticate { - get("/stats") { - context.respond(StatsBuilder.build()) - } - } + /** + * Creates a [MagmaClient] for the supplied [userId] with an optional [clientName] + * + * @param userId + * @param clientName + */ + fun createClient(userId: Long, clientName: String? = null): MagmaClient { + return MagmaClient(userId).also { + it.name = clientName + clients[userId] = it + println(clients) } + } - routing.tracks() - routing.routePlanner() + /** + * Extracts the 'User-Id' header or 'user-id' query parameter from the provided [request] + * + * @param request + * [ApplicationRequest] to extract the user id from. + */ + fun extractUserId(request: ApplicationRequest): Long? { + return request.headers["user-id"]?.toLongOrNull() + ?: request.queryParameters["user-id"]?.toLongOrNull() } - suspend fun shutdown(client: MagmaClient) { - client.shutdown() - clients.remove(client.clientId) + fun ApplicationRequest.userId(): Long? = + extractUserId(this) + + /** + * Extracts the 'Client-Name' header or 'client-name' query parameter from the provided [request] + * + * @param request + * [ApplicationRequest] to extract the client name from. + */ + fun extractClientName(request: ApplicationRequest): String? { + return request.headers["Client-Name"] + ?: request.queryParameters["client-name"] } - suspend fun shutdown() { - if (clients.isNotEmpty()) { - logger.info("Shutting down ${clients.size} clients.") - for ((_, client) in clients) { - client.shutdown() - } - } else { - logger.info("No clients to shutdown.") + fun ApplicationRequest.clientName(): String? = + extractClientName(this) + + /** + * Handles a [WebSocketServerSession] for the supplied [client] + */ + suspend fun handleWebsocket(client: MagmaClient, wss: WebSocketServerSession) { + val wsh = WebSocketHandler(client, wss).also { + client.websocket = it + } + + /* listen for incoming messages. */ + try { + wsh.listen() + } catch (ex: Exception) { + log.error("${client.displayName} threw an error", ex) + wss.close(CloseReason(4006, ex.message ?: ex.cause?.message ?: "unknown error")) } - } - companion object { - val magma: Magma by lazy { Magma() } - private val logger = LoggerFactory.getLogger(Magma::class.java) + wsh.handleClose() } } diff --git a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt index af0a76d..9bc58e3 100644 --- a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt +++ b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt @@ -16,387 +16,133 @@ package obsidian.server.io -import com.sedmelluq.discord.lavaplayer.track.TrackMarker import io.ktor.http.cio.websocket.* -import io.ktor.util.* -import io.ktor.util.network.* -import io.ktor.websocket.* -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* -import kotlinx.serialization.json.Json -import obsidian.bedrock.* -import obsidian.bedrock.gateway.AbstractMediaGatewayConnection.Companion.asFlow -import obsidian.server.io.Magma.Companion.magma -import obsidian.server.player.Link -import obsidian.server.player.TrackEndMarkerHandler -import obsidian.server.player.filter.FilterChain -import obsidian.server.util.TrackUtil -import obsidian.server.util.threadFactory +import kotlinx.coroutines.launch +import moe.kyokobot.koe.KoeClient +import moe.kyokobot.koe.KoeEventAdapter +import moe.kyokobot.koe.MediaConnection +import obsidian.server.io.ws.WebSocketClosedEvent +import obsidian.server.io.ws.WebSocketHandler +import obsidian.server.io.ws.WebSocketOpenEvent +import obsidian.server.player.Player +import obsidian.server.util.KoeUtil import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.lang.Runnable -import java.nio.charset.Charset -import java.util.concurrent.* -import kotlin.coroutines.CoroutineContext +import java.net.InetSocketAddress +import java.util.concurrent.ConcurrentHashMap -class MagmaClient( - val clientName: String?, - val clientId: Long, - private var session: WebSocketServerSession -) : CoroutineScope { - /** - * Identification for this Client. - */ - val identification: String - get() = "${clientName ?: clientId}" - - /** - * The Bedrock client for this Session - */ - val bedrock = BedrockClient(clientId) - - /** - * guild id -> [Link] - */ - val links = ConcurrentHashMap() - - /** - * Resume key - */ - var resumeKey: String? = null - - /** - * Stats interval. - */ - private var stats = Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Stats Dispatcher %d")) +class MagmaClient(val userId: Long) { /** - * Whether this magma client is active + * The name of this client. */ - private var active: Boolean = false + var name: String? = null /** - * Resume timeout + * The display name for this client. */ - private var resumeTimeout: Long? = null + val displayName: String + get() = "${name ?: userId}" /** - * Timeout future + * The koe client used to send audio frames. */ - private var resumeTimeoutFuture: ScheduledFuture<*>? = null + val koe: KoeClient by lazy { + KoeUtil.koe.newClient(userId) + } /** - * The dispatch buffer timeout + * The websocket handler for this client, or null if one hasn't been initialized. */ - private var bufferTimeout: Long? = null + var websocket: WebSocketHandler? = null /** - * The dispatch buffer + * Current players */ - private var dispatchBuffer: ConcurrentLinkedQueue? = null + var players = ConcurrentHashMap() /** - * Events flow lol - idk kotlin + * Convenience method for ensuring that a player with the supplied guild id exists. + * + * @param guildId ID of the guild. */ - private val events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) - - override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + SupervisorJob() - - init { - on { - val conn = mediaConnectionFor(guildId) - val link = links.computeIfAbsent(guildId) { - Link(this@MagmaClient, guildId) - } - - conn.connect(VoiceServerInfo(sessionId, token, endpoint)) - link.provideTo(conn) - } - - on { - val link = links.computeIfAbsent(guildId) { - Link(this@MagmaClient, guildId) - } - - link.filters = FilterChain.from(link, this) - } - - on { - val link = links.computeIfAbsent(guildId) { - Link(this@MagmaClient, guildId) - } - - link.audioPlayer.isPaused = state - } - - on { - val link = links.computeIfAbsent(guildId) { - Link(this@MagmaClient, guildId) - } - - link.seekTo(position) - } - - on { - val link = links.remove(guildId) - link?.audioPlayer?.destroy() - - bedrock.destroyConnection(guildId) - } - - on { - val link = links.computeIfAbsent(guildId) { - Link(this@MagmaClient, guildId) - } - - link.audioPlayer.stopTrack() - } - - on { - resumeKey = key - resumeTimeout = timeout - - logger.debug("$identification - resuming is configured; key= $key, timeout= $timeout") - } - - on { - bufferTimeout = timeout - logger.debug("$identification - dispatch buffer timeout: $timeout") - } - - on { - val link = links.computeIfAbsent(guildId) { - Link(this@MagmaClient, guildId) - } - - pause?.let { link.audioPlayer.isPaused = it } - - filters?.let { link.filters = FilterChain.from(link, it) } - - sendPlayerUpdates?.let { link.playerUpdates.enabled = it } - } - - on { - val link = links.computeIfAbsent(guildId) { - Link(this@MagmaClient, guildId) - } - - if (link.audioPlayer.playingTrack != null && noReplace) { - logger.info("$identification - skipping PLAY_TRACK operation") - return@on - } - - val track = TrackUtil.decode(track) - - // handle "end_time" and "start_time" parameters - if (startTime in 0..track.duration) { - track.position = startTime - } - - if (endTime in 0..track.duration) { - val handler = TrackEndMarkerHandler(link) - val marker = TrackMarker(endTime, handler) - track.setMarker(marker) - } - - link.play(track) - } - } - - suspend fun handleClose() { - if (resumeKey != null) { - if (bufferTimeout?.takeIf { it > 0 } != null) { - dispatchBuffer = ConcurrentLinkedQueue() - } - - val runnable = Runnable { - runBlocking { - magma.shutdown(this@MagmaClient) - } - } - - resumeTimeoutFuture = magma.executor.schedule(runnable, resumeTimeout!!, TimeUnit.MILLISECONDS) - logger.info("$identification - session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") - return + fun playerFor(guildId: Long): Player { + return players.computeIfAbsent(guildId) { + Player(guildId, this) } - - magma.shutdown(this) - } - - suspend fun resume(session: WebSocketServerSession) { - logger.info("$identification - session has been resumed") - - this.session = session - this.active = true - this.resumeTimeoutFuture?.cancel(false) - - dispatchBuffer?.let { - for (payload in dispatchBuffer!!) { - send(payload) - } - } - - listen() - } - - suspend fun listen() { - active = true - - /* starting sending stats. */ - stats.scheduleAtFixedRate(this::sendStats, 0, 1, TimeUnit.MINUTES) - - /* listen for incoming frames. */ - session.incoming.asFlow().buffer(Channel.UNLIMITED) - .collect { - when (it) { - is Frame.Binary, is Frame.Text -> handleIncomingFrame(it) - else -> { // no-op - } - } - } - - /* connection has been closed. */ - active = false } /** - * Sends node stats to the Client. + * Returns a [MediaConnection] for the supplied [guildId] + * + * @param guildId ID of the guild to get a media connection for. */ - private fun sendStats() { - launch { - send(StatsBuilder.build(this@MagmaClient)) + fun mediaConnectionFor(guildId: Long): MediaConnection { + var connection = koe.getConnection(guildId) + if (connection == null) { + connection = koe.createConnection(guildId) + connection.registerListener(EventAdapterImpl(connection)) } - } - private inline fun on(crossinline block: suspend T.() -> Unit) { - events.filterIsInstance() - .onEach { - try { - block.invoke(it) - } catch (ex: Exception) { - logger.error("$identification -", ex) - } - } - .launchIn(this) + return connection } /** - * Handles an incoming [Frame]. + * Shutdown this magma client. * - * @param frame The received text or binary frame. + * @param safe + * Whether we should be cautious about shutting down. */ - private suspend fun handleIncomingFrame(frame: Frame) { - val json = frame.data.toString(Charset.defaultCharset()) + suspend fun shutdown(safe: Boolean = true) { + websocket?.session?.close(CloseReason(1000, "shutting down")) + websocket = null - try { - logger.info("$identification >>> $json") - jsonParser.decodeFromString(Operation, json)?.let { events.emit(it) } - } catch (ex: Exception) { - logger.error("$identification -", ex) + val activePlayers = players.count { (_, player) -> + player.audioPlayer.playingTrack != null } - } - - private fun mediaConnectionFor(guildId: Long): MediaConnection { - var mediaConnection = bedrock.getConnection(guildId) - if (mediaConnection == null) { - mediaConnection = bedrock.createConnection(guildId) - EventListener(mediaConnection) - } - - return mediaConnection - } - /** - * Send a JSON payload to the client. - * - * @param dispatch The dispatch instance - */ - suspend fun send(dispatch: Dispatch) { - val json = jsonParser.encodeToString(Dispatch.Companion, dispatch) - if (!active) { - dispatchBuffer?.offer(json) + if (safe && activePlayers != 0) { return } - send(json) - } + /* no players are active so it's safe to remove the client. */ - /** - * Sends a JSON encoded dispatch payload to the client - * - * @param json JSON encoded dispatch payload - */ - private suspend fun send(json: String) { - try { - logger.trace("$identification <<< $json") - session.send(json) - } catch (ex: Exception) { - logger.error("$identification -", ex) + for ((id, player) in players) { + player.destroy() + players.remove(id) } - } - internal suspend fun shutdown() { - /* shut down stats task */ - stats.shutdown() - - /* shut down all links */ - logger.info("$identification - shutting down ${links.size} links.") - for ((id, link) in links) { - link.playerUpdates.stop() - link.audioPlayer.destroy() - links.remove(id) - } - - bedrock.close() + koe.close() } - inner class EventListener(mediaConnection: MediaConnection) { - private var lastHeartbeat: Long? = null - private var lastHeartbeatNonce: Long? = null - - init { - mediaConnection.on { - val dispatch = WebSocketOpenEvent(guildId = guildId, ssrc = ssrc, target = target.hostname) - send(dispatch) - } + inner class EventAdapterImpl(val connection: MediaConnection) : KoeEventAdapter() { + override fun gatewayReady(target: InetSocketAddress, ssrc: Int) { + websocket?.launch { + val event = WebSocketOpenEvent( + guildId = connection.guildId, + ssrc = ssrc, + target = target.toString(), + ) - mediaConnection.on { - val dispatch = WebSocketClosedEvent(guildId = guildId, reason = reason, code = code) - send(dispatch) + websocket?.send(event) } + } - mediaConnection.on { - if (lastHeartbeatNonce == null || lastHeartbeat == null) { - return@on - } - - if (lastHeartbeatNonce != nonce) { - logger.debug("$identification - a heartbeat was acknowledged but it wasn't the last?") - return@on - } - - logger.debug("$identification - voice WebSocket latency is ${System.currentTimeMillis() - lastHeartbeat!!}ms") - } + override fun gatewayClosed(code: Int, reason: String?, byRemote: Boolean) { + websocket?.launch { + val event = WebSocketClosedEvent( + guildId = connection.guildId, + code = code, + reason = reason, + byRemote = byRemote + ) - mediaConnection.on { - lastHeartbeat = System.currentTimeMillis() - lastHeartbeatNonce = nonce + websocket?.send(event) } } } companion object { - /** - * JSON parser for everything. - */ - val jsonParser = Json { - ignoreUnknownKeys = true - isLenient = true - encodeDefaults = true - } - - private val logger: Logger = LoggerFactory.getLogger(MagmaClient::class.java) + val log: Logger = LoggerFactory.getLogger(MagmaClient::class.java) } } diff --git a/Server/src/main/kotlin/obsidian/server/io/MagmaCloseReason.kt b/Server/src/main/kotlin/obsidian/server/io/MagmaCloseReason.kt deleted file mode 100644 index 7d765c2..0000000 --- a/Server/src/main/kotlin/obsidian/server/io/MagmaCloseReason.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.server.io - -import io.ktor.http.cio.websocket.CloseReason - -object MagmaCloseReason { - val INVALID_AUTHORIZATION = CloseReason(4001, "Invalid Authorization") - val NO_USER_ID = CloseReason(4002, "No user id provided.") - val CLIENT_EXISTS = CloseReason(4004, "A client for the provided user already exists.") - val MISSING_CLIENT_NAME = CloseReason(4006, "This server requires the 'Client-Name' to be present.") -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/server/io/Op.kt b/Server/src/main/kotlin/obsidian/server/io/Op.kt deleted file mode 100644 index aa769fb..0000000 --- a/Server/src/main/kotlin/obsidian/server/io/Op.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.server.io - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -@Serializable(with = Op.Serializer::class) -enum class Op(val code: Int) { - Unknown(Int.MIN_VALUE), - SubmitVoiceUpdate(0), - - // obsidian related. - Stats(1), - SetupResuming(10), - SetupDispatchBuffer(11), - - // player information. - PlayerEvent(2), - PlayerUpdate(3), - - // player control. - PlayTrack(4), - StopTrack(5), - Pause(6), - Filters(7), - Seek(8), - Destroy(9), - Configure(12); - - companion object Serializer : KSerializer { - /** - * Finds the Op for the provided [code] - * - * @param code The operation code. - */ - operator fun get(code: Int): Op? = - values().firstOrNull { it.code == code } - - override val descriptor: SerialDescriptor - get() = PrimitiveSerialDescriptor("op", PrimitiveKind.INT) - - override fun deserialize(decoder: Decoder): Op = - this[decoder.decodeInt()] ?: Unknown - - override fun serialize(encoder: Encoder, value: Op) = - encoder.encodeInt(value.code) - } - -} diff --git a/Server/src/main/kotlin/obsidian/server/io/controllers/RoutePlanner.kt b/Server/src/main/kotlin/obsidian/server/io/routes/planner.kt similarity index 87% rename from Server/src/main/kotlin/obsidian/server/io/controllers/RoutePlanner.kt rename to Server/src/main/kotlin/obsidian/server/io/routes/planner.kt index 375d84f..089fe97 100644 --- a/Server/src/main/kotlin/obsidian/server/io/controllers/RoutePlanner.kt +++ b/Server/src/main/kotlin/obsidian/server/io/routes/planner.kt @@ -14,23 +14,21 @@ * limitations under the License. */ -package obsidian.server.io.controllers +package obsidian.server.io.routes import io.ktor.auth.* import io.ktor.http.* -import io.ktor.locations.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* +import obsidian.server.Application import kotlinx.serialization.Serializable -import obsidian.server.Obsidian.playerManager import obsidian.server.io.RoutePlannerStatus import obsidian.server.io.RoutePlannerUtil.getDetailBlock import java.net.InetAddress -import java.util.* -fun Routing.routePlanner() { - val routePlanner = playerManager.routePlanner +fun Routing.planner() { + val routePlanner = Application.players.routePlanner route("/routeplanner") { authenticate { @@ -40,8 +38,8 @@ fun Routing.routePlanner() { /* respond with route planner status */ val status = RoutePlannerStatus( - playerManager::class.simpleName, - getDetailBlock(playerManager.routePlanner!!) + Application.players::class.simpleName, + getDetailBlock(Application.players.routePlanner!!) ) context.respond(status) diff --git a/Server/src/main/kotlin/obsidian/server/io/routes/players.kt b/Server/src/main/kotlin/obsidian/server/io/routes/players.kt new file mode 100644 index 0000000..bf11f9c --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/io/routes/players.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.io.routes + +import io.ktor.application.* +import io.ktor.auth.* +import io.ktor.http.* +import io.ktor.http.HttpStatusCode.Companion.BadRequest +import io.ktor.http.HttpStatusCode.Companion.NotFound +import io.ktor.request.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.util.* +import io.ktor.util.pipeline.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import moe.kyokobot.koe.VoiceServerInfo +import obsidian.server.Application.config +import obsidian.server.io.Handlers +import obsidian.server.io.Magma +import obsidian.server.io.Magma.clientName +import obsidian.server.io.Magma.userId +import obsidian.server.io.ws.CurrentTrack +import obsidian.server.io.ws.Frames +import obsidian.server.player.PlayerUpdates.Companion.currentTrackFor +import obsidian.server.player.filter.Filters +import obsidian.server.util.Obsidian + +val UserIdAttributeKey = AttributeKey("User-Id") +val ClientNameAttributeKey = AttributeKey("Client-Name") + +fun Routing.players() = this.authenticate { + this.route("/players/{guild}") { + intercept(ApplicationCallPipeline.Call) { + /* extract user id from the http request */ + val userId = call.request.userId() + ?: return@intercept respondAndFinish(BadRequest, Response("Missing 'User-Id' header or query parameter.")) + + context.attributes.put(UserIdAttributeKey, userId) + + /* extract client name from the request */ + val clientName = call.request.clientName() + if (clientName != null) { + context.attributes.put(ClientNameAttributeKey, clientName) + } else if (config[Obsidian.requireClientName]) { + return@intercept respondAndFinish(BadRequest, Response("Missing 'Client-Name' header or query parameter.")) + } + } + + get { + /* get the guild id */ + val guildId = call.parameters["guild"]?.toLongOrNull() + ?: return@get respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) + + /* get a client for this. */ + val client = Magma.getClient(context.attributes[UserIdAttributeKey], context.attributes[ClientNameAttributeKey]) + + /* get the requested player */ + val player = client.players[guildId] + ?: return@get respondAndFinish(NotFound, Response("Unknown player for guild '$guildId'")) + + /* respond */ + val response = GetPlayer(currentTrackFor(player), player.filters, player.frameLossTracker.payload) + call.respond(response) + } + + put("/submit-voice-server") { + /* get the guild id */ + val guildId = call.parameters["guild"]?.toLongOrNull() + ?: return@put respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) + + /* get a client for this. */ + val client = + Magma.getClient(context.attributes[UserIdAttributeKey], context.attributes.getOrNull(ClientNameAttributeKey)) + + /* connect to the voice server described in the request body */ + val (session, token, endpoint) = call.receive() + Handlers.submitVoiceServer(client, guildId, VoiceServerInfo(session, endpoint, token)) + + /* respond */ + call.respond(Response("successfully queued connection", success = true)) + } + + post("/play") { + /* get the guild id */ + val guildId = call.parameters["guild"]?.toLongOrNull() + ?: return@post respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) + + /* get a client for this. */ + val client = + Magma.getClient(context.attributes[UserIdAttributeKey], context.attributes.getOrNull(ClientNameAttributeKey)) + + /* connect to the voice server described in the request body */ + val (track, start, end, noReplace) = call.receive() + Handlers.playTrack(client, guildId, track, start, end, noReplace) + + /* respond */ + call.respond(Response("playback has started", success = true)) + } + } +} + +/** + * Body for `PUT /player/{guild}/submit-voice-server` + */ +@Serializable +data class SubmitVoiceServer(@SerialName("session_id") val sessionId: String, val token: String, val endpoint: String) + +/** + * + */ +@Serializable +data class PlayTrack( + val track: String, + @SerialName("start_time") val startTime: Long? = null, + @SerialName("end_time") val endTime: Long? = null, + @SerialName("no_replace") val noReplace: Boolean = false +) + +/** + * Response for `GET /player/{guild}` + */ +@Serializable +data class GetPlayer( + @SerialName("current_track") val currentTrack: CurrentTrack, + val filters: Filters?, + val frames: Frames +) + +/** + * Data class for creating a request error + */ +@Serializable +data class Response(val message: String, val success: Boolean = false) + +/** + * + */ +suspend inline fun PipelineContext.respondAndFinish( + statusCode: HttpStatusCode, + message: T +) { + call.respond(statusCode, message) + finish() +} diff --git a/Server/src/main/kotlin/obsidian/server/io/controllers/Tracks.kt b/Server/src/main/kotlin/obsidian/server/io/routes/tracks.kt similarity index 74% rename from Server/src/main/kotlin/obsidian/server/io/controllers/Tracks.kt rename to Server/src/main/kotlin/obsidian/server/io/routes/tracks.kt index 181e643..53db421 100644 --- a/Server/src/main/kotlin/obsidian/server/io/controllers/Tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/routes/tracks.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package obsidian.server.io.controllers +package obsidian.server.io.routes import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack @@ -31,23 +31,20 @@ import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonTransformingSerializer -import obsidian.server.Obsidian -import obsidian.server.io.search.AudioLoader -import obsidian.server.io.search.LoadType +import obsidian.server.Application import obsidian.server.util.TrackUtil import obsidian.server.util.kxs.AudioTrackSerializer +import obsidian.server.util.search.AudioLoader +import obsidian.server.util.search.LoadType import org.slf4j.Logger import org.slf4j.LoggerFactory -private val logger: Logger = LoggerFactory.getLogger("TracksController") +private val logger: Logger = LoggerFactory.getLogger("Routing.tracks") fun Routing.tracks() { authenticate { - @Location("/loadtracks") - data class LoadTracks(val identifier: String) - get { data -> - val result = AudioLoader(Obsidian.playerManager) + val result = AudioLoader(Application.players) .load(data.identifier) .await() @@ -56,11 +53,11 @@ fun Routing.tracks() { } val playlist = result.playlistName?.let { - LoadTracksResponse.PlaylistInfo(name = it, selectedTrack = result.selectedTrack) + LoadTracks.Response.PlaylistInfo(name = it, selectedTrack = result.selectedTrack) } val exception = if (result.loadResultType == LoadType.LOAD_FAILED && result.exception != null) { - LoadTracksResponse.Exception( + LoadTracks.Response.Exception( message = result.exception!!.localizedMessage, severity = result.exception!!.severity ) @@ -68,7 +65,7 @@ fun Routing.tracks() { null } - val response = LoadTracksResponse( + val response = LoadTracks.Response( tracks = result.tracks.map(::getTrack), type = result.loadResultType, playlistInfo = playlist, @@ -78,9 +75,6 @@ fun Routing.tracks() { context.respond(response) } - @Location("/decodetrack") - data class DecodeTrack(val track: String) - get { val track = TrackUtil.decode(it.track) context.respond(getTrackInfo(track)) @@ -93,9 +87,15 @@ fun Routing.tracks() { } } +/** + * + */ private fun getTrack(audioTrack: AudioTrack): Track = Track(track = audioTrack, info = getTrackInfo(audioTrack)) +/** + * + */ private fun getTrackInfo(audioTrack: AudioTrack): Track.Info = Track.Info( title = audioTrack.info.title, @@ -105,33 +105,49 @@ private fun getTrackInfo(audioTrack: AudioTrack): Track.Info = length = audioTrack.duration, isSeekable = audioTrack.isSeekable, isStream = audioTrack.info.isStream, - position = audioTrack.position + position = audioTrack.position, + sourceName = audioTrack.sourceManager?.sourceName ?: "unknown" ) +/** + * + */ @Serializable data class DecodeTracksBody(@Serializable(with = AudioTrackListSerializer::class) val tracks: List) -@Serializable -data class LoadTracksResponse( - @SerialName("load_type") - val type: LoadType, - @SerialName("playlist_info") - val playlistInfo: PlaylistInfo?, - val tracks: List, - val exception: Exception? -) { - @Serializable - data class Exception( - val message: String, - val severity: FriendlyException.Severity - ) +/** + * + */ +@Location("/decodetrack") +data class DecodeTrack(val track: String) +/** + * + */ +@Location("/loadtracks") +data class LoadTracks(val identifier: String) { @Serializable - data class PlaylistInfo( - val name: String, - @SerialName("selected_track") - val selectedTrack: Int? - ) + data class Response( + @SerialName("load_type") + val type: LoadType, + @SerialName("playlist_info") + val playlistInfo: PlaylistInfo?, + val tracks: List, + val exception: Exception? + ) { + @Serializable + data class Exception( + val message: String, + val severity: FriendlyException.Severity + ) + + @Serializable + data class PlaylistInfo( + val name: String, + @SerialName("selected_track") + val selectedTrack: Int? + ) + } } @Serializable @@ -152,10 +168,11 @@ data class Track( val isStream: Boolean, @SerialName("is_seekable") val isSeekable: Boolean, + @SerialName("source_name") + val sourceName: String ) } -// taken from docs lmao object AudioTrackListSerializer : JsonTransformingSerializer>(ListSerializer(AudioTrackSerializer)) { override fun transformDeserialize(element: JsonElement): JsonElement = if (element !is JsonArray) { diff --git a/Server/src/main/kotlin/obsidian/bedrock/crypto/DefaultEncryptionModes.kt b/Server/src/main/kotlin/obsidian/server/io/ws/CloseReasons.kt similarity index 55% rename from Server/src/main/kotlin/obsidian/bedrock/crypto/DefaultEncryptionModes.kt rename to Server/src/main/kotlin/obsidian/server/io/ws/CloseReasons.kt index 48c12bd..c424f3f 100644 --- a/Server/src/main/kotlin/obsidian/bedrock/crypto/DefaultEncryptionModes.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/CloseReasons.kt @@ -14,13 +14,14 @@ * limitations under the License. */ -package obsidian.bedrock.crypto +package obsidian.server.io.ws -object DefaultEncryptionModes { - val encryptionModes = mapOf( - "xsalsa20_poly1305_lite" to { XSalsa20Poly1305LiteEncryptionMode() }, - "xsalsa20_poly1305_suffix" to { XSalsa20Poly1305SuffixEncryptionMode() }, - "xsalsa20_poly1305" to { XSalsa20Poly1305EncryptionMode() }, - "plain" to { PlainEncryptionMode() } - ) -} \ No newline at end of file +import io.ktor.http.cio.websocket.* + +object CloseReasons { + val INVALID_AUTHORIZATION = CloseReason(4001, "Invalid or missing authorization header or query parameter.") + val MISSING_CLIENT_NAME = CloseReason(4002, "Missing 'Client-Name' header or query parameter") + val MISSING_USER_ID = CloseReason(4003, "Missing 'User-Id' header or query parameter") + val DUPLICATE_SESSION = CloseReason(4005, "A session for the supplied user already exists.") + // 4006 +} diff --git a/Server/src/main/kotlin/obsidian/server/io/Dispatch.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt similarity index 90% rename from Server/src/main/kotlin/obsidian/server/io/Dispatch.kt rename to Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt index 54ea31d..b71743b 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Dispatch.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package obsidian.server.io +package obsidian.server.io.ws import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack @@ -40,42 +40,42 @@ sealed class Dispatch { with(encoder.beginStructure(descriptor)) { when (value) { is Stats -> { - encodeSerializableElement(descriptor, 0, Op, Op.Stats) + encodeSerializableElement(descriptor, 0, Op, Op.STATS) encodeSerializableElement(descriptor, 1, Stats.serializer(), value) } is PlayerUpdate -> { - encodeSerializableElement(descriptor, 0, Op, Op.PlayerUpdate) + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_UPDATE) encodeSerializableElement(descriptor, 1, PlayerUpdate.serializer(), value) } is TrackStartEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PlayerEvent) + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) encodeSerializableElement(descriptor, 1, TrackStartEvent.serializer(), value) } is TrackEndEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PlayerEvent) + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) encodeSerializableElement(descriptor, 1, TrackEndEvent.serializer(), value) } is TrackExceptionEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PlayerEvent) + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) encodeSerializableElement(descriptor, 1, TrackExceptionEvent.serializer(), value) } is TrackStuckEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PlayerEvent) + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) encodeSerializableElement(descriptor, 1, TrackStuckEvent.serializer(), value) } is WebSocketOpenEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PlayerEvent) + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) encodeSerializableElement(descriptor, 1, WebSocketOpenEvent.serializer(), value) } is WebSocketClosedEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PlayerEvent) + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) encodeSerializableElement(descriptor, 1, WebSocketClosedEvent.serializer(), value) } } @@ -86,6 +86,7 @@ sealed class Dispatch { } } + // Player Update @Serializable @@ -139,7 +140,9 @@ data class WebSocketClosedEvent( @SerialName("guild_id") override val guildId: Long, val reason: String?, - val code: Short + val code: Int, + @SerialName("by_remote") + val byRemote: Boolean ) : PlayerEvent() { override val type: PlayerEventType = PlayerEventType.WEBSOCKET_CLOSED } diff --git a/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Op.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt similarity index 60% rename from Server/src/main/kotlin/obsidian/bedrock/gateway/event/Op.kt rename to Server/src/main/kotlin/obsidian/server/io/ws/Op.kt index af5e2ff..a10c02e 100644 --- a/Server/src/main/kotlin/obsidian/bedrock/gateway/event/Op.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt @@ -14,44 +14,48 @@ * limitations under the License. */ -package obsidian.bedrock.gateway.event +package obsidian.server.io.ws import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +@Serializable(with = Op.Serializer::class) enum class Op(val code: Int) { - Unknown(Int.MIN_VALUE), - - Identify(0), - SelectProtocol(1), - Ready(2), - Heartbeat(3), - SessionDescription(4), - Speaking(5), - HeartbeatAck(6), - Hello(8), - ClientConnect(12); + SUBMIT_VOICE_UPDATE(0), + STATS(1), - companion object Serializer : KSerializer { - /** - * Finds the Op for the provided [code] - * - * @param code The operation code. - */ - operator fun get(code: Int): Op? = - values().find { it.code == code } + SETUP_RESUMING(2), + SETUP_DISPATCH_BUFFER(3), + + PLAYER_EVENT(4), + PLAYER_UPDATE(5), + + PLAY_TRACK(6), + STOP_TRACK(7), + PAUSE(8), + FILTERS(9), + SEEK(10), + DESTROY(11), + CONFIGURE(12), - override val descriptor: SerialDescriptor - get() = PrimitiveSerialDescriptor("op", PrimitiveKind.INT) + UNKNOWN(-1); + + companion object Serializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("MagmaOperation", PrimitiveKind.INT) - override fun deserialize(decoder: Decoder): Op = - this[decoder.decodeInt()] ?: Unknown + override fun deserialize(decoder: Decoder): Op { + val code = decoder.decodeInt() + return values().firstOrNull { it.code == code } ?: UNKNOWN + } - override fun serialize(encoder: Encoder, value: Op) = + override fun serialize(encoder: Encoder, value: Op) { encoder.encodeInt(value.code) + } } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/io/Operation.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt similarity index 82% rename from Server/src/main/kotlin/obsidian/server/io/Operation.kt rename to Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt index 7c3b62a..a3ffe3d 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Operation.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt @@ -14,10 +14,12 @@ * limitations under the License. */ -package obsidian.server.io +package obsidian.server.io.ws -import kotlinx.serialization.* -import kotlinx.serialization.builtins.LongAsStringSerializer +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.nullable import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor @@ -25,7 +27,6 @@ import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject -import obsidian.server.player.filter.impl.* sealed class Operation { companion object : DeserializationStrategy { @@ -53,34 +54,34 @@ sealed class Operation { 1 -> data = when (op) { - Op.SubmitVoiceUpdate -> + Op.SUBMIT_VOICE_UPDATE -> decode(SubmitVoiceUpdate.serializer()) - Op.PlayTrack -> + Op.PLAY_TRACK -> decode(PlayTrack.serializer()) - Op.StopTrack -> + Op.STOP_TRACK -> decode(StopTrack.serializer()) - Op.Pause -> + Op.PAUSE -> decode(Pause.serializer()) - Op.Filters -> + Op.FILTERS -> decode(Filters.serializer()) - Op.Seek -> + Op.SEEK -> decode(Seek.serializer()) - Op.Destroy -> + Op.DESTROY -> decode(Destroy.serializer()) - Op.SetupResuming -> + Op.SETUP_RESUMING -> decode(SetupResuming.serializer()) - Op.SetupDispatchBuffer -> + Op.SETUP_DISPATCH_BUFFER -> decode(SetupDispatchBuffer.serializer()) - Op.Configure -> + Op.CONFIGURE -> decode(Configure.serializer()) else -> if (data == null) { @@ -101,6 +102,7 @@ sealed class Operation { } } + @Serializable data class PlayTrack( val track: String, @@ -147,18 +149,7 @@ data class Pause( data class Filters( @SerialName("guild_id") val guildId: Long, - - val volume: Float? = null, - val tremolo: TremoloFilter? = null, - val equalizer: EqualizerFilter? = null, - val timescale: TimescaleFilter? = null, - val karaoke: KaraokeFilter? = null, - @SerialName("channel_mix") - val channelMix: ChannelMixFilter? = null, - val vibrato: VibratoFilter? = null, - val rotation: RotationFilter? = null, - @SerialName("low_pass") - val lowPass: LowPassFilter? = null + val filters: obsidian.server.player.filter.Filters ) : Operation() @Serializable @@ -173,7 +164,7 @@ data class Configure( @SerialName("guild_id") val guildId: Long, val pause: Boolean?, - val filters: Filters?, + val filters: obsidian.server.player.filter.Filters?, @SerialName("send_player_updates") val sendPlayerUpdates: Boolean? ) : Operation() diff --git a/Server/src/main/kotlin/obsidian/server/io/StatsBuilder.kt b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt similarity index 66% rename from Server/src/main/kotlin/obsidian/server/io/StatsBuilder.kt rename to Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt index 0fefd34..fe51d3c 100644 --- a/Server/src/main/kotlin/obsidian/server/io/StatsBuilder.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt @@ -14,12 +14,14 @@ * limitations under the License. */ -package obsidian.server.io +package obsidian.server.io.ws +import kotlinx.coroutines.launch +import obsidian.server.io.Magma import obsidian.server.util.CpuTimer import java.lang.management.ManagementFactory -object StatsBuilder { +object StatsTask { private val cpuTimer = CpuTimer() private var OS_BEAN_CLASS: Class<*>? = null @@ -27,11 +29,21 @@ object StatsBuilder { try { OS_BEAN_CLASS = Class.forName("com.sun.management.OperatingSystemMXBean") } catch (ex: Exception) { + // no-op + } + } + fun getRunnable(wsh: WebSocketHandler): Runnable { + return Runnable { + wsh.launch { + val stats = build(wsh) + wsh.send(stats) + } } } - fun build(client: MagmaClient? = null): Stats { + fun build(wsh: WebSocketHandler?): Stats { + val client = wsh?.client /* memory stats. */ val memory = ManagementFactory.getMemoryMXBean().let { bean -> @@ -65,25 +77,40 @@ object StatsBuilder { } /* player count */ - val players: Stats.Players? = client?.let { - Stats.Players( - active = client.links.count { (_, l) -> l.playing }, - total = client.links.size + val players: Stats.Players = when (client) { + null -> { + var (active, total) = Pair(0, 0) + for ((_, c) in Magma.clients) { + c.players.forEach { (_, p) -> + total++ + if (p.playing) { + active++ + } + } + } + + Stats.Players(active = active, total = total) + } + + else -> Stats.Players( + active = client.players.count { (_, l) -> l.playing }, + total = client.players.size ) } /* frames */ val frames: List = client?.let { - it.links.map { (_, link) -> + it.players.map { (_, player) -> Stats.FrameStats( - usable = link.frameCounter.dataUsable, - guildId = link.guildId, - sent = link.frameCounter.success.sum(), - lost = link.frameCounter.loss.sum(), + usable = player.frameLossTracker.dataUsable, + guildId = player.guildId, + sent = player.frameLossTracker.success.sum(), + lost = player.frameLossTracker.loss.sum(), ) } } ?: emptyList() return Stats(cpu = cpu, memory = memory, threads = threads, frames = frames, players = players) } -} \ No newline at end of file + +} diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt new file mode 100644 index 0000000..9ce2cac --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt @@ -0,0 +1,273 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.io.ws + +import io.ktor.http.cio.websocket.* +import io.ktor.utils.io.charsets.* +import io.ktor.websocket.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.flow.* +import moe.kyokobot.koe.VoiceServerInfo +import obsidian.server.Application.json +import obsidian.server.io.Handlers +import obsidian.server.io.MagmaClient +import obsidian.server.io.Magma.cleanupExecutor +import obsidian.server.util.threadFactory +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.lang.Runnable +import java.util.concurrent.* +import java.util.concurrent.CancellationException +import kotlin.coroutines.CoroutineContext + +class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSession) : CoroutineScope { + + /** + * Resume key + */ + var resumeKey: String? = null + + /** + * Stats interval. + */ + private var stats = Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Stats-Dispatcher %d")) + + /** + * Whether this magma client is active + */ + private var active: Boolean = false + + /** + * Resume timeout + */ + private var resumeTimeout: Long? = null + + /** + * Timeout future + */ + private var resumeTimeoutFuture: ScheduledFuture<*>? = null + + /** + * The dispatch buffer timeout + */ + private var bufferTimeout: Long? = null + + /** + * The dispatch buffer + */ + private var dispatchBuffer: ConcurrentLinkedQueue? = null + + /** + * Events flow lol - idk kotlin + */ + private val events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) + + override val coroutineContext: CoroutineContext + get() = Dispatchers.IO + SupervisorJob() + + init { + /* websocket and rest operations */ + on { + val vsi = VoiceServerInfo(sessionId, endpoint, token) + Handlers.submitVoiceServer(client, guildId, vsi) + } + + on { + Handlers.configure(client, guildId, filters = filters) + } + + on { + Handlers.configure(client, guildId, pause = state) + } + + on { + Handlers.configure(client, guildId, filters, pause, sendPlayerUpdates) + } + + on { + Handlers.seek(client, guildId, position) + } + + on { + Handlers.playTrack(client, guildId, track, startTime, endTime, noReplace) + } + + on { + Handlers.stopTrack(client, guildId) + } + + on { + Handlers.destroy(client, guildId) + } + + /* websocket-only operations */ + on { + resumeKey = key + resumeTimeout = timeout + + logger.debug("${client.displayName} - resuming is configured; key= $key, timeout= $timeout") + } + + on { + bufferTimeout = timeout + logger.debug("${client.displayName} - dispatch buffer timeout: $timeout") + } + + } + + /** + * + */ + suspend fun listen() { + active = true + + /* starting sending stats. */ + val statsRunnable = StatsTask.getRunnable(this) + stats.scheduleAtFixedRate(statsRunnable, 0, 1, TimeUnit.MINUTES) + + /* listen for incoming frames. */ + session.incoming.asFlow().buffer(Channel.UNLIMITED) + .collect { + when (it) { + is Frame.Binary, is Frame.Text -> handleIncomingFrame(it) + else -> { // no-op + } + } + } + + /* connection has been closed. */ + active = false + } + + /** + * Handles + */ + suspend fun handleClose() { + if (resumeKey != null) { + if (bufferTimeout?.takeIf { it > 0 } != null) { + dispatchBuffer = ConcurrentLinkedQueue() + } + + val runnable = Runnable { + runBlocking { + client.shutdown() + } + } + + resumeTimeoutFuture = cleanupExecutor.schedule(runnable, resumeTimeout!!, TimeUnit.MILLISECONDS) + logger.info("${client.displayName} - session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") + return + } + + client.shutdown() + } + + /** + * Resumes this session + */ + suspend fun resume(session: WebSocketServerSession) { + logger.info("${client.displayName} - session has been resumed") + + this.session = session + this.active = true + this.resumeTimeoutFuture?.cancel(false) + + dispatchBuffer?.let { + for (payload in dispatchBuffer!!) { + send(payload) + } + } + + listen() + } + + /** + * Send a JSON payload to the client. + * + * @param dispatch The dispatch instance + */ + suspend fun send(dispatch: Dispatch) { + val json = json.encodeToString(Dispatch.Companion, dispatch) + if (!active) { + dispatchBuffer?.offer(json) + return + } + + send(json) + } + + /** + * Sends a JSON encoded dispatch payload to the client + * + * @param json JSON encoded dispatch payload + */ + private suspend fun send(json: String) { + try { + logger.trace("${client.displayName} <<< $json") + session.send(json) + } catch (ex: Exception) { + logger.error("${client.displayName} -", ex) + } + } + + /** + * Convenience method that calls [block] whenever [T] gets emitted on [events] + */ + private inline fun on(crossinline block: suspend T.() -> Unit) { + events.filterIsInstance() + .onEach { + launch { + try { + block.invoke(it) + } catch (ex: Exception) { + logger.error("${client.displayName} -", ex) + } + } + } + .launchIn(this) + } + + /** + * Handles an incoming [Frame]. + * + * @param frame The received text or binary frame. + */ + private suspend fun handleIncomingFrame(frame: Frame) { + val data = frame.data.toString(Charset.defaultCharset()) + + try { + logger.info("${client.displayName} >>> $data") + json.decodeFromString(Operation, data)?.let { events.emit(it) } + } catch (ex: Exception) { + logger.error("${client.displayName} -", ex) + } + } + + companion object { + fun ReceiveChannel.asFlow() = flow { + try { + for (event in this@asFlow) emit(event) + } catch (ex: CancellationException) { + // no-op + } + } + + private val logger: Logger = LoggerFactory.getLogger(MagmaClient::class.java) + } +} diff --git a/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt b/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt index 9c5d7de..3bc75ed 100644 --- a/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt +++ b/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt @@ -20,12 +20,24 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayer import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason +import obsidian.server.io.ws.Frames import obsidian.server.util.ByteRingBuffer import java.util.concurrent.TimeUnit class FrameLossTracker : AudioEventAdapter() { + /** + * + */ var success = ByteRingBuffer(60) + + /** + * + */ var loss = ByteRingBuffer(60) + + /** + * + */ val dataUsable: Boolean get() { if (lastTrackStarted - lastTrackEnded > ACCEPTABLE_TRACK_SWITCH_TIME && lastTrackEnded != Long.MAX_VALUE) { @@ -35,6 +47,16 @@ class FrameLossTracker : AudioEventAdapter() { return TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - playingSince) >= 60 } + /** + * + */ + val payload: Frames + get() = Frames( + sent = success.sum(), + lost = loss.sum(), + usable = dataUsable + ) + private var curSuccess: Byte = 0 private var curLoss: Byte = 0 @@ -106,4 +128,4 @@ class FrameLossTracker : AudioEventAdapter() { */ const val EXPECTED_PACKET_COUNT_PER_MIN = 60 * 1000 / 20 } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/ObsidianPlayerManager.kt b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt similarity index 82% rename from Server/src/main/kotlin/obsidian/server/player/ObsidianPlayerManager.kt rename to Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt index c19027a..8fe8f2c 100644 --- a/Server/src/main/kotlin/obsidian/server/player/ObsidianPlayerManager.kt +++ b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt @@ -32,21 +32,21 @@ import com.sedmelluq.lava.extensions.youtuberotator.YoutubeIpRotatorSetup import com.sedmelluq.lava.extensions.youtuberotator.planner.* import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block -import obsidian.server.Obsidian.config -import obsidian.server.util.config.ObsidianConfig +import obsidian.server.Application.config +import obsidian.server.util.Obsidian import org.slf4j.Logger import org.slf4j.LoggerFactory import java.net.InetAddress import java.util.function.Predicate -class ObsidianPlayerManager : DefaultAudioPlayerManager() { +class ObsidianAPM : DefaultAudioPlayerManager() { private val enabledSources = mutableListOf() /** * The route planner. */ val routePlanner: AbstractRoutePlanner? by lazy { - val ipBlockList = config[ObsidianConfig.Lavaplayer.RateLimit.IpBlocks] + val ipBlockList = config[Obsidian.Lavaplayer.RateLimit.ipBlocks] if (ipBlockList.isEmpty()) { return@lazy null } @@ -59,14 +59,14 @@ class ObsidianPlayerManager : DefaultAudioPlayerManager() { } } - val blacklisted = config[ObsidianConfig.Lavaplayer.RateLimit.ExcludedIps].map { + val blacklisted = config[Obsidian.Lavaplayer.RateLimit.excludedIps].map { InetAddress.getByName(it) } val filter = Predicate { !blacklisted.contains(it) } - val searchTriggersFail = config[ObsidianConfig.Lavaplayer.RateLimit.SearchTriggersFail] + val searchTriggersFail = config[Obsidian.Lavaplayer.RateLimit.searchTriggersFail] - return@lazy when (config[ObsidianConfig.Lavaplayer.RateLimit.Strategy]) { + return@lazy when (config[Obsidian.Lavaplayer.RateLimit.strategy]) { "rotate-on-ban" -> RotatingIpRoutePlanner(ipBlocks, filter, searchTriggersFail) "load-balance" -> BalancingIpRoutePlanner(ipBlocks, filter, searchTriggersFail) "rotating-nano-switch" -> RotatingNanoIpRoutePlanner(ipBlocks, filter, searchTriggersFail) @@ -78,13 +78,13 @@ class ObsidianPlayerManager : DefaultAudioPlayerManager() { init { configuration.apply { isFilterHotSwapEnabled = true - if (config[ObsidianConfig.Lavaplayer.NonAllocating]) { + if (config[Obsidian.Lavaplayer.nonAllocating]) { logger.info("Using the non-allocating audio frame buffer.") setFrameBufferFactory(::NonAllocatingAudioFrameBuffer) } } - if (config[ObsidianConfig.Lavaplayer.GcMonitoring]) { + if (config[Obsidian.Lavaplayer.gcMonitoring]) { enableGcMonitoring() } @@ -92,18 +92,18 @@ class ObsidianPlayerManager : DefaultAudioPlayerManager() { } private fun registerSources() { - config[ObsidianConfig.Lavaplayer.EnabledSources] + config[Obsidian.Lavaplayer.enabledSources] .forEach { source -> when (source.toLowerCase()) { "youtube" -> { - val youtube = YoutubeAudioSourceManager(config[ObsidianConfig.Lavaplayer.YouTube.AllowSearch]).apply { - setPlaylistPageCount(config[ObsidianConfig.Lavaplayer.YouTube.PlaylistPageLimit]) + val youtube = YoutubeAudioSourceManager(config[Obsidian.Lavaplayer.YouTube.allowSearch]).apply { + setPlaylistPageCount(config[Obsidian.Lavaplayer.YouTube.playlistPageLimit]) if (routePlanner != null) { val rotator = YoutubeIpRotatorSetup(routePlanner) .forSource(this) - val retryLimit = config[ObsidianConfig.Lavaplayer.RateLimit.RetryLimit] + val retryLimit = config[Obsidian.Lavaplayer.RateLimit.retryLimit] if (retryLimit <= 0) { rotator.withRetryLimit(if (retryLimit == 0) Int.MAX_VALUE else retryLimit) } @@ -122,7 +122,7 @@ class ObsidianPlayerManager : DefaultAudioPlayerManager() { registerSourceManager( SoundCloudAudioSourceManager( - config[ObsidianConfig.Lavaplayer.AllowScSearch], + config[Obsidian.Lavaplayer.allowScSearch], dataReader, htmlDataLoader, formatHandler, @@ -132,8 +132,8 @@ class ObsidianPlayerManager : DefaultAudioPlayerManager() { } "nico" -> { - val email = config[ObsidianConfig.Lavaplayer.Nico.Email] - val password = config[ObsidianConfig.Lavaplayer.Nico.Password] + val email = config[Obsidian.Lavaplayer.Nico.email] + val password = config[Obsidian.Lavaplayer.Nico.password] if (email.isNotBlank() && password.isNotBlank()) { registerSourceManager(NicoAudioSourceManager(email, password)) @@ -160,6 +160,6 @@ class ObsidianPlayerManager : DefaultAudioPlayerManager() { } companion object { - private val logger: Logger = LoggerFactory.getLogger(ObsidianPlayerManager::class.java) + private val logger: Logger = LoggerFactory.getLogger(ObsidianAPM::class.java) } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/Link.kt b/Server/src/main/kotlin/obsidian/server/player/Player.kt similarity index 57% rename from Server/src/main/kotlin/obsidian/server/player/Link.kt rename to Server/src/main/kotlin/obsidian/server/player/Player.kt index 33c6fb1..3ee7347 100644 --- a/Server/src/main/kotlin/obsidian/server/player/Link.kt +++ b/Server/src/main/kotlin/obsidian/server/player/Player.kt @@ -22,34 +22,35 @@ import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame import io.netty.buffer.ByteBuf -import obsidian.bedrock.MediaConnection -import obsidian.bedrock.media.OpusAudioFrameProvider -import obsidian.server.Obsidian.playerManager +import moe.kyokobot.koe.MediaConnection +import moe.kyokobot.koe.media.OpusAudioFrameProvider +import obsidian.server.Application.players import obsidian.server.io.MagmaClient -import obsidian.server.player.filter.FilterChain +import obsidian.server.player.filter.Filters import java.nio.ByteBuffer -class Link( - val client: MagmaClient, - val guildId: Long -) { +class Player(val guildId: Long, val client: MagmaClient) { + /** - * Handles sending of player updates + * Handles all updates for this player. */ - val playerUpdates = PlayerUpdates(this) + val updates: PlayerUpdates by lazy { + PlayerUpdates(this) + } /** - * The frame counter. + * Audio player for receiving frames. */ - val frameCounter = FrameLossTracker() + val audioPlayer: AudioPlayer by lazy { + players.createPlayer() + .addEventListener(frameLossTracker) + .addEventListener(updates) + } /** - * The lavaplayer filter. + * Frame loss tracker. */ - val audioPlayer: AudioPlayer = playerManager.createPlayer() - .registerListener(playerUpdates) - .registerListener(frameCounter) - .registerListener(PlayerEvents(this)) + val frameLossTracker = FrameLossTracker() /** * Whether the player is currently playing a track. @@ -58,12 +59,12 @@ class Link( get() = audioPlayer.playingTrack != null && !audioPlayer.isPaused /** - * The current filter chain. + * The current filters that are enabled. */ - var filters: FilterChain = FilterChain(this) + var filters: Filters? = null set(value) { field = value - value.apply() + value?.applyTo(this) } /** @@ -71,11 +72,11 @@ class Link( */ suspend fun play(track: AudioTrack) { audioPlayer.playTrack(track) - playerUpdates.sendUpdate() + updates.sendUpdate() } /** - * Used to seek + * Convenience method for seeking to a specific position in the current track. */ fun seekTo(position: Long) { require(audioPlayer.playingTrack != null) { @@ -94,39 +95,49 @@ class Link( } /** - * Provides frames to the provided [MediaConnection] * - * @param mediaConnection */ - fun provideTo(mediaConnection: MediaConnection) { - mediaConnection.frameProvider = LinkFrameProvider(mediaConnection) + fun provideTo(connection: MediaConnection) { + connection.audioSender = OpusFrameProvider(connection) + } + + /** + * + */ + suspend fun destroy() { + updates.stop() + audioPlayer.destroy() } - inner class LinkFrameProvider(mediaConnection: MediaConnection) : OpusAudioFrameProvider(mediaConnection) { - private val lastFrame = MutableAudioFrame().apply { - val frameBuffer = ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize()) - setBuffer(frameBuffer) + inner class OpusFrameProvider(connection: MediaConnection) : OpusAudioFrameProvider(connection) { + + private val frameBuffer = ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize()) + private val lastFrame = MutableAudioFrame() + + init { + lastFrame.setBuffer(frameBuffer) } override fun canProvide(): Boolean { - val frame = audioPlayer.provide(lastFrame) - if (!frame) { - frameCounter.loss() + val success = audioPlayer.provide(lastFrame) + if (!success) { + frameLossTracker.loss() } - return frame + return success } override fun retrieveOpusFrame(targetBuffer: ByteBuf) { - frameCounter.success() - targetBuffer.writeBytes(lastFrame.data) + val buffered = frameBuffer.flip() + frameLossTracker.success() + targetBuffer.writeBytes(buffered) } } companion object { - fun AudioPlayer.registerListener(listener: AudioEventListener): AudioPlayer { + fun AudioPlayer.addEventListener(listener: AudioEventListener): AudioPlayer { addListener(listener) return this } } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerEvents.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerEvents.kt deleted file mode 100644 index d8f08c6..0000000 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerEvents.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.server.player - -import com.sedmelluq.discord.lavaplayer.player.AudioPlayer -import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException -import com.sedmelluq.discord.lavaplayer.track.AudioTrack -import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason -import kotlinx.coroutines.launch -import obsidian.server.io.TrackEndEvent -import obsidian.server.io.TrackExceptionEvent -import obsidian.server.io.TrackStartEvent -import obsidian.server.io.TrackStuckEvent -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -class PlayerEvents(private val link: Link) : AudioEventAdapter() { - override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack?, endReason: AudioTrackEndReason) { - link.client.launch { - val event = TrackEndEvent( - guildId = link.guildId, - track = track, - endReason = endReason - ) - - link.client.send(event) - } - } - - override fun onTrackStart(player: AudioPlayer?, track: AudioTrack) { - link.client.launch { - val event = TrackStartEvent( - guildId = link.guildId, - track = track - ) - - link.client.send(event) - } - } - - override fun onTrackStuck(player: AudioPlayer?, track: AudioTrack?, thresholdMs: Long) { - link.client.launch { - logger.warn("${track?.info?.title} got stuck! Threshold surpassed: $thresholdMs"); - - val event = TrackStuckEvent( - guildId = link.guildId, - track = track, - thresholdMs = thresholdMs - ) - - link.client.send(event) - } - } - - override fun onTrackException(player: AudioPlayer?, track: AudioTrack?, exception: FriendlyException) { - link.client.launch { - val event = TrackExceptionEvent( - guildId = link.guildId, - track = track, - exception = TrackExceptionEvent.Exception( - message = exception.message, - severity = exception.severity, - cause = exception.rootCause.message - ) - ) - - link.client.send(event) - } - } - - companion object { - private val logger: Logger = LoggerFactory.getLogger(PlayerEvents::class.java) - - val Throwable.rootCause: Throwable - get() { - var rootCause: Throwable? = this - while (rootCause!!.cause != null) { - rootCause = rootCause.cause - } - - return rootCause - } - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index ffbd2db..2be66c0 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -21,15 +21,14 @@ import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason import kotlinx.coroutines.launch -import obsidian.bedrock.util.Interval -import obsidian.server.Obsidian.config -import obsidian.server.io.CurrentTrack -import obsidian.server.io.Frames -import obsidian.server.io.PlayerUpdate +import obsidian.server.Application.config +import obsidian.server.io.ws.CurrentTrack +import obsidian.server.io.ws.PlayerUpdate +import obsidian.server.util.Interval +import obsidian.server.util.Obsidian import obsidian.server.util.TrackUtil -import obsidian.server.util.config.ObsidianConfig -class PlayerUpdates(val link: Link) : AudioEventAdapter() { +class PlayerUpdates(val player: Player) : AudioEventAdapter() { /** * Whether player updates should be sent. */ @@ -37,17 +36,11 @@ class PlayerUpdates(val link: Link) : AudioEventAdapter() { set(value) { field = value - link.client.launch { + player.client.websocket?.launch { if (value) start() else stop() } } - /** - * Whether a track is currently being played. - */ - val playing: Boolean - get() = link.playing - private val interval = Interval() /** @@ -55,7 +48,7 @@ class PlayerUpdates(val link: Link) : AudioEventAdapter() { */ suspend fun start() { if (!interval.started && enabled) { - interval.start(config[ObsidianConfig.PlayerUpdates.Interval], ::sendUpdate) + interval.start(config[Obsidian.PlayerUpdates.Interval], ::sendUpdate) } } @@ -69,32 +62,35 @@ class PlayerUpdates(val link: Link) : AudioEventAdapter() { } suspend fun sendUpdate() { - val currentTrack = CurrentTrack( - track = TrackUtil.encode(link.audioPlayer.playingTrack), - paused = link.audioPlayer.isPaused, - position = link.audioPlayer.playingTrack.position - ) - - val frames = Frames( - sent = link.frameCounter.success.sum(), - lost = link.frameCounter.loss.sum(), - usable = link.frameCounter.dataUsable + val update = PlayerUpdate( + guildId = player.guildId, + currentTrack = currentTrackFor(player), + frames = player.frameLossTracker.payload ) - link.client.send( - PlayerUpdate( - guildId = link.guildId, - currentTrack = currentTrack, - frames = frames - ) - ) + player.client.websocket?.send(update) } override fun onTrackStart(player: AudioPlayer?, track: AudioTrack?) { - link.client.launch { start() } + this.player.client.websocket?.launch { start() } } override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack?, endReason: AudioTrackEndReason?) { - link.client.launch { stop() } + this.player.client.websocket?.launch { stop() } + } + + companion object { + /** + * Returns a [CurrentTrack] for the provided [Player]. + * + * @param player + * Player to get the current track from + */ + fun currentTrackFor(player: Player): CurrentTrack = + CurrentTrack( + track = TrackUtil.encode(player.audioPlayer.playingTrack), + paused = player.audioPlayer.isPaused, + position = player.audioPlayer.playingTrack.position + ) } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt b/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt index f7c5715..de85a9d 100644 --- a/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt +++ b/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt @@ -18,10 +18,10 @@ package obsidian.server.player import com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler -class TrackEndMarkerHandler(private val link: Link) : TrackMarkerHandler { +class TrackEndMarkerHandler(private val player: Player) : TrackMarkerHandler { override fun handle(state: TrackMarkerHandler.MarkerState) { if (state == TrackMarkerHandler.MarkerState.REACHED || state == TrackMarkerHandler.MarkerState.BYPASSED) { - link.audioPlayer.stopTrack() + player.audioPlayer.stopTrack() } } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt index af1e9a2..d893450 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt @@ -23,6 +23,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter import obsidian.server.player.filter.Filter.Companion.isSet +import obsidian.server.util.NativeUtil @Serializable data class TimescaleFilter( diff --git a/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt b/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt new file mode 100644 index 0000000..6007fe8 --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.util + +import io.ktor.application.* +import io.ktor.auth.* +import io.ktor.auth.AuthenticationPipeline.Companion.RequestAuthentication +import io.ktor.http.* +import io.ktor.request.* +import io.ktor.response.* +import io.ktor.util.pipeline.* + +object AuthorizationPipeline { + /** + * The interceptor for use in [obsidianProvider] + */ + val interceptor: PipelineInterceptor = { ctx -> + val authorization = call.request.authorization() + ?: call.request.queryParameters["auth"] + + if (!Obsidian.Server.validateAuth(authorization)) { + val cause = when (authorization) { + null -> AuthenticationFailedCause.NoCredentials + else -> AuthenticationFailedCause.InvalidCredentials + } + + ctx.challenge("ObsidianAuth", cause) { + call.respond(HttpStatusCode.Unauthorized) + it.complete() + } + } + } + + /** + * Adds an authentication provider used by Obsidian. + */ + fun Authentication.Configuration.obsidianProvider() = provider { + pipeline.intercept(RequestAuthentication, interceptor) + } +} diff --git a/Server/src/main/kotlin/obsidian/server/util/ByteRingBuffer.kt b/Server/src/main/kotlin/obsidian/server/util/ByteRingBuffer.kt index 4f9341a..b612029 100644 --- a/Server/src/main/kotlin/obsidian/server/util/ByteRingBuffer.kt +++ b/Server/src/main/kotlin/obsidian/server/util/ByteRingBuffer.kt @@ -19,7 +19,7 @@ package obsidian.server.util import kotlin.math.min /** - * Based off + * Based off of * https://github.com/natanbc/andesite/blob/master/api/src/main/java/andesite/util/ByteRingBuffer.java */ @@ -137,4 +137,4 @@ class ByteRingBuffer(private var size: Int) : Iterable { i.inc().takeIf { mod < it } ?: 0 } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt b/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt index 4948d9e..7b5efe3 100644 --- a/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt +++ b/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt @@ -101,4 +101,4 @@ class CpuTimer { private const val ERROR = -1.0 private val logger: Logger = LoggerFactory.getLogger(CpuTimer::class.java) } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/bedrock/util/Interval.kt b/Server/src/main/kotlin/obsidian/server/util/Interval.kt similarity index 98% rename from Server/src/main/kotlin/obsidian/bedrock/util/Interval.kt rename to Server/src/main/kotlin/obsidian/server/util/Interval.kt index af5a129..f5452aa 100644 --- a/Server/src/main/kotlin/obsidian/bedrock/util/Interval.kt +++ b/Server/src/main/kotlin/obsidian/server/util/Interval.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package obsidian.bedrock.util +package obsidian.server.util import kotlinx.coroutines.* import kotlinx.coroutines.channels.ReceiveChannel @@ -95,4 +95,4 @@ class Interval(private val dispatcher: CoroutineDispatcher = Dispatchers.Default companion object { private val logger: Logger = LoggerFactory.getLogger(Interval::class.java) } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt new file mode 100644 index 0000000..ec1acef --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.util + +import io.netty.buffer.ByteBufAllocator +import io.netty.buffer.PooledByteBufAllocator +import io.netty.buffer.UnpooledByteBufAllocator +import moe.kyokobot.koe.Koe +import moe.kyokobot.koe.KoeOptions +import moe.kyokobot.koe.codec.FramePollerFactory +import moe.kyokobot.koe.codec.netty.NettyFramePollerFactory +import moe.kyokobot.koe.codec.udpqueue.UdpQueueFramePollerFactory +import moe.kyokobot.koe.gateway.GatewayVersion +import obsidian.server.Application.config +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +object KoeUtil { + + private val log: Logger = LoggerFactory.getLogger(KoeUtil::class.java) + + /** + * The koe instance + */ + val koe by lazy { + val options = KoeOptions.builder() + options.setFramePollerFactory(framePollerFactory) + options.setByteBufAllocator(allocator) + options.setGatewayVersion(gatewayVersion) + options.setHighPacketPriority(config[Obsidian.Koe.highPacketPriority]) + + println("koe") + Koe.koe(options.create()) + } + + /** + * Gateway version to use + */ + private val gatewayVersion: GatewayVersion by lazy { + when (config[Obsidian.Koe.gatewayVersion]) { + 5 -> GatewayVersion.V5 + 4 -> GatewayVersion.V4 + else -> { + log.info("Invalid gateway version, defaulting to v5.") + GatewayVersion.V5 + } + } + } + + /** + * The frame poller to use. + */ + private val framePollerFactory: FramePollerFactory by lazy { + when { +// NativeUtil.udpQueueAvailable && config[Obsidian.Koe.UdpQueue.enabled] -> { +// log.info("Enabling udp-queue") +// UdpQueueFramePollerFactory(config[Obsidian.Koe.UdpQueue.bufferDuration], config[Obsidian.Koe.UdpQueue.poolSize]) +// } + + else -> { +// if (config[Obsidian.Koe.UdpQueue.enabled]) { +// log.warn("This system and/or architecture appears to not support native audio sending, " +// + "GC pauses may cause your bot to stutter during playback.") +// } + + NettyFramePollerFactory() + } + } + } + + /** + * The byte-buf allocator to use + */ + private val allocator: ByteBufAllocator by lazy { + when (val configured = config[Obsidian.Koe.byteAllocator]) { + "pooled", "default" -> PooledByteBufAllocator.DEFAULT + "netty-default" -> ByteBufAllocator.DEFAULT + "unpooled" -> UnpooledByteBufAllocator.DEFAULT + else -> { + log.warn("Unknown byte-buf allocator '${configured}', defaulting to 'pooled'.") + PooledByteBufAllocator.DEFAULT + } + } + } + +} diff --git a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt index 05c4c97..fe49d29 100644 --- a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt @@ -18,9 +18,10 @@ package obsidian.server.util import com.github.natanbc.lavadsp.natives.TimescaleNativeLibLoader import com.github.natanbc.nativeloader.NativeLibLoader +import com.sedmelluq.discord.lavaplayer.natives.ConnectorNativeLibLoader +import com.sedmelluq.discord.lavaplayer.udpqueue.natives.UdpQueueManagerLibrary import org.slf4j.Logger import org.slf4j.LoggerFactory -import com.sedmelluq.discord.lavaplayer.natives.ConnectorNativeLibLoader /** * Based on https://github.com/natanbc/andesite/blob/master/src/main/java/andesite/util/NativeUtils.java @@ -30,12 +31,14 @@ import com.sedmelluq.discord.lavaplayer.natives.ConnectorNativeLibLoader object NativeUtil { var timescaleAvailable: Boolean = false + var udpQueueAvailable: Boolean = false /* private shit */ private val logger: Logger = LoggerFactory.getLogger(NativeUtil::class.java) // loaders private val CONNECTOR_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "connector") + private val UDP_QUEUE_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "udpqueue") // class names private const val LOAD_RESULT_NAME = "com.sedmelluq.lava.common.natives.NativeLibraryLoader\$LoadResult" @@ -56,6 +59,7 @@ object NativeUtil { */ fun load() { loadConnector() + udpQueueAvailable = loadUdpQueue() timescaleAvailable = loadTimescale() } @@ -92,6 +96,27 @@ object NativeUtil { } } + /** + * Loads udp-queue natives + */ + private fun loadUdpQueue() = try { + /* Load the lp-cross version of the library. */ + UDP_QUEUE_LOADER.load() + + /* mark lavaplayer's loader as loaded to avoid failing when loading mpg123 on windows/attempting to load connector again. */ + with(UdpQueueManagerLibrary::class.java.getDeclaredField("nativeLoader")) { + isAccessible = true + markLoaded(get(null)) + } + + /* return true */ + logger.info("Loaded udp-queue library.") + true + } catch (ex: Throwable) { + logger.warn("Error loading udp-queue library.", ex) + false + } + private fun markLoaded(loader: Any) { val previousResultField = loader.javaClass.getDeclaredField("previousResult") previousResultField.isAccessible = true diff --git a/Server/src/main/kotlin/obsidian/server/util/Obsidian.kt b/Server/src/main/kotlin/obsidian/server/util/Obsidian.kt new file mode 100644 index 0000000..6dee19d --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/util/Obsidian.kt @@ -0,0 +1,200 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.util + +import com.uchuhimo.konf.ConfigSpec +import moe.kyokobot.koe.codec.udpqueue.UdpQueueFramePollerFactory.Companion.DEFAULT_BUFFER_DURATION +import obsidian.server.Application.config + +object Obsidian : ConfigSpec() { + /** + * Whether a client name is required. + */ + val requireClientName by optional(false, "require-client-name") + + /** + * Options related to the HTTP server. + */ + object Server : ConfigSpec() { + /** + * The host the server will bind to. + */ + val host by optional("0.0.0.0") + + /** + * The port to listen for requests on. + */ + val port by optional(3030) + + /** + * The authentication for HTTP endpoints and the WebSocket server. + */ + val auth by optional("") + + /** + * Used to validate a string given as authorization. + * + * @param given The given authorization string. + * + * @return true, if the given authorization matches the configured password. + */ + fun validateAuth(given: String?): Boolean = when { + config[auth].isEmpty() -> true + else -> given == config[auth] + } + } + + /** + * Options related to player updates + */ + object PlayerUpdates : ConfigSpec("player-updates") { + /** + * The delay (in milliseconds) between each player update. + */ + val Interval by optional(5000L, "interval") + + /** + * Whether the filters object should be sent with Player Updates + */ + val SendFilters by optional(true, "send-filters") + } + + /** + * Options related to Koe, the discord media library used by Obsidian. + */ + object Koe : ConfigSpec("koe") { + /** + * The byte-buf allocator to use + */ + val byteAllocator by optional("pooled", "byte-allocator") + + /** + * Whether packets should be prioritized + */ + val highPacketPriority by optional(true, "high-packet-priority") + + /** + * The voice server version to use, defaults to v5 + */ + val gatewayVersion by optional(5, "gateway-version") + + object UdpQueue : ConfigSpec("udp-queue") { + /** + * Whether udp-queue is enabled. + */ + val enabled by optional(true) + + /** + * The buffer duration, in milliseconds. + */ + val bufferDuration by optional(DEFAULT_BUFFER_DURATION, "buffer-duration") + + /** + * The number of threads to create, defaults to twice the amount of processors. + */ + val poolSize by optional(Runtime.getRuntime().availableProcessors() * 2, "pool-size") + } + } + + /** + * Options related to lavaplayer, the library used for audio. + */ + object Lavaplayer : ConfigSpec("lavaplayer") { + /** + * Whether garbage collection should be monitored. + */ + val gcMonitoring by optional(false, "gc-monitoring") + + /** + * Whether lavaplayer shouldn't allocate audio frames + */ + val nonAllocating by optional(false, "non-allocating") + + /** + * Names of sources that will be enabled. + */ + val enabledSources by optional( + setOf( + "youtube", + "yarn", + "bandcamp", + "twitch", + "vimeo", + "nico", + "soundcloud", + "local", + "http" + ), "enabled-sources" + ) + + /** + * Whether `scsearch:` should be allowed. + */ + val allowScSearch by optional(true, "allow-scsearch") + + object RateLimit : ConfigSpec("rate-limit") { + /** + * Ip blocks to use. + */ + val ipBlocks by optional(emptyList(), "ip-blocks") + + /** + * IPs which should be excluded from usage by the route planner + */ + val excludedIps by optional(emptyList(), "excluded-ips") + + /** + * The route planner strategy to use. + */ + val strategy by optional("rotate-on-ban") + + /** + * Whether a search 429 should trigger marking the ip as failing. + */ + val searchTriggersFail by optional(true, "search-triggers-fail") + + /** + * -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times + */ + val retryLimit by optional(-1, "retry-limit") + } + + object Nico : ConfigSpec("nico") { + /** + * The email to use for the Nico Source. + */ + val email by optional("") + + /** + * The password to use for the Nico Source. + */ + val password by optional("") + } + + object YouTube : ConfigSpec("youtube") { + /** + * Whether `ytsearch:` should be allowed. + */ + val allowSearch by optional(true, "allow-search") + + /** + * Total number of pages (100 tracks per page) to load + */ + val playlistPageLimit by optional(6, "playlist-page-limit") + } + } +} diff --git a/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt b/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt index a2ee00d..1ead148 100644 --- a/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt +++ b/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt @@ -20,17 +20,13 @@ import java.util.* import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicInteger -val counter = AtomicInteger() -fun threadFactory( - name: String, - daemon: Boolean? = null, - priority: Int? = null, - exceptionHandler: Thread.UncaughtExceptionHandler? = null -) = - ThreadFactory { runnable -> - Thread(runnable, name.format(Locale.ROOT, counter.getAndIncrement())).apply { - daemon?.let { this.isDaemon = it } - priority?.let { this.priority = priority } - exceptionHandler?.let { this.uncaughtExceptionHandler = it } +fun threadFactory(name: String, daemon: Boolean = false, priority: Int? = null): ThreadFactory { + val counter = AtomicInteger() + return ThreadFactory { runnable -> + Thread(runnable).apply { + this.name = name.format(Locale.ROOT, counter.getAndIncrement()) + this.isDaemon = daemon + priority?.let { this.priority = it } } } +} diff --git a/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt b/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt index 8d6e62a..0c81815 100644 --- a/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt @@ -19,12 +19,26 @@ package obsidian.server.util import com.sedmelluq.discord.lavaplayer.tools.io.MessageInput import com.sedmelluq.discord.lavaplayer.tools.io.MessageOutput import com.sedmelluq.discord.lavaplayer.track.AudioTrack -import obsidian.server.Obsidian.playerManager +import obsidian.server.Application.players import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.util.* object TrackUtil { + /** + * Base64 decoder used by [decode] + */ + private val decoder: Base64.Decoder by lazy { + Base64.getDecoder() + } + + /** + * Base64 encoder used by [encode] + */ + private val encoder: Base64.Encoder by lazy { + Base64.getEncoder() + } + /** * Decodes a base64 encoded string into a usable [AudioTrack] * @@ -33,14 +47,10 @@ object TrackUtil { * @return The decoded [AudioTrack] */ fun decode(encodedTrack: String): AudioTrack { - val decoded = Base64.getDecoder() - .decode(encodedTrack) - - val inputStream = ByteArrayInputStream(decoded) - val track = playerManager.decodeTrack(MessageInput(inputStream))!!.decodedTrack - - inputStream.close() - return track + val inputStream = ByteArrayInputStream(decoder.decode(encodedTrack)) + return inputStream.use { + players.decodeTrack(MessageInput(it))!!.decodedTrack + } } /** @@ -52,12 +62,9 @@ object TrackUtil { */ fun encode(track: AudioTrack): String { val outputStream = ByteArrayOutputStream() - playerManager.encodeTrack(MessageOutput(outputStream), track) - - val encoded = Base64.getEncoder() - .encodeToString(outputStream.toByteArray()) - - outputStream.close() - return encoded + return outputStream.use { + players.encodeTrack(MessageOutput(it), track) + encoder.encodeToString(it.toByteArray()) + } } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/FramePoller.kt b/Server/src/main/kotlin/obsidian/server/util/VersionInfo.kt similarity index 62% rename from Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/FramePoller.kt rename to Server/src/main/kotlin/obsidian/server/util/VersionInfo.kt index 942e3c7..a392613 100644 --- a/Server/src/main/kotlin/obsidian/bedrock/codec/framePoller/FramePoller.kt +++ b/Server/src/main/kotlin/obsidian/server/util/VersionInfo.kt @@ -14,21 +14,21 @@ * limitations under the License. */ -package obsidian.bedrock.codec.framePoller +package obsidian.server.util -interface FramePoller { - /** - * Used to check whether this FramePoller is currently polling. - */ - val polling: Boolean +object VersionInfo { + private val stream = VersionInfo::class.java.classLoader.getResourceAsStream("version.txt") + private val versionTxt = stream?.reader()?.readText()?.split('\n') /** - * Used to start polling. + * Current version of Mixtape. */ - suspend fun start() + val VERSION = versionTxt?.get(0) + ?: "1.0.0" /** - * Used to stop polling. + * Current git revision. */ - fun stop() -} \ No newline at end of file + val GIT_REVISION = versionTxt?.get(1) + ?: "unknown" +} diff --git a/Server/src/main/kotlin/obsidian/server/util/config/LoggingConfig.kt b/Server/src/main/kotlin/obsidian/server/util/config/LoggingConfig.kt deleted file mode 100644 index 85376e2..0000000 --- a/Server/src/main/kotlin/obsidian/server/util/config/LoggingConfig.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.server.util.config - -import com.uchuhimo.konf.ConfigSpec - -object LoggingConfig : ConfigSpec("logging") { - - object Level : ConfigSpec("level") { - /** - * Root logging level - */ - val Root by optional("INFO") - - /** - * Obsidian logging level - */ - val Obsidian by optional("INFO") - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/server/util/config/ObsidianConfig.kt b/Server/src/main/kotlin/obsidian/server/util/config/ObsidianConfig.kt deleted file mode 100644 index d2c93d0..0000000 --- a/Server/src/main/kotlin/obsidian/server/util/config/ObsidianConfig.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package obsidian.server.util.config - -import com.uchuhimo.konf.ConfigSpec -import obsidian.server.Obsidian.config - -object ObsidianConfig : ConfigSpec("obsidian") { - /** - * The address that the server will bind to. - * - * `obsidian.host` - */ - val Host by optional("0.0.0.0") - - /** - * The server port. - * - * `obsidian.port` - */ - val Port by optional(3030) - - /** - * The server password. - * - * `obsidian.password` - */ - val Password by optional("") - - /** - * Whether obsidian should immediately start providing frames after connecting to the voice server. - * - * `immediately-provide` - */ - val ImmediatelyProvide by optional(true, "immediately-provide") - - /** - * Whether the `Client-Name` header is required for Clients. - * - * `require-client-name` - */ - val RequireClientName by optional(false, "require-client-name") - - /** - * Used to validate a string given as authorization. - * - * @param given The given authorization string. - * - * @return true, if the given authorization matches the configured password. - */ - fun validateAuth(given: String?): Boolean = when { - config[Password].isEmpty() -> true - else -> given == config[Password] - } - - object PlayerUpdates : ConfigSpec("player-updates") { - /** - * The delay (in milliseconds) between each player update. - * - * `obsidian.player-updates.interval` - */ - val Interval by optional(5000L, "interval") - - /** - * Whether the filters object should be sent with Player Updates - * - * `obsidian.player-updates.send-filters` - */ - val SendFilters by optional(true, "send-filters") - } - - object Lavaplayer : ConfigSpec("lavaplayer") { - /** - * Whether garbage collection should be monitored. - * - * `obsidian.lavaplayer.gc-monitoring` - */ - val GcMonitoring by optional(false, "gc-monitoring") - - /** - * Whether lavaplayer shouldn't allocate audio frames - * - * `obsidian.lavaplayer.non-allocating` - */ - val NonAllocating by optional(false, "non-allocating") - - /** - * Names of sources that will be enabled. - * - * `obsidian.lavaplayer.enabled-sources` - */ - val EnabledSources by optional( - setOf( - "youtube", - "yarn", - "bandcamp", - "twitch", - "vimeo", - "nico", - "soundcloud", - "local", - "http" - ), "enabled-sources" - ) - - /** - * Whether `scsearch:` should be allowed. - * - * `obsidian.lavaplayer.allow-scsearch` - */ - val AllowScSearch by optional(true, "allow-scsearch") - - object RateLimit : ConfigSpec("rate-limit") { - /** - * Ip blocks to use. - * - * `obsidian.lavaplayer.rate-limit.ip-blocks` - */ - val IpBlocks by optional(emptyList(), "ip-blocks") - - /** - * IPs which should be excluded from usage by the route planner - * - * `obsidian.lavaplayer.rate-limit.excluded-ips` - */ - val ExcludedIps by optional(emptyList(), "excluded-ips") - - /** - * The route planner strategy to use. - * - * `obsidian.lavaplayer.rate-limit.strategy` - */ - val Strategy by optional("rotate-on-ban") - - /** - * Whether a search 429 should trigger marking the ip as failing. - * - * `obsidian.lavaplayer.rate-limit.search-triggers-fail` - */ - val SearchTriggersFail by optional(true, "search-triggers-fail") - - /** - * -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times - * - * `obsidian.lavaplayer.rate-limit.retry-limit` - */ - val RetryLimit by optional(-1, "retry-limit") - } - - object Nico : ConfigSpec("nico") { - /** - * The email to use for the Nico Source. - * - * `obsidian.lavaplayer.nico.email` - */ - val Email by optional("") - - /** - * The password to use for the Nico Source. - * - * `obsidian.lavaplayer.nico.password` - */ - val Password by optional("") - } - - object YouTube : ConfigSpec("youtube") { - /** - * Whether `ytsearch:` should be allowed. - * - * `obsidian.lavaplayer.youtube.allow-ytsearch` - */ - val AllowSearch by optional(true, "allow-search") - - /** - * Total number of pages (100 tracks per page) to load - * - * `obsidian.lavaplayer.youtube.playlist-page-limit` - */ - val PlaylistPageLimit by optional(6, "playlist-page-limit") - } - } -} \ No newline at end of file diff --git a/Server/src/main/kotlin/obsidian/server/io/search/AudioLoader.kt b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt similarity index 98% rename from Server/src/main/kotlin/obsidian/server/io/search/AudioLoader.kt rename to Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt index e049b37..4b715fc 100644 --- a/Server/src/main/kotlin/obsidian/server/io/search/AudioLoader.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt @@ -14,7 +14,8 @@ * limitations under the License. */ -package obsidian.server.io.search +package obsidian.server.util.search + import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager @@ -88,4 +89,4 @@ class AudioLoader(private val audioPlayerManager: AudioPlayerManager) : AudioLoa private val logger = LoggerFactory.getLogger(AudioLoader::class.java) private val NO_MATCHES: LoadResult = LoadResult(LoadType.NO_MATCHES, emptyList(), null, null) } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/io/search/LoadResult.kt b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt similarity index 97% rename from Server/src/main/kotlin/obsidian/server/io/search/LoadResult.kt rename to Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt index bbce834..870cd1c 100644 --- a/Server/src/main/kotlin/obsidian/server/io/search/LoadResult.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt @@ -14,11 +14,12 @@ * limitations under the License. */ -package obsidian.server.io.search +package obsidian.server.util.search import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack + class LoadResult { var loadResultType: LoadType private set @@ -50,4 +51,4 @@ class LoadResult { selectedTrack = null this.exception = exception } -} \ No newline at end of file +} diff --git a/Server/src/main/kotlin/obsidian/server/io/search/LoadType.kt b/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt similarity index 94% rename from Server/src/main/kotlin/obsidian/server/io/search/LoadType.kt rename to Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt index 4009b24..fc3b91f 100644 --- a/Server/src/main/kotlin/obsidian/server/io/search/LoadType.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package obsidian.server.io.search +package obsidian.server.util.search enum class LoadType { TRACK_LOADED, @@ -22,4 +22,4 @@ enum class LoadType { SEARCH_RESULT, NO_MATCHES, LOAD_FAILED -} \ No newline at end of file +} diff --git a/Server/src/main/resources/logback.xml b/Server/src/main/resources/logback.xml index 92b880e..dfa50da 100644 --- a/Server/src/main/resources/logback.xml +++ b/Server/src/main/resources/logback.xml @@ -7,7 +7,7 @@ - + - \ No newline at end of file + diff --git a/Server/src/main/resources/version.txt b/Server/src/main/resources/version.txt new file mode 100644 index 0000000..0f969b9 --- /dev/null +++ b/Server/src/main/resources/version.txt @@ -0,0 +1,2 @@ +2.0.0 +0c6b31c \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 40bbbfe..0000000 --- a/build.gradle +++ /dev/null @@ -1,77 +0,0 @@ -allprojects { - repositories { - jcenter() - - maven { url "https://jitpack.io" } - maven { url "https://dl.bintray.com/natanbc/maven" } - maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } - maven { url "https://m2.dv8tion.net/releases" } - } - - apply plugin: 'idea' - group = "gg.mixtape.obsidian" -} - -subprojects { - buildscript { - ext { - shadow_version = "6.1.0" - kotlin_version = "1.4.32" - } - - repositories { - maven { url "https://plugins.gradle.org/m2/" } - mavenCentral() - } - - dependencies { - classpath "com.github.jengelman.gradle.plugins:shadow:${shadow_version}" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" - classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - } - } - - - apply plugin: "java" - - sourceCompatibility = 11 - targetCompatibility = 11 - - compileJava.options.encoding = 'UTF-8' - compileJava.options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" - - tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' - } - - ext { - // kotlin - kotlinx_coroutines_version = "1.4.3" - - // audio - lavaplayer_version = "1.3.76" - lavadsp_version = "0.7.7" - netty_version = "4.1.63.Final" - lavaplayer_ip_rotator_config = "0.2.3" - native_loader_version = "0.7.0" - lpcross_version = "0.1.1" - - // logging - logback_version = "1.2.3" - mordant_version = "2.0.0-beta1" - - // serialization - serialization_json_version = "1.1.0" - - // config - konf_version = "1.1.2" - - // ktor - ktor_version = "1.5.3" - } -} - -ext { - moduleName = "Obsidian-Root" -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c310c23 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,27 @@ +allprojects { + repositories { + maven("https://jitpack.io") + maven("https://oss.sonatype.org/content/repositories/snapshots/") + maven("https://m2.dv8tion.net/releases") + mavenCentral() + } + + group = "gg.mixtape.obsidian" + apply(plugin = "idea") +} + +subprojects { + buildscript { + repositories { + gradlePluginPortal() + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}") + classpath("org.jetbrains.kotlin:kotlin-serialization:${Versions.kotlin}") + } + } + + apply(plugin = "java") +} diff --git a/Server/src/main/kotlin/obsidian/bedrock/VoiceServerInfo.kt b/buildSrc/build.gradle.kts similarity index 83% rename from Server/src/main/kotlin/obsidian/bedrock/VoiceServerInfo.kt rename to buildSrc/build.gradle.kts index 8624bda..2acc943 100644 --- a/Server/src/main/kotlin/obsidian/bedrock/VoiceServerInfo.kt +++ b/buildSrc/build.gradle.kts @@ -14,10 +14,10 @@ * limitations under the License. */ -package obsidian.bedrock +plugins { + `kotlin-dsl` +} -data class VoiceServerInfo( - val sessionId: String, - val token: String, - val endpoint: String -) +repositories { + mavenCentral() +} diff --git a/Server/src/main/kotlin/obsidian/bedrock/util/RTPHeaderWriter.kt b/buildSrc/src/main/kotlin/Compiler.kt similarity index 61% rename from Server/src/main/kotlin/obsidian/bedrock/util/RTPHeaderWriter.kt rename to buildSrc/src/main/kotlin/Compiler.kt index f5fd43b..31f9144 100644 --- a/Server/src/main/kotlin/obsidian/bedrock/util/RTPHeaderWriter.kt +++ b/buildSrc/src/main/kotlin/Compiler.kt @@ -14,15 +14,9 @@ * limitations under the License. */ -package obsidian.bedrock.util - -import io.netty.buffer.ByteBuf -import kotlin.experimental.and - -fun writeV2(output: ByteBuf, payloadType: Byte, seq: Int, timestamp: Int, ssrc: Int, extension: Boolean) { - output.writeByte(if (extension) 0x90 else 0x80) - output.writeByte(payloadType.and(0x7f).toInt()) - output.writeChar(seq) - output.writeInt(timestamp) - output.writeInt(ssrc) +object CompilerArgs { + const val experimentalStdlibApi = "-Xopt-in=kotlin.ExperimentalStdlibApi" + const val obsoleteCoroutinesApi = "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi" + const val experimentalCoroutinesApi = "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + const val experimentalLocationsApi = "-Xopt-in=io.ktor.locations.KtorExperimentalLocationsAPI" } diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt new file mode 100644 index 0000000..b47fcd8 --- /dev/null +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +object Versions { + const val shadow = "7.0.0" + const val kotlin = "1.4.32" + const val kotlinxCoroutines = "1.4.3" + const val lavaplayer = "1.3.76" + const val lavadsp = "0.7.7" + const val netty = "4.1.63.Final" + const val lavaplayerIpRotator = "0.2.3" + const val nativeLoader = "0.7.0" + const val koe = "master-SNAPSHOT" + const val lpCross = "0.1.1" + const val logback = "1.2.3" + const val mordant = "2.0.0-beta1" + const val serializationJson = "1.1.0" + const val konf = "1.1.2" + const val ktor = "1.5.4" +} + +object Dependencies { + const val kotlin = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" + const val kotlinxCoroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinxCoroutines}" + const val kotlinxCoroutinesJdk8 = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${Versions.kotlinxCoroutines}" + const val kotlinxSerialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.serializationJson}" + + const val lavaplayer = "com.sedmelluq:lavaplayer:${Versions.lavaplayer}" + const val lavaplayerIpRotator = "com.sedmelluq:lavaplayer-ext-youtube-rotator:${Versions.lavaplayerIpRotator}" + + const val lavadsp = "com.github.natanbc:lavadsp:${Versions.lavadsp}" + const val lpCross = "com.github.natanbc:lp-cross:${Versions.lpCross}" + const val nativeLoader = "com.github.natanbc:native-loader:${Versions.nativeLoader}" + + const val koeCore = "moe.kyokobot.koe:core:${Versions.koe}" + + const val logback = "ch.qos.logback:logback-classic:${Versions.logback}" + const val mordant = "com.github.ajalt.mordant:mordant:${Versions.mordant}" + + const val konfCore = "com.github.uchuhimo.konf:konf-core:${Versions.konf}" + const val konfYaml = "com.github.uchuhimo.konf:konf-yaml:${Versions.konf}" + + const val ktorServerCore = "io.ktor:ktor-server-core:${Versions.ktor}" + const val ktorServerCio = "io.ktor:ktor-server-cio:${Versions.ktor}" + const val ktorLocations = "io.ktor:ktor-locations:${Versions.ktor}" + const val ktorWebSockets = "io.ktor:ktor-websockets:${Versions.ktor}" + const val ktorSerialization = "io.ktor:ktor-serialization:${Versions.ktor}" +} diff --git a/Server/src/main/kotlin/obsidian/bedrock/gateway/SpeakingFlags.kt b/buildSrc/src/main/kotlin/Project.kt similarity index 80% rename from Server/src/main/kotlin/obsidian/bedrock/gateway/SpeakingFlags.kt rename to buildSrc/src/main/kotlin/Project.kt index 4c59817..9bf1e55 100644 --- a/Server/src/main/kotlin/obsidian/bedrock/gateway/SpeakingFlags.kt +++ b/buildSrc/src/main/kotlin/Project.kt @@ -14,10 +14,7 @@ * limitations under the License. */ -package obsidian.bedrock.gateway - -object SpeakingFlags { - const val NORMAL = 1 - const val SOUND_SHARE = 1 shl 1 - const val PRIORITY = 1 shl 2 -} \ No newline at end of file +object Project { + const val jvmTarget = "11" + const val mainClassName = "obsidian.server.Application" +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index be52383..f371643 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From e0d81ec7003c3f5632684d82083f2d9bf3349dfc Mon Sep 17 00:00:00 2001 From: melike2d Date: Wed, 5 May 2021 20:12:24 -0700 Subject: [PATCH 03/46] :memo: redo api docs (wip) --- API.md | 24 +- .../server/io/{routes => rest}/planner.kt | 0 .../server/io/{routes => rest}/players.kt | 0 .../server/io/{routes => rest}/tracks.kt | 0 api/README.md | 30 ++ api/ws-rest.md | 14 + api/ws/README.md | 11 + api/ws/payloads.md | 399 ++++++++++++++++++ api/ws/protocol.md | 46 ++ 9 files changed, 519 insertions(+), 5 deletions(-) rename Server/src/main/kotlin/obsidian/server/io/{routes => rest}/planner.kt (100%) rename Server/src/main/kotlin/obsidian/server/io/{routes => rest}/players.kt (100%) rename Server/src/main/kotlin/obsidian/server/io/{routes => rest}/tracks.kt (100%) create mode 100644 api/README.md create mode 100644 api/ws-rest.md create mode 100644 api/ws/README.md create mode 100644 api/ws/payloads.md create mode 100644 api/ws/protocol.md diff --git a/API.md b/API.md index 17ffc9e..ed7a982 100644 --- a/API.md +++ b/API.md @@ -9,13 +9,10 @@ Magma is the name for the WebSocket and REST server! --- -- **Current Version:** 1.0.0-pre +- **Current Version:** 2.0.0 ## Magma REST -As of version `1.0.0` of obsidian the REST API is only used for loading tracks. This will most likely change in future -releases. - ### Route Planner Allows clients to view the route planner and free-up addresses. @@ -101,6 +98,22 @@ Authorization: *204 - No Content* +### Player Controller + +Each request must have a `User-Id` header or query parameter containing your bot's user id + +- **Base Path:** `/players/{guild id}` + +The `Client-Name` header or query parameter may be required if the node you're using requires it. + +| endpoint | description | +| :------- | :------------------------- | +| / | returns info on the player | +| /play | plays +| +| +| + ### Tracks Controller Allows non-jvm clients to search and decode tracks using Obsidian! @@ -131,7 +144,8 @@ Authorization: "length": 270000, "position": 0, "is_stream": false, - "is_seekable": true + "is_seekable": true, + "source_name": "youtube" } } ], diff --git a/Server/src/main/kotlin/obsidian/server/io/routes/planner.kt b/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt similarity index 100% rename from Server/src/main/kotlin/obsidian/server/io/routes/planner.kt rename to Server/src/main/kotlin/obsidian/server/io/rest/planner.kt diff --git a/Server/src/main/kotlin/obsidian/server/io/routes/players.kt b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt similarity index 100% rename from Server/src/main/kotlin/obsidian/server/io/routes/players.kt rename to Server/src/main/kotlin/obsidian/server/io/rest/players.kt diff --git a/Server/src/main/kotlin/obsidian/server/io/routes/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt similarity index 100% rename from Server/src/main/kotlin/obsidian/server/io/routes/tracks.kt rename to Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..9aa1d0b --- /dev/null +++ b/api/README.md @@ -0,0 +1,30 @@ +# Obsidian API Documentation + +Welcome to the Obsidian API Documentation! This document describes mostly everything about the Web Socket and HTTP server. + +###### What's Magma? + +Magma is the name for the Web Socket and REST server! + +## Magma + +Links documenting the specific of Magma. + +- [Using REST & The Web Socket](/ws-rest.md) + +### Rest + +- [Controlling Players](/rest/players.md) +- [Managing the Route Planner](/rest/planner.md) +- [Loading Tracks](/rest/tracks.md) + +### Web Socket + +- [Protocol](/ws/protocol.md) +- [Resuming](/ws/resuming.md) +- [Payloads](/ws/protocol.md) + +--- + +- **Current Version:** 2.0.0 + diff --git a/api/ws-rest.md b/api/ws-rest.md new file mode 100644 index 0000000..0354f0c --- /dev/null +++ b/api/ws-rest.md @@ -0,0 +1,14 @@ +# WebSocket & REST + +Magma is the name of our WebSocket & REST server, we use [Ktor](https://ktor.io) because it is completely in Kotlin and is super fast! + +## Conventions + +Conventions used by Obsidian. + +### Naming + +Everything should use `snake_case`, this includes payloads that are being sent and received. + +For payloads that are being sent, the serialization library used by Obsidian can detect pascal & camel case fields. This does not mean you should use said naming conventions. + diff --git a/api/ws/README.md b/api/ws/README.md new file mode 100644 index 0000000..ab8ff0c --- /dev/null +++ b/api/ws/README.md @@ -0,0 +1,11 @@ +# Web Socket + +Files documenting the websocket + +- [**Protocol**](/protocol.md): close codes, required headers, and payload structure. +- [**Payloads**](/payloads.md): received and sent payloads. + +--- + +**Last Edited**: 2021/05/05 yyyy/mm/dd + diff --git a/api/ws/payloads.md b/api/ws/payloads.md new file mode 100644 index 0000000..095dd55 --- /dev/null +++ b/api/ws/payloads.md @@ -0,0 +1,399 @@ +# Web-socket Payloads + +Magma provides several operations for controlling players and the web-socket connection. + +## Op Codes + +| Code | Name | Description | +| :--: | :---------------------------------------------- | :----------------------------------------------------------- | +| 0 | [Submit Voice Server](#submit-voice-server) | allows obsidian to play music in a voice channel | +| 1 | [**Stats**](#stats) | contains useful statistics like resource usage and player count | +| 2 | [Setup Resuming](#setup-resuming) | configures session resuming, see [resuming](/resuming.md) | +| 3 | [Setup Dispatch Buffer](#setup-dispatch-buffer) | configures he dispatch buffer, see [resuming#buffer](/resuming.md#buffer) | +| 4 | [**Player event**](#player-events) | dispatched when a player event occurs, e.g. track end | +| 5 | [**Player update**](#player-update) | contains possibly useful information about a player, e.g. track position | +| 6 | [Play Track](#play-track) | plays the supplied track | +| 7 | [Stop track](#stop-track) | stops the currently playing track, if any. | +| 8 | [Pause](#pause) | configures the pause state of a player | +| 9 | [Filters](#filters) | configures the filters for the player | +| 10 | [Seek](#seek) | seeks to the specified position in the current track, if any. | +| 11 | [Destroy](#destroy) | destroys the player with the supplied guild id. | +| 12 | [Configure](#configure) | configures multiple player options, such as the pause state and filters. | + +Names in **bold** represent payloads that are received by the client, non-bolded names represent payloads received by the server + +## Submit Voice Server + +The equivalent of `voiceUpdate` for lavalink and `voice-state-update` for Andesite. + +```json +{ + "op": 0, + "d": { + "guild_id": "751571246189379610", + "token": "the voice server token", + "session_id": "voice server session id", + "endpoint": "smart.loyal.discord.gg" + } +} +``` + +- `token` and `endpoint` can be received from Discord's `VOICE_SERVER_UPDATE` dispatch event. +- `session_id` can be received from Discord's `VOICE_STATE_UPDATE` dispatch event. + +## Stats + +```json +{ + "op": 1, + "d": { + "memory": { + "heap_used": { + "init": 130023424, + "max": 2061500416, + "committed": 132120576, + "used": 55584624 + }, + "non_heap_used": { + "init": 39387136, + "max": -1, + "committed": 72859648, + "used": 39730328 + } + }, + "cpu": { + "cores": 4, + "system_load": 0.009758602978941962, + "process_load": 0.09655880842321521 + }, + "threads": { + "running": 17, + "daemon": 16, + "peak": 17, + "total_started": 19 + }, + "frames": [ + { + "guild_id": "751571246189379610", + "loss": 0, + "sent": 3011, + "usable": true + } + ], + "players": { + "active": 1, + "total": 0 + } + } +} +``` + +## Setup Resuming + +- **timeout**: is the amount of time in milliseconds before the session gets destroyed. + +```json +{ + "op": 2, + "d": { + "key": "fduivbubBIvVuVDwabu", + "timeout": 60000 + } +} +``` + +if any players are active the client won't be removed due to the implementation of player endpoints. + +## Setup Dispatch Buffer + +- **timeout**: timeout *in milliseconds* + +```json +{ + "op": 3, + "d": { + "timeout": 60000 + } +} +``` + +## Player Events + +List of current player events. *Example:* + +```json +{ + "op": 4, + "d": { + "type": "TRACK_START", + "guild_id": "751571246189379610", + "track": "QAAAmAIAO0tTSSDigJMgUGF0aWVuY2UgKGZlYXQuIFlVTkdCTFVEICYgUG9sbyBHKSBbT2ZmaWNpYWwgQXVkaW9dAANLU0kAAAAAAALG8AALTXJmVTRhVGNVYU0AAQAraHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1NcmZVNGFUY1VhTQAHeW91dHViZQAAAAAAAAAA" + } +} +``` + +code blocks below represent data in the d field + +### `TRACK_START` + +```JSON +{ + "type": "TRACK_START", + "track": "..." +} +``` + +--- + +### `TRACK_END` + +```json +{ + "type": "TRACK_END", + "track": "...", + "reason": "REPLACED" +} +``` + +#### End Reasons + +- **STOPPED**, **REPLACED**, **CLEANUP**, **LOAD_FAILED**, **FINISHED** + +for more information visit [AudioTrackEndReason.java](https://github.com/sedmelluq/lavaplayer/blob/master/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackEndReason.java) + +--- + +### `TRACK_STUCK` + +- `threshold_ms` the wait threshold that was exceeded for this event to trigger. + +```json +{ + "track": "...", + "threshold_ms": 1000 +} +``` + +--- + +### `TRACK_EXCEPTION` + +```json +{ + "track": "...", + "exception": { + "message": "This video is too cool for the people listening.", + "cause": "Lack of coolness by the listeners", + "severity": "COMMON" + } +} +``` + +#### Exception Severities + +- **COMMON**, **FAULT**, **SUSPICIOUS** + +for more information visit [FriendlyException.java](https://github.com/sedmelluq/lavaplayer/blob/master/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/FriendlyException.java#L30-L46) + +--- + +### `WEBSOCKET_OPEN` + +```json +{ + "target: "420.69.69.9", + "ssrc": 42069 +} +``` + +Refer to the Dscord docs for what the *ssrc* is + +--- + +### `WEBSOCKET_CLOSED` + +```json +{ + "code": 4014, + "reason": "", + "by_remote": true +} +``` + +For more information about close codes, visit [this page](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes) + +## Player Update + +- `frames` + - `sent` the number of successfully sent frames + - `lost` the number of frames that failed to send + - `usable` whether this data is usable +- `current_track` the currently playing track. +- `filters` the current filters, see [Filters](#filters) + +```json +{ + "op": 5, + "d": { + "guild_id": "751571246189379610", + "frames": { + "sent": 3011, + "lost": 0, + "usable": true + }, + "current_track": { + "track": "...", + "paused": false, + "position": 42069 + }, + "filters": {} + } +} +``` + +--- + +## Play Track + +- `start_time` specifies the number of milliseconds to offset the track by. +- `end_time` specifies what position to stop the track at *(in milliseconds)* + +```json +{ + "op": 6, + "d": { + "guild_id": "751571246189379610", + "track": "...", + "start_time": 30000, + "end_time": 130000, + "no_replace": false + } +} +``` + +## Stop Track + +```json +{ + "op": 7, + "d": { + "guild_id": "751571246189379610" + } +} +``` + +## Pause + +- `state` whether or not to pause the player. + +```json +{ + "op": 8, + "d": { + "guild_id": "751571246189379610", + "state": true + } +} +``` + +## Filters + +- `volume` the volume to set. `0.0` through `5.0` is accepted, where `1.0` is 100% + + +- `tremolo` creates a shuddering effect, where the volume quickly oscillates + - `frequency` Effect frequency • `0 < x` + - `depth` Effect depth • `0 < x ≤ 1` + + +- `equalizer` There are 15 bands (0-14) that can be configured. Each band has a gain and band field, band being the band + number and gain being a number between `-0.25` and `1.0` + `-0.25` means the band is completed muted and `0.25` meaning it's doubled + + +- `distortion` Distortion effect, allows some unique audio effects to be generated. + + +- `timescale` [Time stretch and pitch scale](https://en.wikipedia.org/wiki/Audio_time_stretching_and_pitch_scaling) + filter implementation + - `pitch` Sets the audio pitch + - `pitch_octaves` Sets the audio pitch in octaves, this cannot be used in conjunction with the other two options + - `pitch_semi_tones` Sets the audio pitch in semi tones, this cannot be used in conjunction with the other two pitch + options + - `rate` Sets the audio rate, cannot be used in conjunction with `rate_change` + - `rate_change` Sets the audio rate, in percentage, relative to the default + - `speed` Sets the playback speed, cannot be used in conjunction with `speed_change` + - `speed_change` Sets the playback speed, in percentage, relative to the default + + +- `karaoke` Uses equalization to eliminate part of a band, usually targeting vocals. None of these i have explanations + for... ask [natan](https://github.com/natanbc/lavadsp) ig + - `filter_band`, `filter_width`, `level`, `mono_level` + + +- `channel_mix` This filter mixes both channels (left and right), with a configurable factor on how much each channel + affects the other. With the defaults, both channels are kept independent of each other. Setting all factors to `0.5` + means both channels get the same audio + - `right_to_left` The current right-to-left factor. The default is `0.0` + - `right_to_right` The current right-to-right factor. The default is `1.0` + - `left_to_right` The current left-to-right factor. The default is `0.0` + - `left_to_left` The current left-to-left factor. The default is `1.0` + + +- `vibrato` Similar to tremolo. While tremolo oscillates the volume, vibrato oscillates the pitch + - `frequency` Effect frequency • `0 < x ≤ 14` + - `depth` Effect depth • `0 < x ≤ 1` + + +- `rotation` This filter simulates an audio source rotating around the listener + - `rotation_hz` The frequency the audio should rotate around the listener, in Hertz + + +- `low_pass` Higher frequencies get suppressed, while lower frequencies pass through this filter, thus the name low pass + - `smoothing` Smoothing to use. 20 is the default + - +```json +{ + "op": 9, + "d": { + "guild_id": "751571246189379610", + "filters": { + "distortion": {}, + "equalizer": { "bands": [] }, + "karaoke": {}, + "low_pass": {}, + "rotation": {}, + "timescale": {}, + "tremolo": {}, + "vibrato": {}, + "volume": { "volume": 1.0 }, + } + } +} +``` + +## Seek + +- `position` the position to seek to, in milliseconds. + +```json +{ + "op": 10, + "d": { + "guild_id": "751571246189379610", + "position": 30000 + } +} +``` + +## Destroy + +```json +{ + "op": 11, + "d": { + "guild_id": "751571246189379610" + } +} +``` + diff --git a/api/ws/protocol.md b/api/ws/protocol.md new file mode 100644 index 0000000..2737c8a --- /dev/null +++ b/api/ws/protocol.md @@ -0,0 +1,46 @@ +# Web Socket •Â Protocol + +Document describing the web-socket protocol. + +## Opening a Connection + +Opening a connection to the web-socket server is pretty straightforward. The following headers\* must be supplied. + +``` +Authorization: Password matching the obsidian.yml file** +User-Id: The user id of the bot you're playing music with +Client-Name: Name of your bot or project** +Resume-Key: The resume key (like lavalink), however this is only needed if the session needs to be resumed. +``` + +## Close Codes + +| Close Code | Reason | +| ---------- | ---------------------------------------------------- | +| 4001 | Invalid or missing authorization | +| 4002 | Missing `Client-Name` header or query-parameter | +| 4003 | Missing `User-Id` header or query-parameter | +| 4005 | A session for the supplied user already exists. | +| 4006 | An error occurred while handling a received payload. | + +## Payload Structure + +- **op**: numeric op code +- **d**: payload data + +```json +{ + "op": 69, + "d": {} +} +``` + +See [**/payloads.md**](/payloads.md) for all available payloads + +--- + + +\* query parameters will be used if a header isn't found +\*\* varies depending on the configuration of the node you're connecting to + + From 23bbfdf2777a4102726184cf021b32880eb5a5c6 Mon Sep 17 00:00:00 2001 From: 2D Date: Tue, 25 May 2021 15:09:17 -0700 Subject: [PATCH 04/46] :truck: o.s.i.routes -> o.s.i.rest --- .../kotlin/obsidian/server/Application.kt | 19 +- .../main/kotlin/obsidian/server/io/Magma.kt | 18 +- .../kotlin/obsidian/server/io/MagmaClient.kt | 14 +- .../kotlin/obsidian/server/io/rest/planner.kt | 2 +- .../kotlin/obsidian/server/io/rest/players.kt | 166 +++++++++++------- .../kotlin/obsidian/server/io/rest/tracks.kt | 2 +- .../obsidian/server/util/ThreadFactory.kt | 4 +- Server/src/main/resources/version.txt | 2 +- 8 files changed, 141 insertions(+), 86 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/Application.kt b/Server/src/main/kotlin/obsidian/server/Application.kt index 77c2a79..f68d9aa 100644 --- a/Server/src/main/kotlin/obsidian/server/Application.kt +++ b/Server/src/main/kotlin/obsidian/server/Application.kt @@ -16,6 +16,7 @@ package obsidian.server +import com.github.natanbc.lavadsp.natives.TimescaleNativeLibLoader import com.github.natanbc.nativeloader.SystemNativeLibraryProperties import com.github.natanbc.nativeloader.system.SystemType import com.uchuhimo.konf.Config @@ -23,6 +24,7 @@ import com.uchuhimo.konf.source.yaml import io.ktor.application.* import io.ktor.auth.* import io.ktor.features.* +import io.ktor.http.HttpHeaders.Server import io.ktor.http.HttpStatusCode.Companion.InternalServerError import io.ktor.locations.* import io.ktor.response.* @@ -31,12 +33,15 @@ import io.ktor.serialization.* import io.ktor.server.cio.* import io.ktor.server.engine.* import io.ktor.websocket.* +import kotlinx.coroutines.runBlocking import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import obsidian.server.io.Magma import obsidian.server.io.Magma.magma import obsidian.server.player.ObsidianAPM import obsidian.server.util.AuthorizationPipeline.obsidianProvider +import obsidian.server.util.NativeUtil import obsidian.server.util.Obsidian import obsidian.server.util.VersionInfo import org.slf4j.Logger @@ -73,7 +78,7 @@ object Application { } @JvmStatic - fun main(args: Array) { + fun main(args: Array) = runBlocking { /* native library loading lololol */ try { @@ -91,6 +96,8 @@ object Application { try { log.info("Loading Native Libraries") + TimescaleNativeLibLoader.loadTimescaleLibrary() + NativeUtil.timescaleAvailable = true // NativeUtil.load() } catch (ex: Exception) { log.error("Fatal exception while loading native libraries.", ex) @@ -130,6 +137,7 @@ object Application { install(DefaultHeaders) { header("Obsidian-Version", VersionInfo.VERSION) header("Obsidian-Version-Commit", VersionInfo.GIT_REVISION) + header(Server, "obsidian-magma/v${VersionInfo.VERSION}-${VersionInfo.GIT_REVISION}") } /* use content negotiation for REST endpoints */ @@ -147,15 +155,18 @@ object Application { shutdown() } - fun shutdown() { - + suspend fun shutdown() { + Magma.clients.forEach { (_, client) -> + client.shutdown(false) + } } } @Serializable data class ExceptionResponse( val error: Error, - @SerialName("stack_trace") val stackTrace: String + @SerialName("stack_trace") val stackTrace: String, + val success: Boolean = false ) { @Serializable data class Error( diff --git a/Server/src/main/kotlin/obsidian/server/io/Magma.kt b/Server/src/main/kotlin/obsidian/server/io/Magma.kt index 0526ced..b96b9c6 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Magma.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Magma.kt @@ -23,11 +23,10 @@ import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* import io.ktor.websocket.* -import kotlinx.coroutines.launch import obsidian.server.Application.config -import obsidian.server.io.routes.planner -import obsidian.server.io.routes.players -import obsidian.server.io.routes.tracks +import obsidian.server.io.rest.Players.players +import obsidian.server.io.rest.planner +import obsidian.server.io.rest.tracks import obsidian.server.io.ws.CloseReasons import obsidian.server.io.ws.StatsTask import obsidian.server.io.ws.WebSocketHandler @@ -37,6 +36,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService import kotlin.text.Typography.mdash object Magma { @@ -49,10 +49,10 @@ object Magma { /** * Executor used for cleaning up un-resumed sessions. */ - val cleanupExecutor = + val cleanupExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(threadFactory("Obsidian Magma-Cleanup")) - val log: Logger = LoggerFactory.getLogger(Magma::class.java) + private val log: Logger = LoggerFactory.getLogger(Magma::class.java) /** * Adds REST endpoint routes and websocket route @@ -147,7 +147,7 @@ object Magma { * @param request * [ApplicationRequest] to extract the user id from. */ - fun extractUserId(request: ApplicationRequest): Long? { + private fun extractUserId(request: ApplicationRequest): Long? { return request.headers["user-id"]?.toLongOrNull() ?: request.queryParameters["user-id"]?.toLongOrNull() } @@ -161,7 +161,7 @@ object Magma { * @param request * [ApplicationRequest] to extract the client name from. */ - fun extractClientName(request: ApplicationRequest): String? { + private fun extractClientName(request: ApplicationRequest): String? { return request.headers["Client-Name"] ?: request.queryParameters["client-name"] } @@ -172,7 +172,7 @@ object Magma { /** * Handles a [WebSocketServerSession] for the supplied [client] */ - suspend fun handleWebsocket(client: MagmaClient, wss: WebSocketServerSession) { + private suspend fun handleWebsocket(client: MagmaClient, wss: WebSocketServerSession) { val wsh = WebSocketHandler(client, wss).also { client.websocket = it } diff --git a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt index 9bc58e3..c0fc178 100644 --- a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt +++ b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt @@ -38,6 +38,11 @@ class MagmaClient(val userId: Long) { */ var name: String? = null + /** + * The websocket handler for this client, or null if one hasn't been initialized. + */ + var websocket: WebSocketHandler? = null + /** * The display name for this client. */ @@ -51,15 +56,12 @@ class MagmaClient(val userId: Long) { KoeUtil.koe.newClient(userId) } - /** - * The websocket handler for this client, or null if one hasn't been initialized. - */ - var websocket: WebSocketHandler? = null - /** * Current players */ - var players = ConcurrentHashMap() + val players: ConcurrentHashMap by lazy { + ConcurrentHashMap() + } /** * Convenience method for ensuring that a player with the supplied guild id exists. diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt b/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt index 089fe97..37c3d53 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package obsidian.server.io.routes +package obsidian.server.io.rest import io.ktor.auth.* import io.ktor.http.* diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt index bf11f9c..66b366d 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package obsidian.server.io.routes +package obsidian.server.io.rest import io.ktor.application.* import io.ktor.auth.* @@ -34,91 +34,127 @@ import obsidian.server.io.Handlers import obsidian.server.io.Magma import obsidian.server.io.Magma.clientName import obsidian.server.io.Magma.userId +import obsidian.server.io.MagmaClient import obsidian.server.io.ws.CurrentTrack import obsidian.server.io.ws.Frames import obsidian.server.player.PlayerUpdates.Companion.currentTrackFor import obsidian.server.player.filter.Filters import obsidian.server.util.Obsidian -val UserIdAttributeKey = AttributeKey("User-Id") -val ClientNameAttributeKey = AttributeKey("Client-Name") - -fun Routing.players() = this.authenticate { - this.route("/players/{guild}") { - intercept(ApplicationCallPipeline.Call) { - /* extract user id from the http request */ - val userId = call.request.userId() - ?: return@intercept respondAndFinish(BadRequest, Response("Missing 'User-Id' header or query parameter.")) - - context.attributes.put(UserIdAttributeKey, userId) - - /* extract client name from the request */ - val clientName = call.request.clientName() - if (clientName != null) { - context.attributes.put(ClientNameAttributeKey, clientName) - } else if (config[Obsidian.requireClientName]) { - return@intercept respondAndFinish(BadRequest, Response("Missing 'Client-Name' header or query parameter.")) +object Players { + private val ClientAttr = AttributeKey("MagmaClient") + private val GuildAttr = AttributeKey("Guild-Id") + + fun Routing.players() = this.authenticate { + this.route("/players/{guild}") { + /** + * Extracts useful information from each application call. + */ + intercept(ApplicationCallPipeline.Call) { + /* get the guild id */ + val guildId = call.parameters["guild"]?.toLongOrNull() + ?: return@intercept respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) + + context.attributes.put(GuildAttr, guildId) + + /* extract user id from the http request */ + val userId = call.request.userId() + ?: return@intercept respondAndFinish(BadRequest, Response("Missing 'User-Id' header or query parameter.")) + + /* extract client name from the request */ + val clientName = call.request.clientName() + if (clientName == null && config[Obsidian.requireClientName]) { + return@intercept respondAndFinish(BadRequest, Response("Missing 'Client-Name' header or query parameter.")) + } + + context.attributes.put(ClientAttr, Magma.getClient(userId, clientName)) } - } - - get { - /* get the guild id */ - val guildId = call.parameters["guild"]?.toLongOrNull() - ?: return@get respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) - - /* get a client for this. */ - val client = Magma.getClient(context.attributes[UserIdAttributeKey], context.attributes[ClientNameAttributeKey]) - /* get the requested player */ - val player = client.players[guildId] - ?: return@get respondAndFinish(NotFound, Response("Unknown player for guild '$guildId'")) + /** + * + */ + get { + val guildId = context.attributes[GuildAttr] - /* respond */ - val response = GetPlayer(currentTrackFor(player), player.filters, player.frameLossTracker.payload) - call.respond(response) - } + /* get the requested player */ + val player = context.attributes[ClientAttr].players[guildId] + ?: return@get respondAndFinish(NotFound, Response("Unknown player for guild '$guildId'")) - put("/submit-voice-server") { - /* get the guild id */ - val guildId = call.parameters["guild"]?.toLongOrNull() - ?: return@put respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) + /* respond */ + val response = GetPlayerResponse(currentTrackFor(player), player.filters, player.frameLossTracker.payload) + call.respond(response) + } - /* get a client for this. */ - val client = - Magma.getClient(context.attributes[UserIdAttributeKey], context.attributes.getOrNull(ClientNameAttributeKey)) + /** + * + */ + put("/submit-voice-server") { + val vsi = call.receive().vsi + Handlers.submitVoiceServer(context.attributes[ClientAttr], context.attributes[GuildAttr], vsi) + call.respond(Response("successfully queued connection", success = true)) + } - /* connect to the voice server described in the request body */ - val (session, token, endpoint) = call.receive() - Handlers.submitVoiceServer(client, guildId, VoiceServerInfo(session, endpoint, token)) + /** + * + */ + put("/filters") { + val filters = call.receive() + Handlers.configure(context.attributes[ClientAttr], context.attributes[GuildAttr], filters) + call.respond(Response("applied filters", success = true)) + } - /* respond */ - call.respond(Response("successfully queued connection", success = true)) - } + /** + * + */ + put("/seek") { + val (position) = call.receive() + Handlers.seek(context.attributes[ClientAttr], context.attributes[GuildAttr], position) + call.respond(Response("seeked to $position", success = true)) + } - post("/play") { - /* get the guild id */ - val guildId = call.parameters["guild"]?.toLongOrNull() - ?: return@post respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) + /** + * + */ + post("/play") { + val client = context.attributes[ClientAttr] - /* get a client for this. */ - val client = - Magma.getClient(context.attributes[UserIdAttributeKey], context.attributes.getOrNull(ClientNameAttributeKey)) + /* connect to the voice server described in the request body */ + val (track, start, end, noReplace) = call.receive() + Handlers.playTrack(client, context.attributes[GuildAttr], track, start, end, noReplace) - /* connect to the voice server described in the request body */ - val (track, start, end, noReplace) = call.receive() - Handlers.playTrack(client, guildId, track, start, end, noReplace) + /* respond */ + call.respond(Response("playback has started", success = true)) + } - /* respond */ - call.respond(Response("playback has started", success = true)) + /** + * + */ + post("/stop") { + Handlers.stopTrack(context.attributes[ClientAttr], context.attributes[GuildAttr]) + call.respond(Response("stopped the current track, if any.", success = true)) + } } } + } /** * Body for `PUT /player/{guild}/submit-voice-server` */ @Serializable -data class SubmitVoiceServer(@SerialName("session_id") val sessionId: String, val token: String, val endpoint: String) +data class SubmitVoiceServer(@SerialName("session_id") val sessionId: String, val token: String, val endpoint: String) { + /** + * The voice server info instance + */ + val vsi: VoiceServerInfo + get() = VoiceServerInfo(sessionId, endpoint, token) +} + +/** + * Body for `PUT /player/{guild}/seek` + */ +@Serializable +data class Seek(val position: Long) /** * @@ -131,11 +167,17 @@ data class PlayTrack( @SerialName("no_replace") val noReplace: Boolean = false ) +@Serializable +data class StopTrackResponse( + val track: Track?, + val success: Boolean +) + /** * Response for `GET /player/{guild}` */ @Serializable -data class GetPlayer( +data class GetPlayerResponse( @SerialName("current_track") val currentTrack: CurrentTrack, val filters: Filters?, val frames: Frames diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 53db421..053d108 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package obsidian.server.io.routes +package obsidian.server.io.rest import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack diff --git a/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt b/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt index 1ead148..d2c10e1 100644 --- a/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt +++ b/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt @@ -23,9 +23,9 @@ import java.util.concurrent.atomic.AtomicInteger fun threadFactory(name: String, daemon: Boolean = false, priority: Int? = null): ThreadFactory { val counter = AtomicInteger() return ThreadFactory { runnable -> - Thread(runnable).apply { + Thread(System.getSecurityManager()?.threadGroup ?: Thread.currentThread().threadGroup, runnable).apply { this.name = name.format(Locale.ROOT, counter.getAndIncrement()) - this.isDaemon = daemon + this.isDaemon = if (!isDaemon) daemon else true priority?.let { this.priority = it } } } diff --git a/Server/src/main/resources/version.txt b/Server/src/main/resources/version.txt index 0f969b9..204be99 100644 --- a/Server/src/main/resources/version.txt +++ b/Server/src/main/resources/version.txt @@ -1,2 +1,2 @@ 2.0.0 -0c6b31c \ No newline at end of file +e0d81ec \ No newline at end of file From 2991a27313e9cc1d0375b681b3e21524cf54ebca Mon Sep 17 00:00:00 2001 From: 2D Date: Tue, 25 May 2021 15:10:18 -0700 Subject: [PATCH 05/46] :fire: remove buildSrc and use my jfrog instance for natan's shit --- Server/build.gradle.kts | 66 ++++++++++++------------ build.gradle.kts | 18 +++---- buildSrc/build.gradle.kts | 23 --------- buildSrc/src/main/kotlin/Compiler.kt | 22 -------- buildSrc/src/main/kotlin/Dependencies.kt | 61 ---------------------- buildSrc/src/main/kotlin/Project.kt | 20 ------- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 2 - settings.gradle.kts | 29 +++++++++++ 9 files changed, 70 insertions(+), 173 deletions(-) delete mode 100644 buildSrc/build.gradle.kts delete mode 100644 buildSrc/src/main/kotlin/Compiler.kt delete mode 100644 buildSrc/src/main/kotlin/Dependencies.kt delete mode 100644 buildSrc/src/main/kotlin/Project.kt delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 30de894..6f06820 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -1,10 +1,10 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.ByteArrayOutputStream plugins { application - id("com.github.johnrengelman.shadow") version Versions.shadow + id("com.github.johnrengelman.shadow") version "7.0.0" } apply(plugin = "kotlin") @@ -14,55 +14,53 @@ description = "A robust and performant audio sending node meant for Discord Bots version = "2.0.0" application { - mainClass.set(Project.mainClassName) -} - -repositories { - jcenter() + mainClass.set("obsidian.server.Application") } dependencies { /* kotlin */ - implementation(Dependencies.kotlin) - implementation(Dependencies.kotlinxCoroutines) - implementation(Dependencies.kotlinxCoroutinesJdk8) - implementation(Dependencies.kotlinxSerialization) + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.10") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") /* ktor, server related */ - implementation(Dependencies.ktorServerCore) - implementation(Dependencies.ktorServerCio) - implementation(Dependencies.ktorLocations) - implementation(Dependencies.ktorWebSockets) - implementation(Dependencies.ktorSerialization) + val ktorVersion = "1.5.4" + implementation("io.ktor:ktor-server-core:$ktorVersion") // ktor server core + implementation("io.ktor:ktor-server-cio:$ktorVersion") // ktor cio engine + implementation("io.ktor:ktor-locations:$ktorVersion") // ktor locations + implementation("io.ktor:ktor-websockets:$ktorVersion") // ktor websockets + implementation("io.ktor:ktor-serialization:$ktorVersion") // ktor serialization /* media library */ - implementation(Dependencies.koeCore) { + implementation("moe.kyokobot.koe:core:master-SNAPSHOT") { exclude(group = "org.slf4j", module = "slf4j-api") } /* */ - implementation(Dependencies.lavaplayer)/*{ + implementation("com.sedmelluq:lavaplayer:1.3.77") { exclude(group = "com.sedmelluq", module = "lavaplayer-natives") - } */ + } - implementation(Dependencies.lavaplayerIpRotator) { + implementation("com.sedmelluq:lavaplayer-ext-youtube-rotator:0.2.3") { exclude(group = "com.sedmelluq", module = "lavaplayer") } /* audio filters */ - implementation(Dependencies.lavadsp) + implementation("com.github.natanbc:lavadsp:0.7.7") /* native libraries */ - implementation(Dependencies.nativeLoader) -// implementation(Dependencies.lpCross) + implementation("com.github.natanbc:native-loader:0.7.0") // native loader + implementation("com.github.natanbc:lp-cross:0.1.3") // lp-cross natives /* logging */ - implementation(Dependencies.logback) - implementation(Dependencies.mordant) + implementation("ch.qos.logback:logback-classic:1.2.3") // slf4j logging backend + implementation("com.github.ajalt.mordant:mordant:2.0.0-beta1") // terminal coloring & styling /* configuration */ - implementation(Dependencies.konfCore) - implementation(Dependencies.konfYaml) + val konfVersion = "1.1.2" + implementation("com.github.uchuhimo.konf:konf-core:$konfVersion") // konf core shit + implementation("com.github.uchuhimo.konf:konf-yaml:$konfVersion") // yaml source } tasks.withType { @@ -71,17 +69,17 @@ tasks.withType { } tasks.withType { - sourceCompatibility = Project.jvmTarget - targetCompatibility = Project.jvmTarget + sourceCompatibility = "16" + targetCompatibility = "16" kotlinOptions { - jvmTarget = Project.jvmTarget + jvmTarget = "16" incremental = true freeCompilerArgs = listOf( - CompilerArgs.experimentalCoroutinesApi, - CompilerArgs.experimentalLocationsApi, - CompilerArgs.experimentalStdlibApi, - CompilerArgs.obsoleteCoroutinesApi + "-Xopt-in=kotlin.ExperimentalStdlibApi", + "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-Xopt-in=io.ktor.locations.KtorExperimentalLocationsAPI", + "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi" ) } } diff --git a/build.gradle.kts b/build.gradle.kts index c310c23..0412e0f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,10 @@ -allprojects { - repositories { - maven("https://jitpack.io") - maven("https://oss.sonatype.org/content/repositories/snapshots/") - maven("https://m2.dv8tion.net/releases") - mavenCentral() - } +plugins { + idea + java +} - group = "gg.mixtape.obsidian" +allprojects { + group = "obsidian" apply(plugin = "idea") } @@ -18,8 +16,8 @@ subprojects { } dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}") - classpath("org.jetbrains.kotlin:kotlin-serialization:${Versions.kotlin}") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10") + classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.10") } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index 2acc943..0000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -plugins { - `kotlin-dsl` -} - -repositories { - mavenCentral() -} diff --git a/buildSrc/src/main/kotlin/Compiler.kt b/buildSrc/src/main/kotlin/Compiler.kt deleted file mode 100644 index 31f9144..0000000 --- a/buildSrc/src/main/kotlin/Compiler.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -object CompilerArgs { - const val experimentalStdlibApi = "-Xopt-in=kotlin.ExperimentalStdlibApi" - const val obsoleteCoroutinesApi = "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi" - const val experimentalCoroutinesApi = "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" - const val experimentalLocationsApi = "-Xopt-in=io.ktor.locations.KtorExperimentalLocationsAPI" -} diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt deleted file mode 100644 index b47fcd8..0000000 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -object Versions { - const val shadow = "7.0.0" - const val kotlin = "1.4.32" - const val kotlinxCoroutines = "1.4.3" - const val lavaplayer = "1.3.76" - const val lavadsp = "0.7.7" - const val netty = "4.1.63.Final" - const val lavaplayerIpRotator = "0.2.3" - const val nativeLoader = "0.7.0" - const val koe = "master-SNAPSHOT" - const val lpCross = "0.1.1" - const val logback = "1.2.3" - const val mordant = "2.0.0-beta1" - const val serializationJson = "1.1.0" - const val konf = "1.1.2" - const val ktor = "1.5.4" -} - -object Dependencies { - const val kotlin = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" - const val kotlinxCoroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinxCoroutines}" - const val kotlinxCoroutinesJdk8 = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${Versions.kotlinxCoroutines}" - const val kotlinxSerialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.serializationJson}" - - const val lavaplayer = "com.sedmelluq:lavaplayer:${Versions.lavaplayer}" - const val lavaplayerIpRotator = "com.sedmelluq:lavaplayer-ext-youtube-rotator:${Versions.lavaplayerIpRotator}" - - const val lavadsp = "com.github.natanbc:lavadsp:${Versions.lavadsp}" - const val lpCross = "com.github.natanbc:lp-cross:${Versions.lpCross}" - const val nativeLoader = "com.github.natanbc:native-loader:${Versions.nativeLoader}" - - const val koeCore = "moe.kyokobot.koe:core:${Versions.koe}" - - const val logback = "ch.qos.logback:logback-classic:${Versions.logback}" - const val mordant = "com.github.ajalt.mordant:mordant:${Versions.mordant}" - - const val konfCore = "com.github.uchuhimo.konf:konf-core:${Versions.konf}" - const val konfYaml = "com.github.uchuhimo.konf:konf-yaml:${Versions.konf}" - - const val ktorServerCore = "io.ktor:ktor-server-core:${Versions.ktor}" - const val ktorServerCio = "io.ktor:ktor-server-cio:${Versions.ktor}" - const val ktorLocations = "io.ktor:ktor-locations:${Versions.ktor}" - const val ktorWebSockets = "io.ktor:ktor-websockets:${Versions.ktor}" - const val ktorSerialization = "io.ktor:ktor-serialization:${Versions.ktor}" -} diff --git a/buildSrc/src/main/kotlin/Project.kt b/buildSrc/src/main/kotlin/Project.kt deleted file mode 100644 index 9bf1e55..0000000 --- a/buildSrc/src/main/kotlin/Project.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2021 MixtapeBot and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -object Project { - const val jvmTarget = "11" - const val mainClassName = "obsidian.server.Application" -} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f371643..29e4134 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 09ca0c5..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -include 'Server' - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..21174d6 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,29 @@ +rootProject.name = "Obsidian-Root" + +include(":Server") + +dependencyResolutionManagement { + repositories { + maven { + url = uri("https://dimensional.jfrog.io/artifactory/maven") + name = "Jfrog Dimensional" + } + + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + name = "Sonatype" + } + + maven{ + url = uri("https://m2.dv8tion.net/releases") + name = "Dv8tion" + } + + maven{ + url = uri("https://jitpack.io") + name = "Jitpack" + } + + mavenCentral() + } +} From 0bd5445d391bd3aa0646d7c136c3a4a48773f55d Mon Sep 17 00:00:00 2001 From: 2D Date: Tue, 25 May 2021 20:09:32 -0700 Subject: [PATCH 06/46] :sparkles: ktor eap, rest logs, working natives --- Server/build.gradle.kts | 39 +++------ .../koe/codec/udpqueue/QueueManagerPool.kt | 2 +- .../kotlin/obsidian/server/Application.kt | 85 +++++++++---------- .../kotlin/obsidian/server/io/Handlers.kt | 5 +- .../main/kotlin/obsidian/server/io/Magma.kt | 4 +- .../kotlin/obsidian/server/io/rest/players.kt | 24 ++++-- .../kotlin/obsidian/server/io/rest/tracks.kt | 1 - .../kotlin/obsidian/server/io/ws/StatsTask.kt | 8 +- .../obsidian/server/io/ws/WebSocketHandler.kt | 42 ++++++--- .../obsidian/server/player/ObsidianAPM.kt | 2 +- .../obsidian/server/player/filter/Filters.kt | 22 ++--- .../kotlin/obsidian/server/util/Interval.kt | 3 - .../kotlin/obsidian/server/util/KoeUtil.kt | 17 ++-- .../server/util/search/AudioLoader.kt | 7 +- Server/src/main/resources/logback.xml | 2 +- Server/src/main/resources/version.txt | 2 +- build.gradle.kts | 1 - settings.gradle.kts | 5 ++ 18 files changed, 141 insertions(+), 130 deletions(-) diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 6f06820..32bd071 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -5,10 +5,11 @@ import java.io.ByteArrayOutputStream plugins { application id("com.github.johnrengelman.shadow") version "7.0.0" + kotlin("jvm") version "1.5.10" + kotlin("plugin.serialization") version "1.5.10" } apply(plugin = "kotlin") -apply(plugin = "kotlinx-serialization") description = "A robust and performant audio sending node meant for Discord Bots." version = "2.0.0" @@ -18,46 +19,37 @@ application { } dependencies { - /* kotlin */ - implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.10") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") - - /* ktor, server related */ - val ktorVersion = "1.5.4" + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.10") // standard library + implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.10") // reflection + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") // core coroutine library + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") // json serialization + + val ktorVersion = "main-128" implementation("io.ktor:ktor-server-core:$ktorVersion") // ktor server core implementation("io.ktor:ktor-server-cio:$ktorVersion") // ktor cio engine implementation("io.ktor:ktor-locations:$ktorVersion") // ktor locations implementation("io.ktor:ktor-websockets:$ktorVersion") // ktor websockets implementation("io.ktor:ktor-serialization:$ktorVersion") // ktor serialization - /* media library */ - implementation("moe.kyokobot.koe:core:master-SNAPSHOT") { + implementation("moe.kyokobot.koe:core:master-SNAPSHOT") { // discord send system exclude(group = "org.slf4j", module = "slf4j-api") } - /* */ - implementation("com.sedmelluq:lavaplayer:1.3.77") { + implementation("com.sedmelluq:lavaplayer:1.3.77") { // yes exclude(group = "com.sedmelluq", module = "lavaplayer-natives") } - implementation("com.sedmelluq:lavaplayer-ext-youtube-rotator:0.2.3") { + implementation("com.sedmelluq:lavaplayer-ext-youtube-rotator:0.2.3") { // ip rotation exclude(group = "com.sedmelluq", module = "lavaplayer") } - /* audio filters */ - implementation("com.github.natanbc:lavadsp:0.7.7") - - /* native libraries */ - implementation("com.github.natanbc:native-loader:0.7.0") // native loader - implementation("com.github.natanbc:lp-cross:0.1.3") // lp-cross natives + implementation("com.github.natanbc:lavadsp:0.7.7") // audio filters + implementation("com.github.natanbc:native-loader:0.7.2") // native loader + implementation("com.github.natanbc:lp-cross:0.1.3-1") // lp-cross natives - /* logging */ implementation("ch.qos.logback:logback-classic:1.2.3") // slf4j logging backend implementation("com.github.ajalt.mordant:mordant:2.0.0-beta1") // terminal coloring & styling - /* configuration */ val konfVersion = "1.1.2" implementation("com.github.uchuhimo.konf:konf-core:$konfVersion") // konf core shit implementation("com.github.uchuhimo.konf:konf-yaml:$konfVersion") // yaml source @@ -69,9 +61,6 @@ tasks.withType { } tasks.withType { - sourceCompatibility = "16" - targetCompatibility = "16" - kotlinOptions { jvmTarget = "16" incremental = true diff --git a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt index 9d8274e..f0d454d 100644 --- a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt +++ b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt @@ -43,7 +43,7 @@ class QueueManagerPool(val size: Int, val bufferDuration: Int) { MAXIMUM_PACKET_SIZE ) - threadFactory.newThread(queueManager::process) + threadFactory.newThread(queueManager::process).start() queueManager } diff --git a/Server/src/main/kotlin/obsidian/server/Application.kt b/Server/src/main/kotlin/obsidian/server/Application.kt index f68d9aa..e88c9ff 100644 --- a/Server/src/main/kotlin/obsidian/server/Application.kt +++ b/Server/src/main/kotlin/obsidian/server/Application.kt @@ -16,7 +16,7 @@ package obsidian.server -import com.github.natanbc.lavadsp.natives.TimescaleNativeLibLoader +import com.github.natanbc.nativeloader.NativeLibLoader import com.github.natanbc.nativeloader.SystemNativeLibraryProperties import com.github.natanbc.nativeloader.system.SystemType import com.uchuhimo.konf.Config @@ -85,7 +85,7 @@ object Application { val type = SystemType.detect(SystemNativeLibraryProperties(null, "nativeloader.")) log.info("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") -// log.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}") + log.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}") } catch (e: Exception) { val message = "Unable to load system info" + if (e is UnsatisfiedLinkError || e is RuntimeException && e.cause is UnsatisfiedLinkError) @@ -96,60 +96,59 @@ object Application { try { log.info("Loading Native Libraries") - TimescaleNativeLibLoader.loadTimescaleLibrary() NativeUtil.timescaleAvailable = true -// NativeUtil.load() + NativeUtil.load() } catch (ex: Exception) { log.error("Fatal exception while loading native libraries.", ex) exitProcess(1) } - val server = embeddedServer(CIO, host = config[Obsidian.Server.host], port = config[Obsidian.Server.port]) { - install(WebSockets) + val server = + embeddedServer(CIO, host = config[Obsidian.Server.host], port = config[Obsidian.Server.port]) { + install(WebSockets) + install(Locations) - install(Locations) - - /* use the custom authentication provider */ - install(Authentication) { - obsidianProvider() - } + /* use the custom authentication provider */ + install(Authentication) { + obsidianProvider() + } - /* install status pages. */ - install(StatusPages) { - exception { exc -> - val error = ExceptionResponse.Error( - className = exc::class.simpleName ?: "Throwable", - message = exc.message, - cause = exc.cause?.let { - ExceptionResponse.Error( - it.message, - className = it::class.simpleName ?: "Throwable" - ) - } - ) - - val message = ExceptionResponse(error, exc.stackTraceToString()) - call.respond(InternalServerError, message) + /* install status pages. */ + install(StatusPages) { + exception { exc -> + val error = ExceptionResponse.Error( + className = exc::class.simpleName ?: "Throwable", + message = exc.message, + cause = exc.cause?.let { + ExceptionResponse.Error( + it.message, + className = it::class.simpleName ?: "Throwable" + ) + } + ) + + val message = ExceptionResponse(error, exc.stackTraceToString()) + call.respond(InternalServerError, message) + } } - } - /* append version headers. */ - install(DefaultHeaders) { - header("Obsidian-Version", VersionInfo.VERSION) - header("Obsidian-Version-Commit", VersionInfo.GIT_REVISION) - header(Server, "obsidian-magma/v${VersionInfo.VERSION}-${VersionInfo.GIT_REVISION}") - } + /* append version headers. */ + install(DefaultHeaders) { + header("Obsidian-Version", VersionInfo.VERSION) + header("Obsidian-Version-Commit", VersionInfo.GIT_REVISION) + header(Server, "obsidian-magma/v${VersionInfo.VERSION}-${VersionInfo.GIT_REVISION}") + } - /* use content negotiation for REST endpoints */ - install(ContentNegotiation) { - json(json) - } + /* use content negotiation for REST endpoints */ + install(ContentNegotiation) { + json(json) + } - /* install routing */ - install(Routing) { - magma() + /* install routing */ + install(Routing) { + magma() + } } - } server.start(wait = true) shutdown() diff --git a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt index f9d6030..91c9148 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt @@ -30,9 +30,8 @@ object Handlers { fun submitVoiceServer(client: MagmaClient, guildId: Long, vsi: VoiceServerInfo) { val connection = client.mediaConnectionFor(guildId) - connection.connect(vsi).whenComplete { _, _ -> - client.playerFor(guildId).provideTo(connection) - } + connection.connect(vsi) + client.playerFor(guildId).provideTo(connection) } fun seek(client: MagmaClient, guildId: Long, position: Long) { diff --git a/Server/src/main/kotlin/obsidian/server/io/Magma.kt b/Server/src/main/kotlin/obsidian/server/io/Magma.kt index b96b9c6..5795008 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Magma.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Magma.kt @@ -65,7 +65,8 @@ object Magma { authenticate { get("/stats") { - val stats = StatsTask.build(null) + val client = call.request.userId()?.let { clients[it] } + val stats = StatsTask.build(client) call.respond(stats) } } @@ -137,7 +138,6 @@ object Magma { return MagmaClient(userId).also { it.name = clientName clients[userId] = it - println(clients) } } diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt index 66b366d..f36985a 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt @@ -18,6 +18,7 @@ package obsidian.server.io.rest import io.ktor.application.* import io.ktor.auth.* +import io.ktor.features.* import io.ktor.http.* import io.ktor.http.HttpStatusCode.Companion.BadRequest import io.ktor.http.HttpStatusCode.Companion.NotFound @@ -40,10 +41,14 @@ import obsidian.server.io.ws.Frames import obsidian.server.player.PlayerUpdates.Companion.currentTrackFor import obsidian.server.player.filter.Filters import obsidian.server.util.Obsidian +import org.slf4j.LoggerFactory +import kotlin.text.Typography.mdash +import kotlin.text.Typography.ndash object Players { private val ClientAttr = AttributeKey("MagmaClient") private val GuildAttr = AttributeKey("Guild-Id") + private val log = LoggerFactory.getLogger(Players::class.java) fun Routing.players() = this.authenticate { this.route("/players/{guild}") { @@ -51,6 +56,19 @@ object Players { * Extracts useful information from each application call. */ intercept(ApplicationCallPipeline.Call) { + /* extract client name from the request */ + val clientName = call.request.clientName() + + /* log out the request */ + log.info(with(call.request) { + "${clientName ?: origin.remoteHost} $ndash ${httpMethod.value.padEnd(4, ' ')} $uri" + }) + + /* check if a client name is required, if so check if there was a provided client name */ + if (clientName == null && config[Obsidian.requireClientName]) { + return@intercept respondAndFinish(BadRequest, Response("Missing 'Client-Name' header or query parameter.")) + } + /* get the guild id */ val guildId = call.parameters["guild"]?.toLongOrNull() ?: return@intercept respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) @@ -61,12 +79,6 @@ object Players { val userId = call.request.userId() ?: return@intercept respondAndFinish(BadRequest, Response("Missing 'User-Id' header or query parameter.")) - /* extract client name from the request */ - val clientName = call.request.clientName() - if (clientName == null && config[Obsidian.requireClientName]) { - return@intercept respondAndFinish(BadRequest, Response("Missing 'Client-Name' header or query parameter.")) - } - context.attributes.put(ClientAttr, Magma.getClient(userId, clientName)) } diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 053d108..34c0c28 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -24,7 +24,6 @@ import io.ktor.locations.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* -import kotlinx.coroutines.future.await import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt index fe51d3c..ea1ae29 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt @@ -18,6 +18,7 @@ package obsidian.server.io.ws import kotlinx.coroutines.launch import obsidian.server.io.Magma +import obsidian.server.io.MagmaClient import obsidian.server.util.CpuTimer import java.lang.management.ManagementFactory @@ -36,16 +37,15 @@ object StatsTask { fun getRunnable(wsh: WebSocketHandler): Runnable { return Runnable { wsh.launch { - val stats = build(wsh) + val stats = build(wsh.client) wsh.send(stats) } } } - fun build(wsh: WebSocketHandler?): Stats { - val client = wsh?.client - + fun build(client: MagmaClient?): Stats { /* memory stats. */ + println("${Runtime.getRuntime().let { it.totalMemory() - it.freeMemory() } / 1024 / 1024}mb") val memory = ManagementFactory.getMemoryMXBean().let { bean -> val heapUsed = bean.heapMemoryUsage.let { Stats.Memory.Usage(committed = it.committed, max = it.max, init = it.init, used = it.used) diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt index 9ce2cac..841befa 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt @@ -26,15 +26,20 @@ import kotlinx.coroutines.flow.* import moe.kyokobot.koe.VoiceServerInfo import obsidian.server.Application.json import obsidian.server.io.Handlers -import obsidian.server.io.MagmaClient import obsidian.server.io.Magma.cleanupExecutor -import obsidian.server.util.threadFactory +import obsidian.server.io.MagmaClient +import obsidian.server.util.Interval import org.slf4j.Logger import org.slf4j.LoggerFactory import java.lang.Runnable -import java.util.concurrent.* import java.util.concurrent.CancellationException +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext +import kotlin.text.Typography.ndash +import kotlin.time.Duration +import kotlin.time.ExperimentalTime class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSession) : CoroutineScope { @@ -46,7 +51,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess /** * Stats interval. */ - private var stats = Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Stats-Dispatcher %d")) + private var stats = Interval(Dispatchers.IO) /** * Whether this magma client is active @@ -121,12 +126,12 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess resumeKey = key resumeTimeout = timeout - logger.debug("${client.displayName} - resuming is configured; key= $key, timeout= $timeout") + logger.debug("${client.displayName} $ndash Resuming has been configured; key= $key, timeout= $timeout") } on { bufferTimeout = timeout - logger.debug("${client.displayName} - dispatch buffer timeout: $timeout") + logger.debug("${client.displayName} $ndash Dispatch buffer timeout: $timeout") } } @@ -134,15 +139,24 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess /** * */ + @OptIn(ExperimentalTime::class) suspend fun listen() { active = true /* starting sending stats. */ - val statsRunnable = StatsTask.getRunnable(this) - stats.scheduleAtFixedRate(statsRunnable, 0, 1, TimeUnit.MINUTES) + coroutineScope { + launch(coroutineContext) { + stats.start(Duration.minutes(1).inWholeMilliseconds) { + val stats = StatsTask.build(client) + send(stats) + } + } + } /* listen for incoming frames. */ - session.incoming.asFlow().buffer(Channel.UNLIMITED) + session.incoming + .asFlow() + .buffer(Channel.UNLIMITED) .collect { when (it) { is Frame.Binary, is Frame.Text -> handleIncomingFrame(it) @@ -171,7 +185,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess } resumeTimeoutFuture = cleanupExecutor.schedule(runnable, resumeTimeout!!, TimeUnit.MILLISECONDS) - logger.info("${client.displayName} - session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") + logger.info("${client.displayName} $ndash Session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") return } @@ -182,7 +196,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess * Resumes this session */ suspend fun resume(session: WebSocketServerSession) { - logger.info("${client.displayName} - session has been resumed") + logger.info("${client.displayName} $ndash session has been resumed") this.session = session this.active = true @@ -222,7 +236,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess logger.trace("${client.displayName} <<< $json") session.send(json) } catch (ex: Exception) { - logger.error("${client.displayName} -", ex) + logger.error("${client.displayName} $ndash An exception occurred while sending a json payload", ex) } } @@ -236,7 +250,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess try { block.invoke(it) } catch (ex: Exception) { - logger.error("${client.displayName} -", ex) + logger.error("${client.displayName} $ndash An exception occurred while handling a command", ex) } } } @@ -255,7 +269,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess logger.info("${client.displayName} >>> $data") json.decodeFromString(Operation, data)?.let { events.emit(it) } } catch (ex: Exception) { - logger.error("${client.displayName} -", ex) + logger.error("${client.displayName} $ndash An exception occurred while handling an incoming frame", ex) } } diff --git a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt index 8fe8f2c..c4d2739 100644 --- a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt +++ b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt @@ -94,7 +94,7 @@ class ObsidianAPM : DefaultAudioPlayerManager() { private fun registerSources() { config[Obsidian.Lavaplayer.enabledSources] .forEach { source -> - when (source.toLowerCase()) { + when (source.lowercase()) { "youtube" -> { val youtube = YoutubeAudioSourceManager(config[Obsidian.Lavaplayer.YouTube.allowSearch]).apply { setPlaylistPageCount(config[Obsidian.Lavaplayer.YouTube.playlistPageLimit]) diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt b/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt index 3f5d9f7..2e6d57a 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt @@ -29,18 +29,18 @@ import obsidian.server.player.filter.impl.* @Serializable data class Filters( - val volume: VolumeFilter?, - val equalizer: EqualizerFilter?, - val karaoke: KaraokeFilter?, - val rotation: RotationFilter?, - val tremolo: TremoloFilter?, - val vibrato: VibratoFilter?, - val distortion: DistortionFilter?, - val timescale: TimescaleFilter?, + val volume: VolumeFilter? = null, + val equalizer: EqualizerFilter? = null, + val karaoke: KaraokeFilter? = null, + val rotation: RotationFilter? = null, + val tremolo: TremoloFilter? = null, + val vibrato: VibratoFilter? = null, + val distortion: DistortionFilter? = null, + val timescale: TimescaleFilter? = null, @SerialName("low_pass") - val lowPass: LowPassFilter?, + val lowPass: LowPassFilter? = null, @SerialName("channel_mix") - val channelMix: ChannelMixFilter?, + val channelMix: ChannelMixFilter? = null, ) { /** * All filters @@ -91,7 +91,7 @@ data class Filters( } } - return list.toMutableList() + return list.reversed().toMutableList() } } } diff --git a/Server/src/main/kotlin/obsidian/server/util/Interval.kt b/Server/src/main/kotlin/obsidian/server/util/Interval.kt index f5452aa..eaaccb0 100644 --- a/Server/src/main/kotlin/obsidian/server/util/Interval.kt +++ b/Server/src/main/kotlin/obsidian/server/util/Interval.kt @@ -31,7 +31,6 @@ import kotlin.coroutines.CoroutineContext * * @param dispatcher The dispatchers the events will be fired on. */ -@ObsoleteCoroutinesApi class Interval(private val dispatcher: CoroutineDispatcher = Dispatchers.Default) : CoroutineScope { /** * The coroutine context. @@ -66,10 +65,8 @@ class Interval(private val dispatcher: CoroutineDispatcher = Dispatchers.Default stop() mutex.withLock { ticker = ticker(delay) - launch { started = true - ticker?.consumeEach { try { block() diff --git a/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt index ec1acef..aca4fc8 100644 --- a/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt @@ -43,7 +43,6 @@ object KoeUtil { options.setGatewayVersion(gatewayVersion) options.setHighPacketPriority(config[Obsidian.Koe.highPacketPriority]) - println("koe") Koe.koe(options.create()) } @@ -66,16 +65,16 @@ object KoeUtil { */ private val framePollerFactory: FramePollerFactory by lazy { when { -// NativeUtil.udpQueueAvailable && config[Obsidian.Koe.UdpQueue.enabled] -> { -// log.info("Enabling udp-queue") -// UdpQueueFramePollerFactory(config[Obsidian.Koe.UdpQueue.bufferDuration], config[Obsidian.Koe.UdpQueue.poolSize]) -// } + NativeUtil.udpQueueAvailable && config[Obsidian.Koe.UdpQueue.enabled] -> { + log.info("Enabling udp-queue") + UdpQueueFramePollerFactory(config[Obsidian.Koe.UdpQueue.bufferDuration], config[Obsidian.Koe.UdpQueue.poolSize]) + } else -> { -// if (config[Obsidian.Koe.UdpQueue.enabled]) { -// log.warn("This system and/or architecture appears to not support native audio sending, " -// + "GC pauses may cause your bot to stutter during playback.") -// } + if (config[Obsidian.Koe.UdpQueue.enabled]) { + log.warn("This system and/or architecture appears to not support native audio sending, " + + "GC pauses may cause your bot to stutter during playback.") + } NettyFramePollerFactory() } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt index 4b715fc..0b75852 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt @@ -22,17 +22,16 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import kotlinx.coroutines.CompletableDeferred import org.slf4j.LoggerFactory import java.util.* -import java.util.concurrent.CompletableFuture -import java.util.concurrent.CompletionStage import java.util.concurrent.atomic.AtomicBoolean class AudioLoader(private val audioPlayerManager: AudioPlayerManager) : AudioLoadResultHandler { - private val loadResult: CompletableFuture = CompletableFuture() + private val loadResult: CompletableDeferred = CompletableDeferred() private val used = AtomicBoolean(false) - fun load(identifier: String?): CompletionStage { + fun load(identifier: String?): CompletableDeferred { val isUsed = used.getAndSet(true) check(!isUsed) { "This loader can only be used once per instance" diff --git a/Server/src/main/resources/logback.xml b/Server/src/main/resources/logback.xml index dfa50da..f4dc39c 100644 --- a/Server/src/main/resources/logback.xml +++ b/Server/src/main/resources/logback.xml @@ -7,7 +7,7 @@ - + diff --git a/Server/src/main/resources/version.txt b/Server/src/main/resources/version.txt index 204be99..60c2fcc 100644 --- a/Server/src/main/resources/version.txt +++ b/Server/src/main/resources/version.txt @@ -1,2 +1,2 @@ 2.0.0 -e0d81ec \ No newline at end of file +2991a27 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 0412e0f..e53fc78 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,6 @@ subprojects { dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10") - classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.10") } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 21174d6..f4ca834 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,11 @@ dependencyResolutionManagement { name = "Jfrog Dimensional" } + maven { + url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap/") + name = "Ktor EAP" + } + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") name = "Sonatype" From 16d5fc850cc112e44a3b178bda8ffa0c9d89e1ef Mon Sep 17 00:00:00 2001 From: 2D Date: Wed, 26 May 2021 05:29:45 -0700 Subject: [PATCH 07/46] :sparkles: add configurable logging (pt 1) --- .../kotlin/obsidian/server/Application.kt | 27 ++++++++++++-- .../obsidian/server/config/spec/Logging.kt | 37 +++++++++++++++++++ .../server/{util => config/spec}/Obsidian.kt | 2 +- .../main/kotlin/obsidian/server/io/Magma.kt | 2 +- .../kotlin/obsidian/server/io/rest/players.kt | 3 +- .../obsidian/server/player/ObsidianAPM.kt | 2 +- .../obsidian/server/player/PlayerUpdates.kt | 2 +- .../server/util/AuthorizationPipeline.kt | 1 + .../kotlin/obsidian/server/util/KoeUtil.kt | 1 + 9 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 Server/src/main/kotlin/obsidian/server/config/spec/Logging.kt rename Server/src/main/kotlin/obsidian/server/{util => config/spec}/Obsidian.kt (99%) diff --git a/Server/src/main/kotlin/obsidian/server/Application.kt b/Server/src/main/kotlin/obsidian/server/Application.kt index e88c9ff..74429db 100644 --- a/Server/src/main/kotlin/obsidian/server/Application.kt +++ b/Server/src/main/kotlin/obsidian/server/Application.kt @@ -16,6 +16,9 @@ package obsidian.server +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.LoggerContext import com.github.natanbc.nativeloader.NativeLibLoader import com.github.natanbc.nativeloader.SystemNativeLibraryProperties import com.github.natanbc.nativeloader.system.SystemType @@ -37,14 +40,14 @@ import kotlinx.coroutines.runBlocking import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import obsidian.server.config.spec.Logging import obsidian.server.io.Magma import obsidian.server.io.Magma.magma import obsidian.server.player.ObsidianAPM import obsidian.server.util.AuthorizationPipeline.obsidianProvider import obsidian.server.util.NativeUtil -import obsidian.server.util.Obsidian +import obsidian.server.config.spec.Obsidian import obsidian.server.util.VersionInfo -import org.slf4j.Logger import org.slf4j.LoggerFactory import kotlin.system.exitProcess @@ -54,7 +57,7 @@ object Application { * * @see Obsidian */ - val config = Config { addSpec(Obsidian) } + val config = Config { addSpec(Obsidian); addSpec(Logging) } .from.yaml.file("obsidian.yml", optional = true) .from.env() @@ -66,7 +69,7 @@ object Application { /** * Logger */ - val log: Logger = LoggerFactory.getLogger(Application::class.java) + val log: org.slf4j.Logger = LoggerFactory.getLogger(Application::class.java) /** * Json parser used by ktor and us. @@ -80,6 +83,9 @@ object Application { @JvmStatic fun main(args: Array) = runBlocking { + /* setup logging */ + configureLogging() + /* native library loading lololol */ try { val type = SystemType.detect(SystemNativeLibraryProperties(null, "nativeloader.")) @@ -159,6 +165,19 @@ object Application { client.shutdown(false) } } + + /** + * Configures the root logger and obsidian level logger. + */ + private fun configureLogging() { + val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext + + val rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME) as Logger + rootLogger.level = Level.toLevel(config[Logging.Level.Root], Level.INFO) + + val obsidianLogger = loggerContext.getLogger("obsidian") as Logger + obsidianLogger.level = Level.toLevel(config[Logging.Level.Obsidian], Level.INFO) + } } @Serializable diff --git a/Server/src/main/kotlin/obsidian/server/config/spec/Logging.kt b/Server/src/main/kotlin/obsidian/server/config/spec/Logging.kt new file mode 100644 index 0000000..f3e038b --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/config/spec/Logging.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.config.spec + +import com.uchuhimo.konf.ConfigSpec + +object Logging : ConfigSpec() { + + // TODO: config for logging to files + + object Level : ConfigSpec("level") { + /** + * Root logging level + */ + val Root by optional("INFO") + + /** + * Obsidian logging level + */ + val Obsidian by optional("INFO") + } + +} diff --git a/Server/src/main/kotlin/obsidian/server/util/Obsidian.kt b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt similarity index 99% rename from Server/src/main/kotlin/obsidian/server/util/Obsidian.kt rename to Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt index 6dee19d..98f5464 100644 --- a/Server/src/main/kotlin/obsidian/server/util/Obsidian.kt +++ b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package obsidian.server.util +package obsidian.server.config.spec import com.uchuhimo.konf.ConfigSpec import moe.kyokobot.koe.codec.udpqueue.UdpQueueFramePollerFactory.Companion.DEFAULT_BUFFER_DURATION diff --git a/Server/src/main/kotlin/obsidian/server/io/Magma.kt b/Server/src/main/kotlin/obsidian/server/io/Magma.kt index 5795008..34c3654 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Magma.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Magma.kt @@ -30,7 +30,7 @@ import obsidian.server.io.rest.tracks import obsidian.server.io.ws.CloseReasons import obsidian.server.io.ws.StatsTask import obsidian.server.io.ws.WebSocketHandler -import obsidian.server.util.Obsidian +import obsidian.server.config.spec.Obsidian import obsidian.server.util.threadFactory import org.slf4j.Logger import org.slf4j.LoggerFactory diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt index f36985a..62d7850 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt @@ -40,9 +40,8 @@ import obsidian.server.io.ws.CurrentTrack import obsidian.server.io.ws.Frames import obsidian.server.player.PlayerUpdates.Companion.currentTrackFor import obsidian.server.player.filter.Filters -import obsidian.server.util.Obsidian +import obsidian.server.config.spec.Obsidian import org.slf4j.LoggerFactory -import kotlin.text.Typography.mdash import kotlin.text.Typography.ndash object Players { diff --git a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt index c4d2739..e85cfe3 100644 --- a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt +++ b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt @@ -33,7 +33,7 @@ import com.sedmelluq.lava.extensions.youtuberotator.planner.* import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block import obsidian.server.Application.config -import obsidian.server.util.Obsidian +import obsidian.server.config.spec.Obsidian import org.slf4j.Logger import org.slf4j.LoggerFactory import java.net.InetAddress diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index 2be66c0..a8ff7aa 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -25,7 +25,7 @@ import obsidian.server.Application.config import obsidian.server.io.ws.CurrentTrack import obsidian.server.io.ws.PlayerUpdate import obsidian.server.util.Interval -import obsidian.server.util.Obsidian +import obsidian.server.config.spec.Obsidian import obsidian.server.util.TrackUtil class PlayerUpdates(val player: Player) : AudioEventAdapter() { diff --git a/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt b/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt index 6007fe8..a76c5fe 100644 --- a/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt +++ b/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt @@ -23,6 +23,7 @@ import io.ktor.http.* import io.ktor.request.* import io.ktor.response.* import io.ktor.util.pipeline.* +import obsidian.server.config.spec.Obsidian object AuthorizationPipeline { /** diff --git a/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt index aca4fc8..b140d31 100644 --- a/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt @@ -26,6 +26,7 @@ import moe.kyokobot.koe.codec.netty.NettyFramePollerFactory import moe.kyokobot.koe.codec.udpqueue.UdpQueueFramePollerFactory import moe.kyokobot.koe.gateway.GatewayVersion import obsidian.server.Application.config +import obsidian.server.config.spec.Obsidian import org.slf4j.Logger import org.slf4j.LoggerFactory From 4f0ae8abeab94cb156f6d9cdd6b68550988a06bc Mon Sep 17 00:00:00 2001 From: 2D Date: Wed, 26 May 2021 06:47:37 -0700 Subject: [PATCH 08/46] :sparkles: suspending lavaplayer events and a working websocket --- Server/build.gradle.kts | 1 + .../main/kotlin/obsidian/server/io/Magma.kt | 35 ++++++++-- .../kotlin/obsidian/server/io/MagmaClient.kt | 2 +- .../kotlin/obsidian/server/io/rest/players.kt | 35 +++------- .../kotlin/obsidian/server/io/ws/Dispatch.kt | 30 ++++++-- .../kotlin/obsidian/server/io/ws/StatsTask.kt | 1 - .../obsidian/server/io/ws/WebSocketHandler.kt | 48 ++++++------- .../kotlin/obsidian/server/player/Player.kt | 68 ++++++++++++++++++- .../obsidian/server/player/PlayerUpdates.kt | 14 ++-- .../server/util/CoroutineAudioEventAdapter.kt | 58 ++++++++++++++++ Server/src/main/resources/version.txt | 2 +- 11 files changed, 225 insertions(+), 69 deletions(-) create mode 100644 Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 32bd071..086152f 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -66,6 +66,7 @@ tasks.withType { incremental = true freeCompilerArgs = listOf( "-Xopt-in=kotlin.ExperimentalStdlibApi", + "-Xopt-in=kotlin.RequiresOptIn", "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-Xopt-in=io.ktor.locations.KtorExperimentalLocationsAPI", "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi" diff --git a/Server/src/main/kotlin/obsidian/server/io/Magma.kt b/Server/src/main/kotlin/obsidian/server/io/Magma.kt index 34c3654..b17f222 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Magma.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Magma.kt @@ -18,19 +18,21 @@ package obsidian.server.io import io.ktor.application.* import io.ktor.auth.* +import io.ktor.features.* +import io.ktor.http.* import io.ktor.http.cio.websocket.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* +import io.ktor.util.* import io.ktor.websocket.* import obsidian.server.Application.config import obsidian.server.io.rest.Players.players -import obsidian.server.io.rest.planner -import obsidian.server.io.rest.tracks import obsidian.server.io.ws.CloseReasons import obsidian.server.io.ws.StatsTask import obsidian.server.io.ws.WebSocketHandler import obsidian.server.config.spec.Obsidian +import obsidian.server.io.rest.* import obsidian.server.util.threadFactory import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -41,6 +43,8 @@ import kotlin.text.Typography.mdash object Magma { + val ClientName = AttributeKey("ClientName") + /** * All connected clients. */ @@ -71,6 +75,25 @@ object Magma { } } + intercept(ApplicationCallPipeline.Call) { + /* extract client name from the request */ + val clientName = call.request.clientName() + + /* log out the request */ + log.info(with(call.request) { + "${clientName ?: origin.remoteHost} ${Typography.ndash} ${httpMethod.value.padEnd(4, ' ')} $uri" + }) + + /* check if a client name is required, if so check if there was a provided client name */ + if (clientName == null && config[Obsidian.requireClientName]) { + return@intercept respondAndFinish(HttpStatusCode.BadRequest, Response("Missing 'Client-Name' header or query parameter.")) + } + + if (clientName != null) { + call.attributes.put(ClientName, clientName) + } + } + /* websocket */ webSocket("/magma") { val request = call.request @@ -78,7 +101,7 @@ object Magma { /* check if client names are required, if so check if one was supplied */ val clientName = request.clientName() if (config[Obsidian.requireClientName] && clientName.isNullOrBlank()) { - log.warn("${request.local.remoteHost} $mdash missing 'Client-Name' header/query parameter.") + log.warn("${request.local.remoteHost} - missing 'Client-Name' header/query parameter.") return@webSocket close(CloseReasons.MISSING_CLIENT_NAME) } @@ -90,17 +113,17 @@ object Magma { ?: request.queryParameters["auth"] if (!Obsidian.Server.validateAuth(auth)) { - log.warn("$display $mdash authentication failed") + log.warn("$display - authentication failed") return@webSocket close(CloseReasons.INVALID_AUTHORIZATION) } - log.info("$display $mdash incoming connection") + log.info("$display - incoming connection") /* check for user id */ val userId = request.userId() if (userId == null) { /* no user-id was given, close the connection */ - log.info("$display $mdash missing 'User-Id' header/query parameter") + log.info("$display - missing 'User-Id' header/query parameter") return@webSocket close(CloseReasons.MISSING_USER_ID) } diff --git a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt index c0fc178..834a39e 100644 --- a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt +++ b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt @@ -96,7 +96,7 @@ class MagmaClient(val userId: Long) { * Whether we should be cautious about shutting down. */ suspend fun shutdown(safe: Boolean = true) { - websocket?.session?.close(CloseReason(1000, "shutting down")) + websocket?.shutdown() websocket = null val activePlayers = players.count { (_, player) -> diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt index 62d7850..5c3e06e 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt @@ -41,13 +41,13 @@ import obsidian.server.io.ws.Frames import obsidian.server.player.PlayerUpdates.Companion.currentTrackFor import obsidian.server.player.filter.Filters import obsidian.server.config.spec.Obsidian +import obsidian.server.io.Magma.ClientName import org.slf4j.LoggerFactory import kotlin.text.Typography.ndash object Players { private val ClientAttr = AttributeKey("MagmaClient") private val GuildAttr = AttributeKey("Guild-Id") - private val log = LoggerFactory.getLogger(Players::class.java) fun Routing.players() = this.authenticate { this.route("/players/{guild}") { @@ -55,40 +55,27 @@ object Players { * Extracts useful information from each application call. */ intercept(ApplicationCallPipeline.Call) { - /* extract client name from the request */ - val clientName = call.request.clientName() - - /* log out the request */ - log.info(with(call.request) { - "${clientName ?: origin.remoteHost} $ndash ${httpMethod.value.padEnd(4, ' ')} $uri" - }) - - /* check if a client name is required, if so check if there was a provided client name */ - if (clientName == null && config[Obsidian.requireClientName]) { - return@intercept respondAndFinish(BadRequest, Response("Missing 'Client-Name' header or query parameter.")) - } - /* get the guild id */ val guildId = call.parameters["guild"]?.toLongOrNull() ?: return@intercept respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) - context.attributes.put(GuildAttr, guildId) + call.attributes.put(GuildAttr, guildId) /* extract user id from the http request */ val userId = call.request.userId() ?: return@intercept respondAndFinish(BadRequest, Response("Missing 'User-Id' header or query parameter.")) - context.attributes.put(ClientAttr, Magma.getClient(userId, clientName)) + call.attributes.put(ClientAttr, Magma.getClient(userId, call.attributes.getOrNull(ClientName))) } /** * */ get { - val guildId = context.attributes[GuildAttr] + val guildId = call.attributes[GuildAttr] /* get the requested player */ - val player = context.attributes[ClientAttr].players[guildId] + val player = call.attributes[ClientAttr].players[guildId] ?: return@get respondAndFinish(NotFound, Response("Unknown player for guild '$guildId'")) /* respond */ @@ -101,7 +88,7 @@ object Players { */ put("/submit-voice-server") { val vsi = call.receive().vsi - Handlers.submitVoiceServer(context.attributes[ClientAttr], context.attributes[GuildAttr], vsi) + Handlers.submitVoiceServer(call.attributes[ClientAttr], call.attributes[GuildAttr], vsi) call.respond(Response("successfully queued connection", success = true)) } @@ -110,7 +97,7 @@ object Players { */ put("/filters") { val filters = call.receive() - Handlers.configure(context.attributes[ClientAttr], context.attributes[GuildAttr], filters) + Handlers.configure(call.attributes[ClientAttr], call.attributes[GuildAttr], filters) call.respond(Response("applied filters", success = true)) } @@ -119,7 +106,7 @@ object Players { */ put("/seek") { val (position) = call.receive() - Handlers.seek(context.attributes[ClientAttr], context.attributes[GuildAttr], position) + Handlers.seek(context.attributes[ClientAttr], call.attributes[GuildAttr], position) call.respond(Response("seeked to $position", success = true)) } @@ -127,11 +114,11 @@ object Players { * */ post("/play") { - val client = context.attributes[ClientAttr] + val client = call.attributes[ClientAttr] /* connect to the voice server described in the request body */ val (track, start, end, noReplace) = call.receive() - Handlers.playTrack(client, context.attributes[GuildAttr], track, start, end, noReplace) + Handlers.playTrack(client, call.attributes[GuildAttr], track, start, end, noReplace) /* respond */ call.respond(Response("playback has started", success = true)) @@ -141,7 +128,7 @@ object Players { * */ post("/stop") { - Handlers.stopTrack(context.attributes[ClientAttr], context.attributes[GuildAttr]) + Handlers.stopTrack(call.attributes[ClientAttr], call.attributes[GuildAttr]) call.respond(Response("stopped the current track, if any.", success = true)) } } diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt index b71743b..14cd6cd 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt @@ -94,10 +94,8 @@ data class PlayerUpdate( @Serializable(with = LongAsStringSerializer::class) @SerialName("guild_id") val guildId: Long, - - @SerialName("frames") val frames: Frames, - + val filters: obsidian.server.player.filter.Filters?, @SerialName("current_track") val currentTrack: CurrentTrack ) : Dispatch() @@ -106,7 +104,7 @@ data class PlayerUpdate( data class CurrentTrack( val track: String, val position: Long, - val paused: Boolean + val paused: Boolean, ) @Serializable @@ -206,11 +204,31 @@ data class TrackExceptionEvent( val message: String?, val severity: FriendlyException.Severity, val cause: String? - ) + ) { + companion object { + /** + * Creates an [Exception] object from the supplied [FriendlyException] + * + * @param exc + * The friendly exception to use + */ + fun fromFriendlyException(exc: FriendlyException): Exception = Exception( + message = exc.message, + severity = exc.severity, + cause = exc.cause?.message + ) + } + } } @Serializable -data class Stats(val memory: Memory, val cpu: CPU, val threads: Threads, val frames: List, val players: Players?) : Dispatch() { +data class Stats( + val memory: Memory, + val cpu: CPU, + val threads: Threads, + val frames: List, + val players: Players? +) : Dispatch() { @Serializable data class Memory( @SerialName("heap_used") val heapUsed: Usage, diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt index ea1ae29..c6073a1 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt @@ -45,7 +45,6 @@ object StatsTask { fun build(client: MagmaClient?): Stats { /* memory stats. */ - println("${Runtime.getRuntime().let { it.totalMemory() - it.freeMemory() } / 1024 / 1024}mb") val memory = ManagementFactory.getMemoryMXBean().let { bean -> val heapUsed = bean.heapMemoryUsage.let { Stats.Memory.Usage(committed = it.committed, max = it.max, init = it.init, used = it.used) diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt index 841befa..6015dc2 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt @@ -29,15 +29,14 @@ import obsidian.server.io.Handlers import obsidian.server.io.Magma.cleanupExecutor import obsidian.server.io.MagmaClient import obsidian.server.util.Interval +import obsidian.server.util.threadFactory import org.slf4j.Logger import org.slf4j.LoggerFactory import java.lang.Runnable +import java.util.concurrent.* import java.util.concurrent.CancellationException -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext -import kotlin.text.Typography.ndash +import kotlin.coroutines.suspendCoroutine import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -51,7 +50,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess /** * Stats interval. */ - private var stats = Interval(Dispatchers.IO) + private var stats = Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Stats-Dispatcher %d", daemon = true)) /** * Whether this magma client is active @@ -126,12 +125,12 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess resumeKey = key resumeTimeout = timeout - logger.debug("${client.displayName} $ndash Resuming has been configured; key= $key, timeout= $timeout") + logger.debug("${client.displayName} - Resuming has been configured; key= $key, timeout= $timeout") } on { bufferTimeout = timeout - logger.debug("${client.displayName} $ndash Dispatch buffer timeout: $timeout") + logger.debug("${client.displayName} - Dispatch buffer timeout: $timeout") } } @@ -144,14 +143,8 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess active = true /* starting sending stats. */ - coroutineScope { - launch(coroutineContext) { - stats.start(Duration.minutes(1).inWholeMilliseconds) { - val stats = StatsTask.build(client) - send(stats) - } - } - } + val statsRunnable = StatsTask.getRunnable(this) + stats.scheduleAtFixedRate(statsRunnable, 0, 1, TimeUnit.MINUTES) /* listen for incoming frames. */ session.incoming @@ -185,7 +178,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess } resumeTimeoutFuture = cleanupExecutor.schedule(runnable, resumeTimeout!!, TimeUnit.MILLISECONDS) - logger.info("${client.displayName} $ndash Session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") + logger.info("${client.displayName} - Session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") return } @@ -196,7 +189,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess * Resumes this session */ suspend fun resume(session: WebSocketServerSession) { - logger.info("${client.displayName} $ndash session has been resumed") + logger.info("${client.displayName} - session has been resumed") this.session = session this.active = true @@ -223,7 +216,16 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess return } - send(json) + send(json, dispatch::class.simpleName) + } + + /** + * Shuts down this websocket handler + */ + suspend fun shutdown() { + stats.shutdownNow() + currentCoroutineContext().cancel() + session.close(CloseReason(1000, "shutting down")) } /** @@ -231,12 +233,12 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess * * @param json JSON encoded dispatch payload */ - private suspend fun send(json: String) { + private suspend fun send(json: String, payloadName: String? = null) { try { - logger.trace("${client.displayName} <<< $json") + logger.trace("${client.displayName} ${payloadName?.let { "$it " } ?: ""}<<< $json") session.send(json) } catch (ex: Exception) { - logger.error("${client.displayName} $ndash An exception occurred while sending a json payload", ex) + logger.error("${client.displayName} - An exception occurred while sending a json payload", ex) } } @@ -250,7 +252,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess try { block.invoke(it) } catch (ex: Exception) { - logger.error("${client.displayName} $ndash An exception occurred while handling a command", ex) + logger.error("${client.displayName} - An exception occurred while handling a command", ex) } } } @@ -269,7 +271,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess logger.info("${client.displayName} >>> $data") json.decodeFromString(Operation, data)?.let { events.emit(it) } } catch (ex: Exception) { - logger.error("${client.displayName} $ndash An exception occurred while handling an incoming frame", ex) + logger.error("${client.displayName} - An exception occurred while handling an incoming frame", ex) } } diff --git a/Server/src/main/kotlin/obsidian/server/player/Player.kt b/Server/src/main/kotlin/obsidian/server/player/Player.kt index 3ee7347..4d8ef7e 100644 --- a/Server/src/main/kotlin/obsidian/server/player/Player.kt +++ b/Server/src/main/kotlin/obsidian/server/player/Player.kt @@ -19,17 +19,24 @@ package obsidian.server.player import com.sedmelluq.discord.lavaplayer.format.StandardAudioDataFormats import com.sedmelluq.discord.lavaplayer.player.AudioPlayer import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame import io.netty.buffer.ByteBuf +import kotlinx.coroutines.Dispatchers import moe.kyokobot.koe.MediaConnection import moe.kyokobot.koe.media.OpusAudioFrameProvider import obsidian.server.Application.players import obsidian.server.io.MagmaClient +import obsidian.server.io.ws.TrackExceptionEvent +import obsidian.server.io.ws.TrackStartEvent +import obsidian.server.io.ws.TrackStuckEvent import obsidian.server.player.filter.Filters +import obsidian.server.util.CoroutineAudioEventAdapter import java.nio.ByteBuffer -class Player(val guildId: Long, val client: MagmaClient) { +class Player(val guildId: Long, val client: MagmaClient) : CoroutineAudioEventAdapter(Dispatchers.IO) { /** * Handles all updates for this player. @@ -45,6 +52,7 @@ class Player(val guildId: Long, val client: MagmaClient) { players.createPlayer() .addEventListener(frameLossTracker) .addEventListener(updates) + .addEventListener(this) } /** @@ -101,6 +109,64 @@ class Player(val guildId: Long, val client: MagmaClient) { connection.audioSender = OpusFrameProvider(connection) } + /** + * + */ + override suspend fun onTrackStuck(thresholdMs: Long, track: AudioTrack, player: AudioPlayer) { + client.websocket?.let { + val event = TrackStuckEvent( + guildId = guildId, + thresholdMs = thresholdMs, + track = track + ) + + it.send(event) + } + } + + /** + * + */ + override suspend fun onTrackException(exception: FriendlyException, track: AudioTrack, player: AudioPlayer) { + client.websocket?.let { + val event = TrackExceptionEvent( + guildId = guildId, + track = track, + exception = TrackExceptionEvent.Exception.fromFriendlyException(exception) + ) + + it.send(event) + } + } + + /** + * + */ + override suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) { + client.websocket?.let { + val event = TrackStartEvent( + guildId = guildId, + track = track + ) + + it.send(event) + } + } + + /** + * Sends a track end player event to the websocket connection, if any. + */ + override suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) { + client.websocket?.let { + val event = TrackStartEvent( + track = track, + guildId = guildId + ) + + it.send(event) + } + } + /** * */ diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index a8ff7aa..1f173c6 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -26,9 +26,10 @@ import obsidian.server.io.ws.CurrentTrack import obsidian.server.io.ws.PlayerUpdate import obsidian.server.util.Interval import obsidian.server.config.spec.Obsidian +import obsidian.server.util.CoroutineAudioEventAdapter import obsidian.server.util.TrackUtil -class PlayerUpdates(val player: Player) : AudioEventAdapter() { +class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { /** * Whether player updates should be sent. */ @@ -65,18 +66,19 @@ class PlayerUpdates(val player: Player) : AudioEventAdapter() { val update = PlayerUpdate( guildId = player.guildId, currentTrack = currentTrackFor(player), - frames = player.frameLossTracker.payload + frames = player.frameLossTracker.payload, + filters = player.filters ) player.client.websocket?.send(update) } - override fun onTrackStart(player: AudioPlayer?, track: AudioTrack?) { - this.player.client.websocket?.launch { start() } + override suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) { + start() } - override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack?, endReason: AudioTrackEndReason?) { - this.player.client.websocket?.launch { stop() } + override suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) { + stop() } companion object { diff --git a/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt new file mode 100644 index 0000000..cbc759a --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2021 MixtapeBot and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package obsidian.server.util + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer +import com.sedmelluq.discord.lavaplayer.player.event.* +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException +import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason +import kotlinx.coroutines.* +import kotlin.coroutines.CoroutineContext + +open class CoroutineAudioEventAdapter(private val dispatcher: CoroutineDispatcher = Dispatchers.Default) : + AudioEventListener, + CoroutineScope { + + override val coroutineContext: CoroutineContext + get() = dispatcher + SupervisorJob() + + /* playback start/end */ + open suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) = Unit + open suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) = Unit + + /* exception */ + open suspend fun onTrackStuck(thresholdMs: Long, track: AudioTrack, player: AudioPlayer) = Unit + open suspend fun onTrackException(exception: FriendlyException, track: AudioTrack, player: AudioPlayer) = Unit + + /* playback state */ + open suspend fun onPlayerResume(player: AudioPlayer) = Unit + open suspend fun onPlayerPause(player: AudioPlayer) = Unit + + override fun onEvent(event: AudioEvent) { + launch { + when (event) { + is TrackStartEvent -> onTrackStart(event.track, event.player) + is TrackEndEvent -> onTrackEnd(event.track, event.endReason, event.player) + is TrackStuckEvent -> onTrackStuck(event.thresholdMs, event.track, event.player) + is TrackExceptionEvent -> onTrackException(event.exception, event.track, event.player) + is PlayerResumeEvent -> onPlayerResume(event.player) + is PlayerPauseEvent -> onPlayerPause(event.player) + } + } + } +} diff --git a/Server/src/main/resources/version.txt b/Server/src/main/resources/version.txt index 60c2fcc..407bea3 100644 --- a/Server/src/main/resources/version.txt +++ b/Server/src/main/resources/version.txt @@ -1,2 +1,2 @@ 2.0.0 -2991a27 \ No newline at end of file +16d5fc8 \ No newline at end of file From 807712273403965ec56d590fc721f0ca4ea84e1e Mon Sep 17 00:00:00 2001 From: 2D Date: Wed, 26 May 2021 10:11:28 -0700 Subject: [PATCH 09/46] :zap: int -> short --- Server/src/main/kotlin/obsidian/server/io/ws/Op.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt index a10c02e..fdb4cda 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt @@ -25,7 +25,7 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder @Serializable(with = Op.Serializer::class) -enum class Op(val code: Int) { +enum class Op(val code: Short) { SUBMIT_VOICE_UPDATE(0), STATS(1), @@ -47,15 +47,15 @@ enum class Op(val code: Int) { companion object Serializer : KSerializer { override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("MagmaOperation", PrimitiveKind.INT) + PrimitiveSerialDescriptor("MagmaOperation", PrimitiveKind.SHORT) override fun deserialize(decoder: Decoder): Op { - val code = decoder.decodeInt() + val code = decoder.decodeShort() return values().firstOrNull { it.code == code } ?: UNKNOWN } override fun serialize(encoder: Encoder, value: Op) { - encoder.encodeInt(value.code) + encoder.encodeShort(value.code) } } } From c4fc0400bfa3ec39e1da65e984de7523b2080462 Mon Sep 17 00:00:00 2001 From: 2D Date: Wed, 26 May 2021 10:13:03 -0700 Subject: [PATCH 10/46] :goal_net: more cancellation error handling --- .../obsidian/server/io/ws/WebSocketHandler.kt | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt index 6015dc2..b7c7c57 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt @@ -28,7 +28,6 @@ import obsidian.server.Application.json import obsidian.server.io.Handlers import obsidian.server.io.Magma.cleanupExecutor import obsidian.server.io.MagmaClient -import obsidian.server.util.Interval import obsidian.server.util.threadFactory import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -36,11 +35,9 @@ import java.lang.Runnable import java.util.concurrent.* import java.util.concurrent.CancellationException import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.suspendCoroutine -import kotlin.time.Duration import kotlin.time.ExperimentalTime -class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSession) : CoroutineScope { +class WebSocketHandler(val client: MagmaClient, private var session: WebSocketServerSession) : CoroutineScope { /** * Resume key @@ -50,7 +47,8 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess /** * Stats interval. */ - private var stats = Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Stats-Dispatcher %d", daemon = true)) + private var stats = + Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Stats-Dispatcher %d", daemon = true)) /** * Whether this magma client is active @@ -83,7 +81,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess private val events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + SupervisorJob() + get() = Dispatchers.IO + Job() init { /* websocket and rest operations */ @@ -125,12 +123,12 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess resumeKey = key resumeTimeout = timeout - logger.debug("${client.displayName} - Resuming has been configured; key= $key, timeout= $timeout") + log.debug("${client.displayName} - Resuming has been configured; key= $key, timeout= $timeout") } on { bufferTimeout = timeout - logger.debug("${client.displayName} - Dispatch buffer timeout: $timeout") + log.debug("${client.displayName} - Dispatch buffer timeout: $timeout") } } @@ -158,6 +156,8 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess } } + log.info("${client.displayName} - web-socket session has closed.") + /* connection has been closed. */ active = false } @@ -178,7 +178,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess } resumeTimeoutFuture = cleanupExecutor.schedule(runnable, resumeTimeout!!, TimeUnit.MILLISECONDS) - logger.info("${client.displayName} - Session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") + log.info("${client.displayName} - Session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") return } @@ -189,7 +189,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess * Resumes this session */ suspend fun resume(session: WebSocketServerSession) { - logger.info("${client.displayName} - session has been resumed") + log.info("${client.displayName} - session has been resumed") this.session = session this.active = true @@ -209,9 +209,9 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess * * @param dispatch The dispatch instance */ - suspend fun send(dispatch: Dispatch) { + fun send(dispatch: Dispatch) { val json = json.encodeToString(Dispatch.Companion, dispatch) - if (!active) { + if (!session.isActive) { dispatchBuffer?.offer(json) return } @@ -224,8 +224,19 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess */ suspend fun shutdown() { stats.shutdownNow() - currentCoroutineContext().cancel() - session.close(CloseReason(1000, "shutting down")) + + /* cancel this coroutine context */ + try { + currentCoroutineContext().cancelChildren() + currentCoroutineContext().cancel() + } catch (ex: Exception) { + log.warn("${client.displayName} - Error occurred while cancelling this coroutine scope") + } + + /* close the websocket session, if not already */ + if (active) { + session.close(CloseReason(1000, "shutting down")) + } } /** @@ -233,12 +244,12 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess * * @param json JSON encoded dispatch payload */ - private suspend fun send(json: String, payloadName: String? = null) { + private fun send(json: String, payloadName: String? = null) { try { - logger.trace("${client.displayName} ${payloadName?.let { "$it " } ?: ""}<<< $json") - session.send(json) + log.trace("${client.displayName} ${payloadName?.let { "$it " } ?: ""}<<< $json") + session.outgoing.trySend(Frame.Text(json)) } catch (ex: Exception) { - logger.error("${client.displayName} - An exception occurred while sending a json payload", ex) + log.error("${client.displayName} - An exception occurred while sending a json payload", ex) } } @@ -252,7 +263,7 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess try { block.invoke(it) } catch (ex: Exception) { - logger.error("${client.displayName} - An exception occurred while handling a command", ex) + log.error("${client.displayName} - An exception occurred while handling a command", ex) } } } @@ -264,14 +275,14 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess * * @param frame The received text or binary frame. */ - private suspend fun handleIncomingFrame(frame: Frame) { + private fun handleIncomingFrame(frame: Frame) { val data = frame.data.toString(Charset.defaultCharset()) try { - logger.info("${client.displayName} >>> $data") - json.decodeFromString(Operation, data)?.let { events.emit(it) } + log.info("${client.displayName} >>> $data") + json.decodeFromString(Operation, data)?.let { events.tryEmit(it) } } catch (ex: Exception) { - logger.error("${client.displayName} - An exception occurred while handling an incoming frame", ex) + log.error("${client.displayName} - An exception occurred while handling an incoming frame", ex) } } @@ -284,6 +295,6 @@ class WebSocketHandler(val client: MagmaClient, var session: WebSocketServerSess } } - private val logger: Logger = LoggerFactory.getLogger(MagmaClient::class.java) + private val log: Logger = LoggerFactory.getLogger(MagmaClient::class.java) } } From 09521ea1bdfaed6a927b3e2198d12c09096e5158 Mon Sep 17 00:00:00 2001 From: 2D Date: Wed, 26 May 2021 10:14:31 -0700 Subject: [PATCH 11/46] :sparkles: stuff --- Server/build.gradle.kts | 3 +- .../kotlin/obsidian/server/io/Handlers.kt | 4 +-- .../main/kotlin/obsidian/server/io/Magma.kt | 7 +++-- .../kotlin/obsidian/server/io/MagmaClient.kt | 1 - .../kotlin/obsidian/server/player/Player.kt | 19 +++++++------ .../obsidian/server/player/PlayerUpdates.kt | 18 ++++++------ .../server/util/LogbackColorConverter.kt | 28 +++++++++++-------- Server/src/main/resources/version.txt | 2 +- 8 files changed, 45 insertions(+), 37 deletions(-) diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 086152f..1f05162 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -28,8 +28,8 @@ dependencies { implementation("io.ktor:ktor-server-core:$ktorVersion") // ktor server core implementation("io.ktor:ktor-server-cio:$ktorVersion") // ktor cio engine implementation("io.ktor:ktor-locations:$ktorVersion") // ktor locations - implementation("io.ktor:ktor-websockets:$ktorVersion") // ktor websockets implementation("io.ktor:ktor-serialization:$ktorVersion") // ktor serialization + implementation("io.ktor:ktor-websockets:$ktorVersion") // ktor websockets implementation("moe.kyokobot.koe:core:master-SNAPSHOT") { // discord send system exclude(group = "org.slf4j", module = "slf4j-api") @@ -48,7 +48,6 @@ dependencies { implementation("com.github.natanbc:lp-cross:0.1.3-1") // lp-cross natives implementation("ch.qos.logback:logback-classic:1.2.3") // slf4j logging backend - implementation("com.github.ajalt.mordant:mordant:2.0.0-beta1") // terminal coloring & styling val konfVersion = "1.1.2" implementation("com.github.uchuhimo.konf:konf-core:$konfVersion") // konf core shit diff --git a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt index 91c9148..11fda49 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt @@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory object Handlers { - val log: Logger = LoggerFactory.getLogger(Handlers::class.java) + private val log: Logger = LoggerFactory.getLogger(Handlers::class.java) fun submitVoiceServer(client: MagmaClient, guildId: Long, vsi: VoiceServerInfo) { val connection = client.mediaConnectionFor(guildId) @@ -45,7 +45,7 @@ object Handlers { client.koe.destroyConnection(guildId) } - suspend fun playTrack( + fun playTrack( client: MagmaClient, guildId: Long, track: String, diff --git a/Server/src/main/kotlin/obsidian/server/io/Magma.kt b/Server/src/main/kotlin/obsidian/server/io/Magma.kt index b17f222..670e0c8 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Magma.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Magma.kt @@ -26,6 +26,7 @@ import io.ktor.response.* import io.ktor.routing.* import io.ktor.util.* import io.ktor.websocket.* +import kotlinx.coroutines.isActive import obsidian.server.Application.config import obsidian.server.io.rest.Players.players import obsidian.server.io.ws.CloseReasons @@ -204,8 +205,10 @@ object Magma { try { wsh.listen() } catch (ex: Exception) { - log.error("${client.displayName} threw an error", ex) - wss.close(CloseReason(4006, ex.message ?: ex.cause?.message ?: "unknown error")) + log.error("${client.displayName} - An error occurred while listening for frames.", ex) + if (wss.isActive) { + wss.close(CloseReason(4006, ex.message ?: ex.cause?.message ?: "unknown error")) + } } wsh.handleClose() diff --git a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt index 834a39e..ed95346 100644 --- a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt +++ b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt @@ -16,7 +16,6 @@ package obsidian.server.io -import io.ktor.http.cio.websocket.* import kotlinx.coroutines.launch import moe.kyokobot.koe.KoeClient import moe.kyokobot.koe.KoeEventAdapter diff --git a/Server/src/main/kotlin/obsidian/server/player/Player.kt b/Server/src/main/kotlin/obsidian/server/player/Player.kt index 4d8ef7e..d036a65 100644 --- a/Server/src/main/kotlin/obsidian/server/player/Player.kt +++ b/Server/src/main/kotlin/obsidian/server/player/Player.kt @@ -18,25 +18,25 @@ package obsidian.server.player import com.sedmelluq.discord.lavaplayer.format.StandardAudioDataFormats import com.sedmelluq.discord.lavaplayer.player.AudioPlayer +import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame import io.netty.buffer.ByteBuf -import kotlinx.coroutines.Dispatchers import moe.kyokobot.koe.MediaConnection import moe.kyokobot.koe.media.OpusAudioFrameProvider import obsidian.server.Application.players import obsidian.server.io.MagmaClient +import obsidian.server.io.ws.TrackEndEvent import obsidian.server.io.ws.TrackExceptionEvent import obsidian.server.io.ws.TrackStartEvent import obsidian.server.io.ws.TrackStuckEvent import obsidian.server.player.filter.Filters -import obsidian.server.util.CoroutineAudioEventAdapter import java.nio.ByteBuffer -class Player(val guildId: Long, val client: MagmaClient) : CoroutineAudioEventAdapter(Dispatchers.IO) { +class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { /** * Handles all updates for this player. @@ -78,7 +78,7 @@ class Player(val guildId: Long, val client: MagmaClient) : CoroutineAudioEventAd /** * Plays the provided [track] and dispatches a Player Update */ - suspend fun play(track: AudioTrack) { + fun play(track: AudioTrack) { audioPlayer.playTrack(track) updates.sendUpdate() } @@ -112,7 +112,7 @@ class Player(val guildId: Long, val client: MagmaClient) : CoroutineAudioEventAd /** * */ - override suspend fun onTrackStuck(thresholdMs: Long, track: AudioTrack, player: AudioPlayer) { + override fun onTrackStuck(player: AudioPlayer?, track: AudioTrack?, thresholdMs: Long) { client.websocket?.let { val event = TrackStuckEvent( guildId = guildId, @@ -127,7 +127,7 @@ class Player(val guildId: Long, val client: MagmaClient) : CoroutineAudioEventAd /** * */ - override suspend fun onTrackException(exception: FriendlyException, track: AudioTrack, player: AudioPlayer) { + override fun onTrackException(player: AudioPlayer?, track: AudioTrack, exception: FriendlyException) { client.websocket?.let { val event = TrackExceptionEvent( guildId = guildId, @@ -142,7 +142,7 @@ class Player(val guildId: Long, val client: MagmaClient) : CoroutineAudioEventAd /** * */ - override suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) { + override fun onTrackStart(player: AudioPlayer?, track: AudioTrack) { client.websocket?.let { val event = TrackStartEvent( guildId = guildId, @@ -156,10 +156,11 @@ class Player(val guildId: Long, val client: MagmaClient) : CoroutineAudioEventAd /** * Sends a track end player event to the websocket connection, if any. */ - override suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) { + override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack, reason: AudioTrackEndReason) { client.websocket?.let { - val event = TrackStartEvent( + val event = TrackEndEvent( track = track, + endReason = reason, guildId = guildId ) diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index 1f173c6..66428f5 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -62,15 +62,17 @@ class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { } } - suspend fun sendUpdate() { - val update = PlayerUpdate( - guildId = player.guildId, - currentTrack = currentTrackFor(player), - frames = player.frameLossTracker.payload, - filters = player.filters - ) + fun sendUpdate() { + player.client.websocket?.let { + val update = PlayerUpdate( + guildId = player.guildId, + currentTrack = currentTrackFor(player), + frames = player.frameLossTracker.payload, + filters = player.filters + ) - player.client.websocket?.send(update) + it.send(update) + } } override suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) { diff --git a/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt b/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt index 5393673..64ab309 100644 --- a/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt +++ b/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt @@ -19,8 +19,8 @@ package obsidian.server.util import ch.qos.logback.classic.Level import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.pattern.CompositeConverter -import com.github.ajalt.mordant.rendering.TextColors.* -import com.github.ajalt.mordant.rendering.TextStyles.* + +fun interface Convert { fun take(str: String): String } class LogbackColorConverter : CompositeConverter() { override fun transform(event: ILoggingEvent, element: String): String { @@ -28,21 +28,25 @@ class LogbackColorConverter : CompositeConverter() { ?: ANSI_COLORS[LEVELS[event.level.toInt()]] ?: ANSI_COLORS["green"] - return option!!(element) + return option!!.take(element) } companion object { - private val ANSI_COLORS = mapOf String>( - "red" to { t -> red(t) }, - "green" to { t -> green(t) }, - "yellow" to { t -> yellow(t) }, - "blue" to { t -> blue(t) }, - "magenta" to { t -> magenta(t) }, - "cyan" to { t -> cyan(t) }, - "gray" to { t -> gray(t) }, - "faint" to { t -> dim(t) } + val Number.ansi: String + get() = "\u001b[${this}m" + + private val ANSI_COLORS = mutableMapOf( + "gray" to Convert { t -> "${90.ansi}$t${39.ansi}" }, + "faint" to Convert { t -> "${2.ansi}$t${22.ansi}" } ) + init { + val names = listOf("red", "green", "yellow", "blue", "magenta", "cyan") + for ((idx, code) in (31..36).withIndex()) { + ANSI_COLORS[names[idx]] = Convert { t -> "${code.ansi}$t${39.ansi}" } + } + } + private val LEVELS = mapOf( Level.ERROR_INTEGER to "red", Level.WARN_INTEGER to "yellow", diff --git a/Server/src/main/resources/version.txt b/Server/src/main/resources/version.txt index 407bea3..0a1e116 100644 --- a/Server/src/main/resources/version.txt +++ b/Server/src/main/resources/version.txt @@ -1,2 +1,2 @@ 2.0.0 -16d5fc8 \ No newline at end of file +4f0ae8a \ No newline at end of file From 46eeb4ffcdb9d6412f0e3efdece0a8c03d72d0b5 Mon Sep 17 00:00:00 2001 From: 2D Date: Wed, 26 May 2021 10:14:53 -0700 Subject: [PATCH 12/46] :zap: use value classes for some filter --- .../player/filter/impl/EqualizerFilter.kt | 3 ++- .../server/player/filter/impl/LowPassFilter.kt | 3 ++- .../player/filter/impl/RotationFilter.kt | 6 ++---- .../server/player/filter/impl/VolumeFilter.kt | 3 ++- {api => docs}/README.md | 0 {api => docs}/ws-rest.md | 0 {api => docs}/ws/README.md | 0 {api => docs}/ws/payloads.md | 18 +++++++++--------- {api => docs}/ws/protocol.md | 0 9 files changed, 17 insertions(+), 16 deletions(-) rename {api => docs}/README.md (100%) rename {api => docs}/ws-rest.md (100%) rename {api => docs}/ws/README.md (100%) rename {api => docs}/ws/payloads.md (95%) rename {api => docs}/ws/protocol.md (100%) diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt index fe072e6..d1e626f 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt @@ -23,7 +23,8 @@ import obsidian.server.player.filter.Filter import kotlinx.serialization.Serializable @Serializable -data class EqualizerFilter(val bands: List) : Filter { +@JvmInline +value class EqualizerFilter(val bands: List) : Filter { override val enabled: Boolean get() = bands.any { Filter.isSet(it.gain, 0f) diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt index 53291f7..81d4bdd 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt @@ -23,7 +23,8 @@ import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter @Serializable -data class LowPassFilter(val smoothing: Float = 20f) : Filter { +@JvmInline +value class LowPassFilter(val smoothing: Float = 20f) : Filter { override val enabled: Boolean get() = Filter.isSet(smoothing, 20f) diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt index 9318b20..625e9a8 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt @@ -24,10 +24,8 @@ import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter @Serializable -data class RotationFilter( - @SerialName("rotation_hz") - val rotationHz: Float = 5f -) : Filter { +@JvmInline +value class RotationFilter(val rotationHz: Float = 5f) : Filter { override val enabled: Boolean get() = Filter.isSet(rotationHz, 5f) diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt index 6fa8d0d..d546bdc 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt @@ -23,7 +23,8 @@ import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter @Serializable -data class VolumeFilter(val volume: Float) : Filter { +@JvmInline +value class VolumeFilter(val volume: Float) : Filter { override val enabled: Boolean get() = Filter.isSet(volume, 1f) diff --git a/api/README.md b/docs/README.md similarity index 100% rename from api/README.md rename to docs/README.md diff --git a/api/ws-rest.md b/docs/ws-rest.md similarity index 100% rename from api/ws-rest.md rename to docs/ws-rest.md diff --git a/api/ws/README.md b/docs/ws/README.md similarity index 100% rename from api/ws/README.md rename to docs/ws/README.md diff --git a/api/ws/payloads.md b/docs/ws/payloads.md similarity index 95% rename from api/ws/payloads.md rename to docs/ws/payloads.md index 095dd55..8c9cf39 100644 --- a/api/ws/payloads.md +++ b/docs/ws/payloads.md @@ -345,13 +345,13 @@ List of current player events. *Example:* - `depth` Effect depth • `0 < x ≤ 1` -- `rotation` This filter simulates an audio source rotating around the listener - - `rotation_hz` The frequency the audio should rotate around the listener, in Hertz +- `rotation` The frequency the audio should rotate around the listener, in Hertz + - This filter simulates an audio source rotating around the listener. -- `low_pass` Higher frequencies get suppressed, while lower frequencies pass through this filter, thus the name low pass - - `smoothing` Smoothing to use. 20 is the default - - +- `low_pass` Smoothing to use. 20 is the default + - Higher frequencies get suppressed, while lower frequencies pass through this filter, thus the name low pass + ```json { "op": 9, @@ -359,14 +359,14 @@ List of current player events. *Example:* "guild_id": "751571246189379610", "filters": { "distortion": {}, - "equalizer": { "bands": [] }, + "equalizer": [], "karaoke": {}, - "low_pass": {}, - "rotation": {}, + "low_pass": 20.0, + "rotation": 5.0, "timescale": {}, "tremolo": {}, "vibrato": {}, - "volume": { "volume": 1.0 }, + "volume": 1.0 } } } diff --git a/api/ws/protocol.md b/docs/ws/protocol.md similarity index 100% rename from api/ws/protocol.md rename to docs/ws/protocol.md From d32443c03b281e41c86a4eb0e847659023440803 Mon Sep 17 00:00:00 2001 From: 2D Date: Wed, 26 May 2021 17:11:40 -0700 Subject: [PATCH 13/46] :sparkles: moved config variable, new player update field - Moved the player update interval outside of it's own spec - Added the timestamp in which the player update is constructed, used for calculating accurate player position --- .../obsidian/server/config/spec/Obsidian.kt | 20 +++++-------------- .../kotlin/obsidian/server/io/ws/Dispatch.kt | 3 ++- .../obsidian/server/player/PlayerUpdates.kt | 5 +++-- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt index 98f5464..072aa9f 100644 --- a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt +++ b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt @@ -26,6 +26,11 @@ object Obsidian : ConfigSpec() { */ val requireClientName by optional(false, "require-client-name") + /** + * The delay (in milliseconds) between each player update. + */ + val playerUpdateInterval by optional(5000L, "player-update-interval") + /** * Options related to the HTTP server. */ @@ -58,21 +63,6 @@ object Obsidian : ConfigSpec() { } } - /** - * Options related to player updates - */ - object PlayerUpdates : ConfigSpec("player-updates") { - /** - * The delay (in milliseconds) between each player update. - */ - val Interval by optional(5000L, "interval") - - /** - * Whether the filters object should be sent with Player Updates - */ - val SendFilters by optional(true, "send-filters") - } - /** * Options related to Koe, the discord media library used by Obsidian. */ diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt index 14cd6cd..6052319 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt @@ -97,7 +97,8 @@ data class PlayerUpdate( val frames: Frames, val filters: obsidian.server.player.filter.Filters?, @SerialName("current_track") - val currentTrack: CurrentTrack + val currentTrack: CurrentTrack, + val timestamp: Long ) : Dispatch() @Serializable diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index 66428f5..8569d10 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -49,7 +49,7 @@ class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { */ suspend fun start() { if (!interval.started && enabled) { - interval.start(config[Obsidian.PlayerUpdates.Interval], ::sendUpdate) + interval.start(config[Obsidian.playerUpdateInterval], ::sendUpdate) } } @@ -68,7 +68,8 @@ class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { guildId = player.guildId, currentTrack = currentTrackFor(player), frames = player.frameLossTracker.payload, - filters = player.filters + filters = player.filters, + timestamp = System.currentTimeMillis() ) it.send(update) From 4028617d79a6779bb90dc6812ef18f00b0e793a7 Mon Sep 17 00:00:00 2001 From: 2D Date: Wed, 26 May 2021 18:23:26 -0700 Subject: [PATCH 14/46] :ambulance: fix my stupid logic --- Server/src/main/kotlin/obsidian/server/io/Handlers.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt index 11fda49..fbb87c0 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt @@ -87,7 +87,7 @@ object Handlers { pause: Boolean? = null, sendPlayerUpdates: Boolean? = null ) { - if (filters != null && pause != null && sendPlayerUpdates != null) { + if (filters == null && pause == null && sendPlayerUpdates == null) { return } From e48171985c5ea04c24bd0b3dd19e0e5757ce153d Mon Sep 17 00:00:00 2001 From: 2D Date: Mon, 14 Jun 2021 20:34:48 -0700 Subject: [PATCH 15/46] :memo: add client and separation of api versions. --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e15387..38e2c77 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,13 @@ Now go make a bot with the language and client of your choice! > Clients are used to interface with Magma, Obsidian's WebSocket and REST API. -- [obby.js](https://github.com/Sxmurai/obby.js), NodeJS (v14+) +###### v2 + +- [slate](https://github.com/Axelancerr/Slate), discord.py (Python 3.7+) + +###### v1 + +- [obby.js](https://github.com/Sxmurai/obby.js), generic (NodeJS v14+) **Want to make your own? Read our [API Docs](/API.md)** From e850b5a729f95964fe037e490b4c2c72595d9775 Mon Sep 17 00:00:00 2001 From: Clxud <71564480+cloudwithax@users.noreply.github.com> Date: Wed, 7 Jul 2021 02:26:42 -0400 Subject: [PATCH 16/46] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 38e2c77..637c29a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Now go make a bot with the language and client of your choice! ###### v2 - [slate](https://github.com/Axelancerr/Slate), discord.py (Python 3.7+) +- [obsidian.py](https://github.com/cloudwithax/obsidian.py), discord.py (Python 3.8+) ###### v1 From 80dbc8f9deeeb9fae2b3a0b6796254b4e218ae0c Mon Sep 17 00:00:00 2001 From: melike2d Date: Wed, 7 Jul 2021 00:59:39 -0700 Subject: [PATCH 17/46] :arrow_up: upgrade kotlin, ktor, and lavaplayer --- Server/build.gradle.kts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 1f05162..4785986 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -19,12 +19,12 @@ application { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.10") // standard library - implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.10") // reflection + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.20") // standard library + implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.20") // reflection implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") // core coroutine library implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") // json serialization - val ktorVersion = "main-128" + val ktorVersion = "1.6.1" implementation("io.ktor:ktor-server-core:$ktorVersion") // ktor server core implementation("io.ktor:ktor-server-cio:$ktorVersion") // ktor cio engine implementation("io.ktor:ktor-locations:$ktorVersion") // ktor locations @@ -35,7 +35,7 @@ dependencies { exclude(group = "org.slf4j", module = "slf4j-api") } - implementation("com.sedmelluq:lavaplayer:1.3.77") { // yes + implementation("com.sedmelluq:lavaplayer:1.3.78") { // yes exclude(group = "com.sedmelluq", module = "lavaplayer-natives") } @@ -61,7 +61,7 @@ tasks.withType { tasks.withType { kotlinOptions { - jvmTarget = "16" + jvmTarget = "13" incremental = true freeCompilerArgs = listOf( "-Xopt-in=kotlin.ExperimentalStdlibApi", From b2b1129ddfba11c6278c628cdf6b6bfa9873ab4c Mon Sep 17 00:00:00 2001 From: melike2d Date: Wed, 7 Jul 2021 01:00:35 -0700 Subject: [PATCH 18/46] :wrench: update config file --- Server/src/main/kotlin/obsidian/server/Application.kt | 2 +- obsidian.yml | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/Application.kt b/Server/src/main/kotlin/obsidian/server/Application.kt index 74429db..6c02074 100644 --- a/Server/src/main/kotlin/obsidian/server/Application.kt +++ b/Server/src/main/kotlin/obsidian/server/Application.kt @@ -41,12 +41,12 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import obsidian.server.config.spec.Logging +import obsidian.server.config.spec.Obsidian import obsidian.server.io.Magma import obsidian.server.io.Magma.magma import obsidian.server.player.ObsidianAPM import obsidian.server.util.AuthorizationPipeline.obsidianProvider import obsidian.server.util.NativeUtil -import obsidian.server.config.spec.Obsidian import obsidian.server.util.VersionInfo import org.slf4j.LoggerFactory import kotlin.system.exitProcess diff --git a/obsidian.yml b/obsidian.yml index 1473070..aeef006 100644 --- a/obsidian.yml +++ b/obsidian.yml @@ -1,6 +1,8 @@ obsidian: - port: 3030 - password: "" + server: + host: 0.0.0.0 + port: 3030 + password: "" require-client-name: false player-updates: interval: 5000 @@ -12,8 +14,8 @@ obsidian: enabled-sources: [ "youtube", "yarn", "bandcamp", "twitch", "vimeo", "nico", "soundcloud", "local", "http" ] allow-scsearch: true rate-limit: - ip-blocks: [] - excluded-ips: [] + ip-blocks: [ ] + excluded-ips: [ ] strategy: "rotate-on-ban" # rotate-on-ban | load-balance | nano-switch | rotating-nano-switch search-triggers-fail: true # Whether a search 429 should trigger marking the ip as failing. retry-limit: -1 From 2ddec6ce2fcafece711d3788a9d5262043793e12 Mon Sep 17 00:00:00 2001 From: melike2d Date: Wed, 7 Jul 2021 01:00:47 -0700 Subject: [PATCH 19/46] :ambulance: weird netty things --- Server/src/main/kotlin/obsidian/server/player/Player.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/player/Player.kt b/Server/src/main/kotlin/obsidian/server/player/Player.kt index d036a65..bfad7c9 100644 --- a/Server/src/main/kotlin/obsidian/server/player/Player.kt +++ b/Server/src/main/kotlin/obsidian/server/player/Player.kt @@ -195,9 +195,8 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { } override fun retrieveOpusFrame(targetBuffer: ByteBuf) { - val buffered = frameBuffer.flip() - frameLossTracker.success() - targetBuffer.writeBytes(buffered) + frameBuffer.flip() + targetBuffer.writeBytes(frameBuffer) } } From b32a7731c10421f1440cf9fddde9fcce293f8a61 Mon Sep 17 00:00:00 2001 From: melike2d Date: Sat, 31 Jul 2021 07:32:07 -0700 Subject: [PATCH 20/46] :art: reformat everything --- .github/ISSUE_TEMPLATE/bug_report.md | 7 +- API.md | 12 +- README.md | 2 +- Server/build.gradle.kts | 124 ++--- .../udpqueue/natives/UdpQueueManager.java | 158 +++--- .../natives/UdpQueueManagerLibrary.java | 40 +- .../koe/codec/udpqueue/QueueManagerPool.kt | 72 +-- .../koe/codec/udpqueue/UdpQueueFramePoller.kt | 102 ++-- .../udpqueue/UdpQueueFramePollerFactory.kt | 26 +- .../kotlin/obsidian/server/Application.kt | 240 ++++----- .../obsidian/server/config/spec/Logging.kt | 22 +- .../obsidian/server/config/spec/Obsidian.kt | 298 ++++++------ .../kotlin/obsidian/server/io/Handlers.kt | 122 ++--- .../main/kotlin/obsidian/server/io/Magma.kt | 327 ++++++------- .../kotlin/obsidian/server/io/MagmaClient.kt | 198 ++++---- .../obsidian/server/io/RoutePlannerUtil.kt | 146 +++--- .../kotlin/obsidian/server/io/rest/planner.kt | 66 +-- .../kotlin/obsidian/server/io/rest/players.kt | 217 ++++----- .../kotlin/obsidian/server/io/rest/tracks.kt | 196 ++++---- .../obsidian/server/io/ws/CloseReasons.kt | 10 +- .../kotlin/obsidian/server/io/ws/Dispatch.kt | 352 +++++++------- .../main/kotlin/obsidian/server/io/ws/Op.kt | 48 +- .../kotlin/obsidian/server/io/ws/Operation.kt | 206 ++++---- .../kotlin/obsidian/server/io/ws/StatsTask.kt | 152 +++--- .../obsidian/server/io/ws/WebSocketHandler.kt | 454 +++++++++--------- .../server/player/FrameLossTracker.kt | 186 +++---- .../obsidian/server/player/ObsidianAPM.kt | 216 ++++----- .../kotlin/obsidian/server/player/Player.kt | 292 +++++------ .../obsidian/server/player/PlayerUpdates.kt | 117 +++-- .../server/player/TrackEndMarkerHandler.kt | 8 +- .../obsidian/server/player/filter/Filter.kt | 54 +-- .../obsidian/server/player/filter/Filters.kt | 114 ++--- .../player/filter/impl/ChannelMixFilter.kt | 28 +- .../player/filter/impl/DistortionFilter.kt | 40 +- .../player/filter/impl/EqualizerFilter.kt | 32 +- .../player/filter/impl/KaraokeFilter.kt | 32 +- .../player/filter/impl/LowPassFilter.kt | 10 +- .../player/filter/impl/RotationFilter.kt | 11 +- .../player/filter/impl/TimescaleFilter.kt | 114 ++--- .../player/filter/impl/TremoloFilter.kt | 30 +- .../player/filter/impl/VibratoFilter.kt | 36 +- .../server/player/filter/impl/VolumeFilter.kt | 16 +- .../server/util/AuthorizationPipeline.kt | 44 +- .../obsidian/server/util/ByteRingBuffer.kt | 200 ++++---- .../server/util/CoroutineAudioEventAdapter.kt | 58 +-- .../kotlin/obsidian/server/util/CpuTimer.kt | 136 +++--- .../kotlin/obsidian/server/util/Interval.kt | 100 ++-- .../kotlin/obsidian/server/util/KoeUtil.kt | 113 ++--- .../server/util/LogbackColorConverter.kt | 64 +-- .../kotlin/obsidian/server/util/NativeUtil.kt | 176 +++---- .../obsidian/server/util/ThreadFactory.kt | 14 +- .../kotlin/obsidian/server/util/TrackUtil.kt | 74 +-- .../obsidian/server/util/VersionInfo.kt | 24 +- .../server/util/kxs/AudioTrackSerializer.kt | 16 +- .../server/util/search/AudioLoader.kt | 91 ++-- .../obsidian/server/util/search/LoadResult.kt | 60 +-- .../obsidian/server/util/search/LoadType.kt | 10 +- Server/src/main/resources/logback.xml | 6 +- Server/src/main/resources/version.txt | 2 +- Server/src/test/resources/routePlanner.http | 2 +- Server/src/test/resources/tracks.http | 12 +- build.gradle.kts | 26 +- docs/README.md | 3 +- docs/ws-rest.md | 8 +- obsidian.yml | 54 +-- settings.gradle.kts | 46 +- 66 files changed, 3144 insertions(+), 3128 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2a119ed..6e92715 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,9 +1,6 @@ --- -name: Bug report -about: Create a report to help us improve -title: '' -labels: 'type: bug, s: help wanted' -assignees: '' +name: Bug report about: Create a report to help us improve title: '' +labels: 'type: bug, s: help wanted' assignees: '' --- diff --git a/API.md b/API.md index ed7a982..896e9e3 100644 --- a/API.md +++ b/API.md @@ -47,7 +47,8 @@ Authorization: } ``` -*If no route planner was configured, both `class` and `details` will be null, the responses vary depending on what route planner was configured. Fields that are consistent:* +*If no route planner was configured, both `class` and `details` will be null, the responses vary depending on what route +planner was configured. Fields that are consistent:* - `class` *string* name of the route planner - `details.ip_block` *string* the current ip-block @@ -65,7 +66,8 @@ Authorization: **RotatingNanoIpRoutePlanner** -- `details.block_index` *string* containing the file information in which /64 block ips are chosen, this number increases on each ban. +- `details.block_index` *string* containing the file information in which /64 block ips are chosen, this number + increases on each ban. - `details.current_address_index` *long* representing the current offset in the ip-block. #### Unmark a failed address @@ -109,7 +111,7 @@ Each request must have a `User-Id` header or query parameter containing your bot | endpoint | description | | :------- | :------------------------- | | / | returns info on the player | -| /play | plays +| /play | plays | | | @@ -305,7 +307,6 @@ Stats on the node } ``` - #### Player Events List of current player events Example: @@ -381,7 +382,8 @@ dispatched when track playback is stuck } ``` -- [**Lavaplayer**](https://github.com/sedmelluq/lavaplayer/blob/bec39953a037b318663fad76873fbab9ce13c033/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStuckEvent.java) +- [** + Lavaplayer**](https://github.com/sedmelluq/lavaplayer/blob/bec39953a037b318663fad76873fbab9ce13c033/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStuckEvent.java) ##### `TRACK_EXCEPTION` diff --git a/README.md b/README.md index 637c29a..b0425b8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## Usage. -For obsidian to work correctly you must use **Java 11** or above. +For obsidian to work correctly you must use **Java 11** or above. - Goto the [releases page](/releases). - Download the **Latest Jar File** diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 4785986..abef902 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -3,10 +3,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.ByteArrayOutputStream plugins { - application - id("com.github.johnrengelman.shadow") version "7.0.0" - kotlin("jvm") version "1.5.10" - kotlin("plugin.serialization") version "1.5.10" + application + id("com.github.johnrengelman.shadow") version "7.0.0" + kotlin("jvm") version "1.5.10" + kotlin("plugin.serialization") version "1.5.10" } apply(plugin = "kotlin") @@ -15,80 +15,80 @@ description = "A robust and performant audio sending node meant for Discord Bots version = "2.0.0" application { - mainClass.set("obsidian.server.Application") + mainClass.set("obsidian.server.Application") } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.20") // standard library - implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.20") // reflection - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") // core coroutine library - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") // json serialization - - val ktorVersion = "1.6.1" - implementation("io.ktor:ktor-server-core:$ktorVersion") // ktor server core - implementation("io.ktor:ktor-server-cio:$ktorVersion") // ktor cio engine - implementation("io.ktor:ktor-locations:$ktorVersion") // ktor locations - implementation("io.ktor:ktor-serialization:$ktorVersion") // ktor serialization - implementation("io.ktor:ktor-websockets:$ktorVersion") // ktor websockets - - implementation("moe.kyokobot.koe:core:master-SNAPSHOT") { // discord send system - exclude(group = "org.slf4j", module = "slf4j-api") - } - - implementation("com.sedmelluq:lavaplayer:1.3.78") { // yes - exclude(group = "com.sedmelluq", module = "lavaplayer-natives") - } - - implementation("com.sedmelluq:lavaplayer-ext-youtube-rotator:0.2.3") { // ip rotation - exclude(group = "com.sedmelluq", module = "lavaplayer") - } - - implementation("com.github.natanbc:lavadsp:0.7.7") // audio filters - implementation("com.github.natanbc:native-loader:0.7.2") // native loader - implementation("com.github.natanbc:lp-cross:0.1.3-1") // lp-cross natives - - implementation("ch.qos.logback:logback-classic:1.2.3") // slf4j logging backend - - val konfVersion = "1.1.2" - implementation("com.github.uchuhimo.konf:konf-core:$konfVersion") // konf core shit - implementation("com.github.uchuhimo.konf:konf-yaml:$konfVersion") // yaml source + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.20") // standard library + implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.20") // reflection + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") // core coroutine library + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") // json serialization + + val ktorVersion = "1.6.1" + implementation("io.ktor:ktor-server-core:$ktorVersion") // ktor server core + implementation("io.ktor:ktor-server-cio:$ktorVersion") // ktor cio engine + implementation("io.ktor:ktor-locations:$ktorVersion") // ktor locations + implementation("io.ktor:ktor-serialization:$ktorVersion") // ktor serialization + implementation("io.ktor:ktor-websockets:$ktorVersion") // ktor websockets + + implementation("moe.kyokobot.koe:core:master-SNAPSHOT") { // discord send system + exclude(group = "org.slf4j", module = "slf4j-api") + } + + implementation("com.sedmelluq:lavaplayer:1.3.78") { // yes + exclude(group = "com.sedmelluq", module = "lavaplayer-natives") + } + + implementation("com.sedmelluq:lavaplayer-ext-youtube-rotator:0.2.3") { // ip rotation + exclude(group = "com.sedmelluq", module = "lavaplayer") + } + + implementation("com.github.natanbc:lavadsp:0.7.7") // audio filters + implementation("com.github.natanbc:native-loader:0.7.2") // native loader + implementation("com.github.natanbc:lp-cross:0.1.3-1") // lp-cross natives + + implementation("ch.qos.logback:logback-classic:1.2.3") // slf4j logging backend + + val konfVersion = "1.1.2" + implementation("com.github.uchuhimo.konf:konf-core:$konfVersion") // konf core shit + implementation("com.github.uchuhimo.konf:konf-yaml:$konfVersion") // yaml source } tasks.withType { - archiveBaseName.set("Obsidian") - archiveClassifier.set("") + archiveBaseName.set("Obsidian") + archiveClassifier.set("") } tasks.withType { - kotlinOptions { - jvmTarget = "13" - incremental = true - freeCompilerArgs = listOf( - "-Xopt-in=kotlin.ExperimentalStdlibApi", - "-Xopt-in=kotlin.RequiresOptIn", - "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", - "-Xopt-in=io.ktor.locations.KtorExperimentalLocationsAPI", - "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi" - ) - } + kotlinOptions { + jvmTarget = "13" + incremental = true + freeCompilerArgs = listOf( + "-Xopt-in=kotlin.ExperimentalStdlibApi", + "-Xopt-in=kotlin.RequiresOptIn", + "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-Xopt-in=io.ktor.locations.KtorExperimentalLocationsAPI", + "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi" + ) + } } /* version info task */ fun getVersionInfo(): String { - val gitVersion = ByteArrayOutputStream() - exec { - commandLine("git", "rev-parse", "--short", "HEAD") - standardOutput = gitVersion - } + val gitVersion = ByteArrayOutputStream() + exec { + commandLine("git", "rev-parse", "--short", "HEAD") + standardOutput = gitVersion + } - return "$version\n${gitVersion.toString().trim()}" + return "$version\n${gitVersion.toString().trim()}" } tasks.create("writeVersion") { - val resourcePath = sourceSets["main"].resources.srcDirs.first() - if (!file(resourcePath).exists()) { - resourcePath.mkdirs() - } + val resourcePath = sourceSets["main"].resources.srcDirs.first() + if (!file(resourcePath).exists()) { + resourcePath.mkdirs() + } - file("$resourcePath/version.txt").writeText(getVersionInfo()) + file("$resourcePath/version.txt").writeText(getVersionInfo()) } diff --git a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.java b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.java index 392964d..f1fd70c 100644 --- a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.java +++ b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.java @@ -25,97 +25,97 @@ * Manages sending out queues of UDP packets at a fixed interval. */ public class UdpQueueManager extends NativeResourceHolder { - private final int bufferCapacity; - private final ByteBuffer packetBuffer; - private final UdpQueueManagerLibrary library; - private final long instance; - private boolean released; + private final int bufferCapacity; + private final ByteBuffer packetBuffer; + private final UdpQueueManagerLibrary library; + private final long instance; + private boolean released; - /** - * @param bufferCapacity Maximum number of packets in one queue - * @param packetInterval Time interval between packets in a queue - * @param maximumPacketSize Maximum packet size - */ - public UdpQueueManager(int bufferCapacity, long packetInterval, int maximumPacketSize) { - this.bufferCapacity = bufferCapacity; - packetBuffer = ByteBuffer.allocateDirect(maximumPacketSize); - library = UdpQueueManagerLibrary.getInstance(); - instance = library.create(bufferCapacity, packetInterval); - } + /** + * @param bufferCapacity Maximum number of packets in one queue + * @param packetInterval Time interval between packets in a queue + * @param maximumPacketSize Maximum packet size + */ + public UdpQueueManager(int bufferCapacity, long packetInterval, int maximumPacketSize) { + this.bufferCapacity = bufferCapacity; + packetBuffer = ByteBuffer.allocateDirect(maximumPacketSize); + library = UdpQueueManagerLibrary.getInstance(); + instance = library.create(bufferCapacity, packetInterval); + } - /** - * If the queue does not exist yet, returns the maximum number of packets in a queue. - * - * @param key Unique queue identifier - * @return Number of empty packet slots in the specified queue - */ - public int getRemainingCapacity(long key) { - synchronized (library) { - if (released) { - return 0; - } + /** + * If the queue does not exist yet, returns the maximum number of packets in a queue. + * + * @param key Unique queue identifier + * @return Number of empty packet slots in the specified queue + */ + public int getRemainingCapacity(long key) { + synchronized (library) { + if (released) { + return 0; + } - return library.getRemainingCapacity(instance, key); + return library.getRemainingCapacity(instance, key); + } } - } - /** - * @return Total capacity used for queues in this manager. - */ - public int getCapacity() { - return bufferCapacity; - } + /** + * @return Total capacity used for queues in this manager. + */ + public int getCapacity() { + return bufferCapacity; + } - /** - * Adds one packet to the specified queue. Will fail if the maximum size of the queue is reached. There is no need to - * manually create a queue, it is automatically created when the first packet is added to it and deleted when it - * becomes empty. - * - * @param key Unique queue identifier - * @param packet Packet to add to the queue - * @return True if adding the packet to the queue succeeded - */ - public boolean queuePacket(long key, ByteBuffer packet, InetSocketAddress address) { - synchronized (library) { - if (released) { - return false; - } + /** + * Adds one packet to the specified queue. Will fail if the maximum size of the queue is reached. There is no need to + * manually create a queue, it is automatically created when the first packet is added to it and deleted when it + * becomes empty. + * + * @param key Unique queue identifier + * @param packet Packet to add to the queue + * @return True if adding the packet to the queue succeeded + */ + public boolean queuePacket(long key, ByteBuffer packet, InetSocketAddress address) { + synchronized (library) { + if (released) { + return false; + } - int length = packet.remaining(); - packetBuffer.clear(); - packetBuffer.put(packet); + int length = packet.remaining(); + packetBuffer.clear(); + packetBuffer.put(packet); - int port = address.getPort(); - String hostAddress = address.getAddress().getHostAddress(); - return library.queuePacket(instance, key, hostAddress, port, packetBuffer, length); + int port = address.getPort(); + String hostAddress = address.getAddress().getHostAddress(); + return library.queuePacket(instance, key, hostAddress, port, packetBuffer, length); + } } - } - /** - * This is the method that should be called to start processing the queues. It will use the current thread and return - * only when close() method is called on the queue manager. - */ - public void process() { - library.process(instance); - } + /** + * This is the method that should be called to start processing the queues. It will use the current thread and return + * only when close() method is called on the queue manager. + */ + public void process() { + library.process(instance); + } - @Override - protected void freeResources() { - synchronized (library) { - released = true; - library.destroy(instance); + @Override + protected void freeResources() { + synchronized (library) { + released = true; + library.destroy(instance); + } } - } - /** - * Simulate a GC pause stop-the-world by starting a heap iteration via JVMTI. The behaviour of this stop-the-world is - * identical to that of an actual GC pause, so nothing in Java can execute during the pause. - * - * @param length Length of the pause in milliseconds - */ - public static void pauseDemo(int length) { - UdpQueueManagerLibrary.getInstance(); - UdpQueueManagerLibrary.pauseDemo(length); - } + /** + * Simulate a GC pause stop-the-world by starting a heap iteration via JVMTI. The behaviour of this stop-the-world is + * identical to that of an actual GC pause, so nothing in Java can execute during the pause. + * + * @param length Length of the pause in milliseconds + */ + public static void pauseDemo(int length) { + UdpQueueManagerLibrary.getInstance(); + UdpQueueManagerLibrary.pauseDemo(length); + } } diff --git a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.java b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.java index 1bc5822..6c5f12d 100644 --- a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.java +++ b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.java @@ -22,35 +22,35 @@ import java.nio.ByteBuffer; public class UdpQueueManagerLibrary { - private static final NativeLibraryLoader nativeLoader = - NativeLibraryLoader.create(UdpQueueManagerLibrary.class, "udpqueue"); + private static final NativeLibraryLoader nativeLoader = + NativeLibraryLoader.create(UdpQueueManagerLibrary.class, "udpqueue"); - private UdpQueueManagerLibrary() { + private UdpQueueManagerLibrary() { - } + } - public static UdpQueueManagerLibrary getInstance() { - nativeLoader.load(); - return new UdpQueueManagerLibrary(); - } + public static UdpQueueManagerLibrary getInstance() { + nativeLoader.load(); + return new UdpQueueManagerLibrary(); + } - public native long create(int bufferCapacity, long packetInterval); + public native long create(int bufferCapacity, long packetInterval); - public native void destroy(long instance); + public native void destroy(long instance); - public native int getRemainingCapacity(long instance, long key); + public native int getRemainingCapacity(long instance, long key); - public native boolean queuePacket(long instance, long key, String address, int port, ByteBuffer dataDirectBuffer, - int dataLength); + public native boolean queuePacket(long instance, long key, String address, int port, ByteBuffer dataDirectBuffer, + int dataLength); - public native boolean queuePacketWithSocket(long instance, long key, String address, int port, - ByteBuffer dataDirectBuffer, int dataLength, long explicitSocket); + public native boolean queuePacketWithSocket(long instance, long key, String address, int port, + ByteBuffer dataDirectBuffer, int dataLength, long explicitSocket); - public native boolean deleteQueue(long instance, long key); + public native boolean deleteQueue(long instance, long key); - public native void process(long instance); + public native void process(long instance); - public native void processWithSocket(long instance, long ipv4Handle, long ipv6Handle); + public native void processWithSocket(long instance, long ipv4Handle, long ipv6Handle); - public static native void pauseDemo(int length); -} \ No newline at end of file + public static native void pauseDemo(int length); +} diff --git a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt index f0d454d..ab7b924 100644 --- a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt +++ b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/QueueManagerPool.kt @@ -27,50 +27,50 @@ import java.util.concurrent.atomic.AtomicLong class QueueManagerPool(val size: Int, val bufferDuration: Int) { - private var closed: Boolean = false + private var closed: Boolean = false - private val threadFactory = - threadFactory("QueueManagerPool %d", priority = (Thread.NORM_PRIORITY + Thread.MAX_PRIORITY) / 2, daemon = true) + private val threadFactory = + threadFactory("QueueManagerPool %d", priority = (Thread.NORM_PRIORITY + Thread.MAX_PRIORITY) / 2, daemon = true) - private val queueKeySeq: AtomicLong = - AtomicLong() + private val queueKeySeq: AtomicLong = + AtomicLong() - private val managers: List = - List(size) { - val queueManager = UdpQueueManager( - bufferDuration / FRAME_DURATION, - TimeUnit.MILLISECONDS.toNanos(FRAME_DURATION.toLong()), - MAXIMUM_PACKET_SIZE - ) + private val managers: List = + List(size) { + val queueManager = UdpQueueManager( + bufferDuration / FRAME_DURATION, + TimeUnit.MILLISECONDS.toNanos(FRAME_DURATION.toLong()), + MAXIMUM_PACKET_SIZE + ) - threadFactory.newThread(queueManager::process).start() - queueManager - } + threadFactory.newThread(queueManager::process).start() + queueManager + } - fun close() { - if (closed) { - return - } + fun close() { + if (closed) { + return + } - closed = true - managers.forEach(UdpQueueManager::close) - } + closed = true + managers.forEach(UdpQueueManager::close) + } - fun getNextWrapper(): UdpQueueWrapper { - val queueKey = queueKeySeq.getAndIncrement() - return getWrapperForKey(queueKey) - } + fun getNextWrapper(): UdpQueueWrapper { + val queueKey = queueKeySeq.getAndIncrement() + return getWrapperForKey(queueKey) + } - fun getWrapperForKey(queueKey: Long): UdpQueueWrapper { - val manager = managers[(queueKey % managers.size.toLong()).toInt()] - return UdpQueueWrapper(queueKey, manager) - } + fun getWrapperForKey(queueKey: Long): UdpQueueWrapper { + val manager = managers[(queueKey % managers.size.toLong()).toInt()] + return UdpQueueWrapper(queueKey, manager) + } - class UdpQueueWrapper(val queueKey: Long, val manager: UdpQueueManager) { - val remainingCapacity: Int - get() = manager.getRemainingCapacity(queueKey) + class UdpQueueWrapper(val queueKey: Long, val manager: UdpQueueManager) { + val remainingCapacity: Int + get() = manager.getRemainingCapacity(queueKey) - fun queuePacket(packet: ByteBuffer, addr: InetSocketAddress) = - this.manager.queuePacket(queueKey, packet, addr) - } + fun queuePacket(packet: ByteBuffer, addr: InetSocketAddress) = + this.manager.queuePacket(queueKey, packet, addr) + } } diff --git a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt index f6ad353..6639026 100644 --- a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt +++ b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt @@ -25,75 +25,75 @@ import java.net.InetSocketAddress import java.util.concurrent.TimeUnit class UdpQueueFramePoller(connection: MediaConnection, private val manager: QueueManagerPool.UdpQueueWrapper) : - AbstractFramePoller(connection) { + AbstractFramePoller(connection) { - private var lastFrame: Long = 0 - private val timestamp: IntReference = IntReference() + private var lastFrame: Long = 0 + private val timestamp: IntReference = IntReference() - override fun start() { - check(!polling) { - "Polling has already started." + override fun start() { + check(!polling) { + "Polling has already started." + } + + polling = true + lastFrame = System.currentTimeMillis() + eventLoop.execute(::populateQueue) } - polling = true - lastFrame = System.currentTimeMillis() - eventLoop.execute(::populateQueue) - } + override fun stop() { + if (!polling) { + return + } - override fun stop() { - if (!polling) { - return + polling = false } - polling = false - } + private fun populateQueue() { + if (!polling) { + return + } - private fun populateQueue() { - if (!polling) { - return - } + val remaining = manager.remainingCapacity + val handler = connection.connectionHandler as DiscordUDPConnection + val sender = connection.audioSender - val remaining = manager.remainingCapacity - val handler = connection.connectionHandler as DiscordUDPConnection - val sender = connection.audioSender + for (i in 0 until remaining) { + if (sender != null && sender.canSendFrame(OpusCodec.INSTANCE)) { + val buf = allocator.buffer() - for (i in 0 until remaining) { - if (sender != null && sender.canSendFrame(OpusCodec.INSTANCE)) { - val buf = allocator.buffer() + /* retrieve a frame so we can compare */ + val start = buf.writerIndex() + sender.retrieve(OpusCodec.INSTANCE, buf, timestamp) - /* retrieve a frame so we can compare */ - val start = buf.writerIndex() - sender.retrieve(OpusCodec.INSTANCE, buf, timestamp) + /* create a packet */ + val packet = + handler.createPacket(OpusCodec.PAYLOAD_TYPE, timestamp.get(), buf, buf.writerIndex() - start, false) - /* create a packet */ - val packet = - handler.createPacket(OpusCodec.PAYLOAD_TYPE, timestamp.get(), buf, buf.writerIndex() - start, false) + if (packet != null) { + manager.queuePacket(packet.nioBuffer(), handler.serverAddress as InetSocketAddress) + packet.release() + } - if (packet != null) { - manager.queuePacket(packet.nioBuffer(), handler.serverAddress as InetSocketAddress) - packet.release() + buf.release() + } } - buf.release() - } + val frameDelay = 40 - (System.currentTimeMillis() - lastFrame) + if (frameDelay > 0) { + eventLoop.schedule(::loop, frameDelay, TimeUnit.MILLISECONDS) + } else { + loop() + } } - val frameDelay = 40 - (System.currentTimeMillis() - lastFrame) - if (frameDelay > 0) { - eventLoop.schedule(::loop, frameDelay, TimeUnit.MILLISECONDS) - } else { - loop() - } - } + private fun loop() { + if (System.currentTimeMillis() < lastFrame + 60) { + lastFrame += 40 + } else { + lastFrame = System.currentTimeMillis() + } - private fun loop() { - if (System.currentTimeMillis() < lastFrame + 60) { - lastFrame += 40 - } else { - lastFrame = System.currentTimeMillis() + populateQueue() } - populateQueue() - } - } diff --git a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePollerFactory.kt b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePollerFactory.kt index 021a020..4a30357 100644 --- a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePollerFactory.kt +++ b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePollerFactory.kt @@ -23,21 +23,21 @@ import moe.kyokobot.koe.codec.FramePollerFactory import moe.kyokobot.koe.codec.OpusCodec class UdpQueueFramePollerFactory( - bufferDuration: Int = DEFAULT_BUFFER_DURATION, - poolSize: Int = Runtime.getRuntime().availableProcessors() + bufferDuration: Int = DEFAULT_BUFFER_DURATION, + poolSize: Int = Runtime.getRuntime().availableProcessors() ) : FramePollerFactory { - private val pool = QueueManagerPool(poolSize, bufferDuration) + private val pool = QueueManagerPool(poolSize, bufferDuration) - override fun createFramePoller(codec: Codec, connection: MediaConnection): FramePoller? { - if (codec !is OpusCodec) { - return null - } + override fun createFramePoller(codec: Codec, connection: MediaConnection): FramePoller? { + if (codec !is OpusCodec) { + return null + } - return UdpQueueFramePoller(connection, pool.getNextWrapper()) - } + return UdpQueueFramePoller(connection, pool.getNextWrapper()) + } - companion object { - const val MAXIMUM_PACKET_SIZE = 4096 - const val DEFAULT_BUFFER_DURATION = 400 - } + companion object { + const val MAXIMUM_PACKET_SIZE = 4096 + const val DEFAULT_BUFFER_DURATION = 400 + } } diff --git a/Server/src/main/kotlin/obsidian/server/Application.kt b/Server/src/main/kotlin/obsidian/server/Application.kt index 6c02074..a542a89 100644 --- a/Server/src/main/kotlin/obsidian/server/Application.kt +++ b/Server/src/main/kotlin/obsidian/server/Application.kt @@ -52,144 +52,144 @@ import org.slf4j.LoggerFactory import kotlin.system.exitProcess object Application { - /** - * Configuration instance. - * - * @see Obsidian - */ - val config = Config { addSpec(Obsidian); addSpec(Logging) } - .from.yaml.file("obsidian.yml", optional = true) - .from.env() - - /** - * Custom player manager instance. - */ - val players = ObsidianAPM() - - /** - * Logger - */ - val log: org.slf4j.Logger = LoggerFactory.getLogger(Application::class.java) - - /** - * Json parser used by ktor and us. - */ - val json = Json { - isLenient = true - encodeDefaults = true - ignoreUnknownKeys = true - } - - @JvmStatic - fun main(args: Array) = runBlocking { - - /* setup logging */ - configureLogging() - - /* native library loading lololol */ - try { - val type = SystemType.detect(SystemNativeLibraryProperties(null, "nativeloader.")) - - log.info("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") - log.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}") - } catch (e: Exception) { - val message = - "Unable to load system info" + if (e is UnsatisfiedLinkError || e is RuntimeException && e.cause is UnsatisfiedLinkError) - ", this isn't an error" else "." - - log.warn(message, e) + /** + * Configuration instance. + * + * @see Obsidian + */ + val config = Config { addSpec(Obsidian); addSpec(Logging) } + .from.yaml.file("obsidian.yml", optional = true) + .from.env() + + /** + * Custom player manager instance. + */ + val players = ObsidianAPM() + + /** + * Logger + */ + val log: org.slf4j.Logger = LoggerFactory.getLogger(Application::class.java) + + /** + * Json parser used by ktor and us. + */ + val json = Json { + isLenient = true + encodeDefaults = true + ignoreUnknownKeys = true } - try { - log.info("Loading Native Libraries") - NativeUtil.timescaleAvailable = true - NativeUtil.load() - } catch (ex: Exception) { - log.error("Fatal exception while loading native libraries.", ex) - exitProcess(1) - } - - val server = - embeddedServer(CIO, host = config[Obsidian.Server.host], port = config[Obsidian.Server.port]) { - install(WebSockets) - install(Locations) + @JvmStatic + fun main(args: Array) = runBlocking { - /* use the custom authentication provider */ - install(Authentication) { - obsidianProvider() - } + /* setup logging */ + configureLogging() - /* install status pages. */ - install(StatusPages) { - exception { exc -> - val error = ExceptionResponse.Error( - className = exc::class.simpleName ?: "Throwable", - message = exc.message, - cause = exc.cause?.let { - ExceptionResponse.Error( - it.message, - className = it::class.simpleName ?: "Throwable" - ) - } - ) - - val message = ExceptionResponse(error, exc.stackTraceToString()) - call.respond(InternalServerError, message) - } - } + /* native library loading lololol */ + try { + val type = SystemType.detect(SystemNativeLibraryProperties(null, "nativeloader.")) - /* append version headers. */ - install(DefaultHeaders) { - header("Obsidian-Version", VersionInfo.VERSION) - header("Obsidian-Version-Commit", VersionInfo.GIT_REVISION) - header(Server, "obsidian-magma/v${VersionInfo.VERSION}-${VersionInfo.GIT_REVISION}") - } + log.info("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") + log.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}") + } catch (e: Exception) { + val message = + "Unable to load system info" + if (e is UnsatisfiedLinkError || e is RuntimeException && e.cause is UnsatisfiedLinkError) + ", this isn't an error" else "." - /* use content negotiation for REST endpoints */ - install(ContentNegotiation) { - json(json) + log.warn(message, e) } - /* install routing */ - install(Routing) { - magma() + try { + log.info("Loading Native Libraries") + NativeUtil.timescaleAvailable = true + NativeUtil.load() + } catch (ex: Exception) { + log.error("Fatal exception while loading native libraries.", ex) + exitProcess(1) } - } - server.start(wait = true) - shutdown() - } + val server = + embeddedServer(CIO, host = config[Obsidian.Server.host], port = config[Obsidian.Server.port]) { + install(WebSockets) + install(Locations) + + /* use the custom authentication provider */ + install(Authentication) { + obsidianProvider() + } + + /* install status pages. */ + install(StatusPages) { + exception { exc -> + val error = ExceptionResponse.Error( + className = exc::class.simpleName ?: "Throwable", + message = exc.message, + cause = exc.cause?.let { + ExceptionResponse.Error( + it.message, + className = it::class.simpleName ?: "Throwable" + ) + } + ) + + val message = ExceptionResponse(error, exc.stackTraceToString()) + call.respond(InternalServerError, message) + } + } + + /* append version headers. */ + install(DefaultHeaders) { + header("Obsidian-Version", VersionInfo.VERSION) + header("Obsidian-Version-Commit", VersionInfo.GIT_REVISION) + header(Server, "obsidian-magma/v${VersionInfo.VERSION}-${VersionInfo.GIT_REVISION}") + } + + /* use content negotiation for REST endpoints */ + install(ContentNegotiation) { + json(json) + } + + /* install routing */ + install(Routing) { + magma() + } + } + + server.start(wait = true) + shutdown() + } - suspend fun shutdown() { - Magma.clients.forEach { (_, client) -> - client.shutdown(false) + suspend fun shutdown() { + Magma.clients.forEach { (_, client) -> + client.shutdown(false) + } } - } - /** - * Configures the root logger and obsidian level logger. - */ - private fun configureLogging() { - val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext + /** + * Configures the root logger and obsidian level logger. + */ + private fun configureLogging() { + val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext - val rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME) as Logger - rootLogger.level = Level.toLevel(config[Logging.Level.Root], Level.INFO) + val rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME) as Logger + rootLogger.level = Level.toLevel(config[Logging.Level.Root], Level.INFO) - val obsidianLogger = loggerContext.getLogger("obsidian") as Logger - obsidianLogger.level = Level.toLevel(config[Logging.Level.Obsidian], Level.INFO) - } + val obsidianLogger = loggerContext.getLogger("obsidian") as Logger + obsidianLogger.level = Level.toLevel(config[Logging.Level.Obsidian], Level.INFO) + } } @Serializable data class ExceptionResponse( - val error: Error, - @SerialName("stack_trace") val stackTrace: String, - val success: Boolean = false + val error: Error, + @SerialName("stack_trace") val stackTrace: String, + val success: Boolean = false ) { - @Serializable - data class Error( - val message: String?, - val cause: Error? = null, - @SerialName("class_name") val className: String - ) + @Serializable + data class Error( + val message: String?, + val cause: Error? = null, + @SerialName("class_name") val className: String + ) } diff --git a/Server/src/main/kotlin/obsidian/server/config/spec/Logging.kt b/Server/src/main/kotlin/obsidian/server/config/spec/Logging.kt index f3e038b..de42899 100644 --- a/Server/src/main/kotlin/obsidian/server/config/spec/Logging.kt +++ b/Server/src/main/kotlin/obsidian/server/config/spec/Logging.kt @@ -20,18 +20,18 @@ import com.uchuhimo.konf.ConfigSpec object Logging : ConfigSpec() { - // TODO: config for logging to files + // TODO: config for logging to files - object Level : ConfigSpec("level") { - /** - * Root logging level - */ - val Root by optional("INFO") + object Level : ConfigSpec("level") { + /** + * Root logging level + */ + val Root by optional("INFO") - /** - * Obsidian logging level - */ - val Obsidian by optional("INFO") - } + /** + * Obsidian logging level + */ + val Obsidian by optional("INFO") + } } diff --git a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt index 072aa9f..38ce373 100644 --- a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt +++ b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt @@ -21,170 +21,170 @@ import moe.kyokobot.koe.codec.udpqueue.UdpQueueFramePollerFactory.Companion.DEFA import obsidian.server.Application.config object Obsidian : ConfigSpec() { - /** - * Whether a client name is required. - */ - val requireClientName by optional(false, "require-client-name") - - /** - * The delay (in milliseconds) between each player update. - */ - val playerUpdateInterval by optional(5000L, "player-update-interval") - - /** - * Options related to the HTTP server. - */ - object Server : ConfigSpec() { /** - * The host the server will bind to. + * Whether a client name is required. */ - val host by optional("0.0.0.0") + val requireClientName by optional(false, "require-client-name") /** - * The port to listen for requests on. + * The delay (in milliseconds) between each player update. */ - val port by optional(3030) + val playerUpdateInterval by optional(5000L, "player-update-interval") /** - * The authentication for HTTP endpoints and the WebSocket server. + * Options related to the HTTP server. */ - val auth by optional("") - - /** - * Used to validate a string given as authorization. - * - * @param given The given authorization string. - * - * @return true, if the given authorization matches the configured password. - */ - fun validateAuth(given: String?): Boolean = when { - config[auth].isEmpty() -> true - else -> given == config[auth] + object Server : ConfigSpec() { + /** + * The host the server will bind to. + */ + val host by optional("0.0.0.0") + + /** + * The port to listen for requests on. + */ + val port by optional(3030) + + /** + * The authentication for HTTP endpoints and the WebSocket server. + */ + val auth by optional("") + + /** + * Used to validate a string given as authorization. + * + * @param given The given authorization string. + * + * @return true, if the given authorization matches the configured password. + */ + fun validateAuth(given: String?): Boolean = when { + config[auth].isEmpty() -> true + else -> given == config[auth] + } } - } - - /** - * Options related to Koe, the discord media library used by Obsidian. - */ - object Koe : ConfigSpec("koe") { - /** - * The byte-buf allocator to use - */ - val byteAllocator by optional("pooled", "byte-allocator") /** - * Whether packets should be prioritized + * Options related to Koe, the discord media library used by Obsidian. */ - val highPacketPriority by optional(true, "high-packet-priority") - - /** - * The voice server version to use, defaults to v5 - */ - val gatewayVersion by optional(5, "gateway-version") - - object UdpQueue : ConfigSpec("udp-queue") { - /** - * Whether udp-queue is enabled. - */ - val enabled by optional(true) - - /** - * The buffer duration, in milliseconds. - */ - val bufferDuration by optional(DEFAULT_BUFFER_DURATION, "buffer-duration") - - /** - * The number of threads to create, defaults to twice the amount of processors. - */ - val poolSize by optional(Runtime.getRuntime().availableProcessors() * 2, "pool-size") + object Koe : ConfigSpec("koe") { + /** + * The byte-buf allocator to use + */ + val byteAllocator by optional("pooled", "byte-allocator") + + /** + * Whether packets should be prioritized + */ + val highPacketPriority by optional(true, "high-packet-priority") + + /** + * The voice server version to use, defaults to v5 + */ + val gatewayVersion by optional(5, "gateway-version") + + object UdpQueue : ConfigSpec("udp-queue") { + /** + * Whether udp-queue is enabled. + */ + val enabled by optional(true) + + /** + * The buffer duration, in milliseconds. + */ + val bufferDuration by optional(DEFAULT_BUFFER_DURATION, "buffer-duration") + + /** + * The number of threads to create, defaults to twice the amount of processors. + */ + val poolSize by optional(Runtime.getRuntime().availableProcessors() * 2, "pool-size") + } } - } - - /** - * Options related to lavaplayer, the library used for audio. - */ - object Lavaplayer : ConfigSpec("lavaplayer") { - /** - * Whether garbage collection should be monitored. - */ - val gcMonitoring by optional(false, "gc-monitoring") /** - * Whether lavaplayer shouldn't allocate audio frames + * Options related to lavaplayer, the library used for audio. */ - val nonAllocating by optional(false, "non-allocating") - - /** - * Names of sources that will be enabled. - */ - val enabledSources by optional( - setOf( - "youtube", - "yarn", - "bandcamp", - "twitch", - "vimeo", - "nico", - "soundcloud", - "local", - "http" - ), "enabled-sources" - ) - - /** - * Whether `scsearch:` should be allowed. - */ - val allowScSearch by optional(true, "allow-scsearch") - - object RateLimit : ConfigSpec("rate-limit") { - /** - * Ip blocks to use. - */ - val ipBlocks by optional(emptyList(), "ip-blocks") - - /** - * IPs which should be excluded from usage by the route planner - */ - val excludedIps by optional(emptyList(), "excluded-ips") - - /** - * The route planner strategy to use. - */ - val strategy by optional("rotate-on-ban") - - /** - * Whether a search 429 should trigger marking the ip as failing. - */ - val searchTriggersFail by optional(true, "search-triggers-fail") - - /** - * -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times - */ - val retryLimit by optional(-1, "retry-limit") - } - - object Nico : ConfigSpec("nico") { - /** - * The email to use for the Nico Source. - */ - val email by optional("") - - /** - * The password to use for the Nico Source. - */ - val password by optional("") - } - - object YouTube : ConfigSpec("youtube") { - /** - * Whether `ytsearch:` should be allowed. - */ - val allowSearch by optional(true, "allow-search") - - /** - * Total number of pages (100 tracks per page) to load - */ - val playlistPageLimit by optional(6, "playlist-page-limit") + object Lavaplayer : ConfigSpec("lavaplayer") { + /** + * Whether garbage collection should be monitored. + */ + val gcMonitoring by optional(false, "gc-monitoring") + + /** + * Whether lavaplayer shouldn't allocate audio frames + */ + val nonAllocating by optional(false, "non-allocating") + + /** + * Names of sources that will be enabled. + */ + val enabledSources by optional( + setOf( + "youtube", + "yarn", + "bandcamp", + "twitch", + "vimeo", + "nico", + "soundcloud", + "local", + "http" + ), "enabled-sources" + ) + + /** + * Whether `scsearch:` should be allowed. + */ + val allowScSearch by optional(true, "allow-scsearch") + + object RateLimit : ConfigSpec("rate-limit") { + /** + * Ip blocks to use. + */ + val ipBlocks by optional(emptyList(), "ip-blocks") + + /** + * IPs which should be excluded from usage by the route planner + */ + val excludedIps by optional(emptyList(), "excluded-ips") + + /** + * The route planner strategy to use. + */ + val strategy by optional("rotate-on-ban") + + /** + * Whether a search 429 should trigger marking the ip as failing. + */ + val searchTriggersFail by optional(true, "search-triggers-fail") + + /** + * -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times + */ + val retryLimit by optional(-1, "retry-limit") + } + + object Nico : ConfigSpec("nico") { + /** + * The email to use for the Nico Source. + */ + val email by optional("") + + /** + * The password to use for the Nico Source. + */ + val password by optional("") + } + + object YouTube : ConfigSpec("youtube") { + /** + * Whether `ytsearch:` should be allowed. + */ + val allowSearch by optional(true, "allow-search") + + /** + * Total number of pages (100 tracks per page) to load + */ + val playlistPageLimit by optional(6, "playlist-page-limit") + } } - } } diff --git a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt index fbb87c0..24edefd 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt @@ -26,75 +26,75 @@ import org.slf4j.LoggerFactory object Handlers { - private val log: Logger = LoggerFactory.getLogger(Handlers::class.java) - - fun submitVoiceServer(client: MagmaClient, guildId: Long, vsi: VoiceServerInfo) { - val connection = client.mediaConnectionFor(guildId) - connection.connect(vsi) - client.playerFor(guildId).provideTo(connection) - } - - fun seek(client: MagmaClient, guildId: Long, position: Long) { - val player = client.playerFor(guildId) - player.seekTo(position) - } - - suspend fun destroy(client: MagmaClient, guildId: Long) { - val player = client.players[guildId] - player?.destroy() - client.koe.destroyConnection(guildId) - } - - fun playTrack( - client: MagmaClient, - guildId: Long, - track: String, - startTime: Long?, - endTime: Long?, - noReplace: Boolean = false - ) { - val player = client.playerFor(guildId) - if (player.audioPlayer.playingTrack != null && noReplace) { - log.info("${client.displayName} - skipping PLAY_TRACK operation") - return + private val log: Logger = LoggerFactory.getLogger(Handlers::class.java) + + fun submitVoiceServer(client: MagmaClient, guildId: Long, vsi: VoiceServerInfo) { + val connection = client.mediaConnectionFor(guildId) + connection.connect(vsi) + client.playerFor(guildId).provideTo(connection) } - val track = TrackUtil.decode(track) + fun seek(client: MagmaClient, guildId: Long, position: Long) { + val player = client.playerFor(guildId) + player.seekTo(position) + } - /* handle start and end times */ - if (startTime != null && startTime in 0..track.duration) { - track.position = startTime + suspend fun destroy(client: MagmaClient, guildId: Long) { + val player = client.players[guildId] + player?.destroy() + client.koe.destroyConnection(guildId) } - if (endTime != null && endTime in 0..track.duration) { - val handler = TrackEndMarkerHandler(player) - val marker = TrackMarker(endTime, handler) - track.setMarker(marker) + fun playTrack( + client: MagmaClient, + guildId: Long, + track: String, + startTime: Long?, + endTime: Long?, + noReplace: Boolean = false + ) { + val player = client.playerFor(guildId) + if (player.audioPlayer.playingTrack != null && noReplace) { + log.info("${client.displayName} - skipping PLAY_TRACK operation") + return + } + + val track = TrackUtil.decode(track) + + /* handle start and end times */ + if (startTime != null && startTime in 0..track.duration) { + track.position = startTime + } + + if (endTime != null && endTime in 0..track.duration) { + val handler = TrackEndMarkerHandler(player) + val marker = TrackMarker(endTime, handler) + track.setMarker(marker) + } + + player.play(track) } - player.play(track) - } - - fun stopTrack(client: MagmaClient, guildId: Long) { - val player = client.playerFor(guildId) - player.audioPlayer.stopTrack() - } - - fun configure( - client: MagmaClient, - guildId: Long, - filters: Filters? = null, - pause: Boolean? = null, - sendPlayerUpdates: Boolean? = null - ) { - if (filters == null && pause == null && sendPlayerUpdates == null) { - return + fun stopTrack(client: MagmaClient, guildId: Long) { + val player = client.playerFor(guildId) + player.audioPlayer.stopTrack() } - val player = client.playerFor(guildId) - pause?.let { player.audioPlayer.isPaused = it } - filters?.let { player.filters = it } - sendPlayerUpdates?.let { player.updates.enabled = it } - } + fun configure( + client: MagmaClient, + guildId: Long, + filters: Filters? = null, + pause: Boolean? = null, + sendPlayerUpdates: Boolean? = null + ) { + if (filters == null && pause == null && sendPlayerUpdates == null) { + return + } + + val player = client.playerFor(guildId) + pause?.let { player.audioPlayer.isPaused = it } + filters?.let { player.filters = it } + sendPlayerUpdates?.let { player.updates.enabled = it } + } } diff --git a/Server/src/main/kotlin/obsidian/server/io/Magma.kt b/Server/src/main/kotlin/obsidian/server/io/Magma.kt index 670e0c8..38ca074 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Magma.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Magma.kt @@ -28,189 +28,194 @@ import io.ktor.util.* import io.ktor.websocket.* import kotlinx.coroutines.isActive import obsidian.server.Application.config +import obsidian.server.config.spec.Obsidian import obsidian.server.io.rest.Players.players +import obsidian.server.io.rest.Response +import obsidian.server.io.rest.planner +import obsidian.server.io.rest.respondAndFinish +import obsidian.server.io.rest.tracks import obsidian.server.io.ws.CloseReasons import obsidian.server.io.ws.StatsTask import obsidian.server.io.ws.WebSocketHandler -import obsidian.server.config.spec.Obsidian -import obsidian.server.io.rest.* import obsidian.server.util.threadFactory import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService -import kotlin.text.Typography.mdash object Magma { - val ClientName = AttributeKey("ClientName") - - /** - * All connected clients. - */ - val clients = ConcurrentHashMap() - - /** - * Executor used for cleaning up un-resumed sessions. - */ - val cleanupExecutor: ScheduledExecutorService = - Executors.newSingleThreadScheduledExecutor(threadFactory("Obsidian Magma-Cleanup")) - - private val log: Logger = LoggerFactory.getLogger(Magma::class.java) - - /** - * Adds REST endpoint routes and websocket route - */ - fun Routing.magma() { - /* rest endpoints */ - tracks() - planner() - players() - - authenticate { - get("/stats") { - val client = call.request.userId()?.let { clients[it] } - val stats = StatsTask.build(client) - call.respond(stats) - } - } - - intercept(ApplicationCallPipeline.Call) { - /* extract client name from the request */ - val clientName = call.request.clientName() + val ClientName = AttributeKey("ClientName") + + /** + * All connected clients. + */ + val clients = ConcurrentHashMap() + + /** + * Executor used for cleaning up un-resumed sessions. + */ + val cleanupExecutor: ScheduledExecutorService = + Executors.newSingleThreadScheduledExecutor(threadFactory("Obsidian Magma-Cleanup")) + + private val log: Logger = LoggerFactory.getLogger(Magma::class.java) + + /** + * Adds REST endpoint routes and websocket route + */ + fun Routing.magma() { + /* rest endpoints */ + tracks() + planner() + players() + + authenticate { + get("/stats") { + val client = call.request.userId()?.let { clients[it] } + val stats = StatsTask.build(client) + call.respond(stats) + } + } - /* log out the request */ - log.info(with(call.request) { - "${clientName ?: origin.remoteHost} ${Typography.ndash} ${httpMethod.value.padEnd(4, ' ')} $uri" - }) + intercept(ApplicationCallPipeline.Call) { + /* extract client name from the request */ + val clientName = call.request.clientName() + + /* log out the request */ + log.info(with(call.request) { + "${clientName ?: origin.remoteHost} ${Typography.ndash} ${httpMethod.value.padEnd(4, ' ')} $uri" + }) + + /* check if a client name is required, if so check if there was a provided client name */ + if (clientName == null && config[Obsidian.requireClientName]) { + return@intercept respondAndFinish( + HttpStatusCode.BadRequest, + Response("Missing 'Client-Name' header or query parameter.") + ) + } + + if (clientName != null) { + call.attributes.put(ClientName, clientName) + } + } - /* check if a client name is required, if so check if there was a provided client name */ - if (clientName == null && config[Obsidian.requireClientName]) { - return@intercept respondAndFinish(HttpStatusCode.BadRequest, Response("Missing 'Client-Name' header or query parameter.")) - } + /* websocket */ + webSocket("/magma") { + val request = call.request + + /* check if client names are required, if so check if one was supplied */ + val clientName = request.clientName() + if (config[Obsidian.requireClientName] && clientName.isNullOrBlank()) { + log.warn("${request.local.remoteHost} - missing 'Client-Name' header/query parameter.") + return@webSocket close(CloseReasons.MISSING_CLIENT_NAME) + } + + /* used within logs to easily identify different clients */ + val display = "${request.local.remoteHost}${if (!clientName.isNullOrEmpty()) "($clientName)" else ""}" + + /* validate authorization */ + val auth = request.authorization() + ?: request.queryParameters["auth"] + + if (!Obsidian.Server.validateAuth(auth)) { + log.warn("$display - authentication failed") + return@webSocket close(CloseReasons.INVALID_AUTHORIZATION) + } + + log.info("$display - incoming connection") + + /* check for user id */ + val userId = request.userId() + if (userId == null) { + /* no user-id was given, close the connection */ + log.info("$display - missing 'User-Id' header/query parameter") + return@webSocket close(CloseReasons.MISSING_USER_ID) + } + + val client = clients[userId] + ?: createClient(userId, clientName) + + val wsh = client.websocket + if (wsh != null) { + /* check for a resume key, if one was given check if the client has the same resume key/ */ + val resumeKey: String? = request.headers["Resume-Key"] + if (resumeKey != null && wsh.resumeKey == resumeKey) { + /* resume the client session */ + wsh.resume(this) + return@webSocket + } + + return@webSocket close(CloseReasons.DUPLICATE_SESSION) + } + + handleWebsocket(client, this) + } + } - if (clientName != null) { - call.attributes.put(ClientName, clientName) - } + fun getClient(userId: Long, clientName: String? = null): MagmaClient { + return clients[userId] ?: createClient(userId, clientName) } - /* websocket */ - webSocket("/magma") { - val request = call.request - - /* check if client names are required, if so check if one was supplied */ - val clientName = request.clientName() - if (config[Obsidian.requireClientName] && clientName.isNullOrBlank()) { - log.warn("${request.local.remoteHost} - missing 'Client-Name' header/query parameter.") - return@webSocket close(CloseReasons.MISSING_CLIENT_NAME) - } - - /* used within logs to easily identify different clients */ - val display = "${request.local.remoteHost}${if (!clientName.isNullOrEmpty()) "($clientName)" else ""}" - - /* validate authorization */ - val auth = request.authorization() - ?: request.queryParameters["auth"] - - if (!Obsidian.Server.validateAuth(auth)) { - log.warn("$display - authentication failed") - return@webSocket close(CloseReasons.INVALID_AUTHORIZATION) - } - - log.info("$display - incoming connection") - - /* check for user id */ - val userId = request.userId() - if (userId == null) { - /* no user-id was given, close the connection */ - log.info("$display - missing 'User-Id' header/query parameter") - return@webSocket close(CloseReasons.MISSING_USER_ID) - } - - val client = clients[userId] - ?: createClient(userId, clientName) - - val wsh = client.websocket - if (wsh != null) { - /* check for a resume key, if one was given check if the client has the same resume key/ */ - val resumeKey: String? = request.headers["Resume-Key"] - if (resumeKey != null && wsh.resumeKey == resumeKey) { - /* resume the client session */ - wsh.resume(this) - return@webSocket + /** + * Creates a [MagmaClient] for the supplied [userId] with an optional [clientName] + * + * @param userId + * @param clientName + */ + fun createClient(userId: Long, clientName: String? = null): MagmaClient { + return MagmaClient(userId).also { + it.name = clientName + clients[userId] = it } - - return@webSocket close(CloseReasons.DUPLICATE_SESSION) - } - - handleWebsocket(client, this) - } - } - - fun getClient(userId: Long, clientName: String? = null): MagmaClient { - return clients[userId] ?: createClient(userId, clientName) - } - - /** - * Creates a [MagmaClient] for the supplied [userId] with an optional [clientName] - * - * @param userId - * @param clientName - */ - fun createClient(userId: Long, clientName: String? = null): MagmaClient { - return MagmaClient(userId).also { - it.name = clientName - clients[userId] = it } - } - - /** - * Extracts the 'User-Id' header or 'user-id' query parameter from the provided [request] - * - * @param request - * [ApplicationRequest] to extract the user id from. - */ - private fun extractUserId(request: ApplicationRequest): Long? { - return request.headers["user-id"]?.toLongOrNull() - ?: request.queryParameters["user-id"]?.toLongOrNull() - } - - fun ApplicationRequest.userId(): Long? = - extractUserId(this) - - /** - * Extracts the 'Client-Name' header or 'client-name' query parameter from the provided [request] - * - * @param request - * [ApplicationRequest] to extract the client name from. - */ - private fun extractClientName(request: ApplicationRequest): String? { - return request.headers["Client-Name"] - ?: request.queryParameters["client-name"] - } - - fun ApplicationRequest.clientName(): String? = - extractClientName(this) - - /** - * Handles a [WebSocketServerSession] for the supplied [client] - */ - private suspend fun handleWebsocket(client: MagmaClient, wss: WebSocketServerSession) { - val wsh = WebSocketHandler(client, wss).also { - client.websocket = it + + /** + * Extracts the 'User-Id' header or 'user-id' query parameter from the provided [request] + * + * @param request + * [ApplicationRequest] to extract the user id from. + */ + private fun extractUserId(request: ApplicationRequest): Long? { + return request.headers["user-id"]?.toLongOrNull() + ?: request.queryParameters["user-id"]?.toLongOrNull() } - /* listen for incoming messages. */ - try { - wsh.listen() - } catch (ex: Exception) { - log.error("${client.displayName} - An error occurred while listening for frames.", ex) - if (wss.isActive) { - wss.close(CloseReason(4006, ex.message ?: ex.cause?.message ?: "unknown error")) - } + fun ApplicationRequest.userId(): Long? = + extractUserId(this) + + /** + * Extracts the 'Client-Name' header or 'client-name' query parameter from the provided [request] + * + * @param request + * [ApplicationRequest] to extract the client name from. + */ + private fun extractClientName(request: ApplicationRequest): String? { + return request.headers["Client-Name"] + ?: request.queryParameters["client-name"] } - wsh.handleClose() - } + fun ApplicationRequest.clientName(): String? = + extractClientName(this) + + /** + * Handles a [WebSocketServerSession] for the supplied [client] + */ + private suspend fun handleWebsocket(client: MagmaClient, wss: WebSocketServerSession) { + val wsh = WebSocketHandler(client, wss).also { + client.websocket = it + } + + /* listen for incoming messages. */ + try { + wsh.listen() + } catch (ex: Exception) { + log.error("${client.displayName} - An error occurred while listening for frames.", ex) + if (wss.isActive) { + wss.close(CloseReason(4006, ex.message ?: ex.cause?.message ?: "unknown error")) + } + } + + wsh.handleClose() + } } diff --git a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt index ed95346..04389d3 100644 --- a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt +++ b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt @@ -32,118 +32,118 @@ import java.util.concurrent.ConcurrentHashMap class MagmaClient(val userId: Long) { - /** - * The name of this client. - */ - var name: String? = null - - /** - * The websocket handler for this client, or null if one hasn't been initialized. - */ - var websocket: WebSocketHandler? = null - - /** - * The display name for this client. - */ - val displayName: String - get() = "${name ?: userId}" - - /** - * The koe client used to send audio frames. - */ - val koe: KoeClient by lazy { - KoeUtil.koe.newClient(userId) - } - - /** - * Current players - */ - val players: ConcurrentHashMap by lazy { - ConcurrentHashMap() - } - - /** - * Convenience method for ensuring that a player with the supplied guild id exists. - * - * @param guildId ID of the guild. - */ - fun playerFor(guildId: Long): Player { - return players.computeIfAbsent(guildId) { - Player(guildId, this) + /** + * The name of this client. + */ + var name: String? = null + + /** + * The websocket handler for this client, or null if one hasn't been initialized. + */ + var websocket: WebSocketHandler? = null + + /** + * The display name for this client. + */ + val displayName: String + get() = "${name ?: userId}" + + /** + * The koe client used to send audio frames. + */ + val koe: KoeClient by lazy { + KoeUtil.koe.newClient(userId) } - } - - /** - * Returns a [MediaConnection] for the supplied [guildId] - * - * @param guildId ID of the guild to get a media connection for. - */ - fun mediaConnectionFor(guildId: Long): MediaConnection { - var connection = koe.getConnection(guildId) - if (connection == null) { - connection = koe.createConnection(guildId) - connection.registerListener(EventAdapterImpl(connection)) + + /** + * Current players + */ + val players: ConcurrentHashMap by lazy { + ConcurrentHashMap() } - return connection - } - - /** - * Shutdown this magma client. - * - * @param safe - * Whether we should be cautious about shutting down. - */ - suspend fun shutdown(safe: Boolean = true) { - websocket?.shutdown() - websocket = null - - val activePlayers = players.count { (_, player) -> - player.audioPlayer.playingTrack != null + /** + * Convenience method for ensuring that a player with the supplied guild id exists. + * + * @param guildId ID of the guild. + */ + fun playerFor(guildId: Long): Player { + return players.computeIfAbsent(guildId) { + Player(guildId, this) + } } - if (safe && activePlayers != 0) { - return + /** + * Returns a [MediaConnection] for the supplied [guildId] + * + * @param guildId ID of the guild to get a media connection for. + */ + fun mediaConnectionFor(guildId: Long): MediaConnection { + var connection = koe.getConnection(guildId) + if (connection == null) { + connection = koe.createConnection(guildId) + connection.registerListener(EventAdapterImpl(connection)) + } + + return connection } - /* no players are active so it's safe to remove the client. */ + /** + * Shutdown this magma client. + * + * @param safe + * Whether we should be cautious about shutting down. + */ + suspend fun shutdown(safe: Boolean = true) { + websocket?.shutdown() + websocket = null - for ((id, player) in players) { - player.destroy() - players.remove(id) - } + val activePlayers = players.count { (_, player) -> + player.audioPlayer.playingTrack != null + } - koe.close() - } + if (safe && activePlayers != 0) { + return + } - inner class EventAdapterImpl(val connection: MediaConnection) : KoeEventAdapter() { - override fun gatewayReady(target: InetSocketAddress, ssrc: Int) { - websocket?.launch { - val event = WebSocketOpenEvent( - guildId = connection.guildId, - ssrc = ssrc, - target = target.toString(), - ) + /* no players are active so it's safe to remove the client. */ - websocket?.send(event) - } + for ((id, player) in players) { + player.destroy() + players.remove(id) + } + + koe.close() } - override fun gatewayClosed(code: Int, reason: String?, byRemote: Boolean) { - websocket?.launch { - val event = WebSocketClosedEvent( - guildId = connection.guildId, - code = code, - reason = reason, - byRemote = byRemote - ) - - websocket?.send(event) - } + inner class EventAdapterImpl(val connection: MediaConnection) : KoeEventAdapter() { + override fun gatewayReady(target: InetSocketAddress, ssrc: Int) { + websocket?.launch { + val event = WebSocketOpenEvent( + guildId = connection.guildId, + ssrc = ssrc, + target = target.toString(), + ) + + websocket?.send(event) + } + } + + override fun gatewayClosed(code: Int, reason: String?, byRemote: Boolean) { + websocket?.launch { + val event = WebSocketClosedEvent( + guildId = connection.guildId, + code = code, + reason = reason, + byRemote = byRemote + ) + + websocket?.send(event) + } + } } - } - companion object { - val log: Logger = LoggerFactory.getLogger(MagmaClient::class.java) - } + companion object { + val log: Logger = LoggerFactory.getLogger(MagmaClient::class.java) + } } diff --git a/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt b/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt index d0a04d2..758e530 100644 --- a/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt @@ -25,116 +25,116 @@ import kotlinx.serialization.Serializable import java.util.* object RoutePlannerUtil { - /** - * Detail information block for an AbstractRoutePlanner - */ - fun getDetailBlock(planner: AbstractRoutePlanner): RoutePlannerStatus.IRoutePlannerStatus { - val ipBlock = planner.ipBlock - val ipBlockStatus = IpBlockStatus(ipBlock.type.simpleName, ipBlock.size.toString()) - - val failingAddresses = planner.failingAddresses - val failingAddressesStatus = failingAddresses.entries.map { - FailingAddress(it.key, it.value, Date(it.value).toString()) + /** + * Detail information block for an AbstractRoutePlanner + */ + fun getDetailBlock(planner: AbstractRoutePlanner): RoutePlannerStatus.IRoutePlannerStatus { + val ipBlock = planner.ipBlock + val ipBlockStatus = IpBlockStatus(ipBlock.type.simpleName, ipBlock.size.toString()) + + val failingAddresses = planner.failingAddresses + val failingAddressesStatus = failingAddresses.entries.map { + FailingAddress(it.key, it.value, Date(it.value).toString()) + } + + return when (planner) { + is RotatingIpRoutePlanner -> RotatingIpRoutePlannerStatus( + ipBlockStatus, + failingAddressesStatus, + planner.rotateIndex.toString(), + planner.index.toString(), + planner.currentAddress.toString() + ) + + is NanoIpRoutePlanner -> NanoIpRoutePlannerStatus( + ipBlockStatus, + failingAddressesStatus, + planner.currentAddress.toString() + ) + + is RotatingNanoIpRoutePlanner -> RotatingNanoIpRoutePlannerStatus( + ipBlockStatus, + failingAddressesStatus, + planner.currentBlock.toString(), + planner.addressIndexInBlock.toString() + ) + + else -> GenericRoutePlannerStatus(ipBlockStatus, failingAddressesStatus) + } } - - return when (planner) { - is RotatingIpRoutePlanner -> RotatingIpRoutePlannerStatus( - ipBlockStatus, - failingAddressesStatus, - planner.rotateIndex.toString(), - planner.index.toString(), - planner.currentAddress.toString() - ) - - is NanoIpRoutePlanner -> NanoIpRoutePlannerStatus( - ipBlockStatus, - failingAddressesStatus, - planner.currentAddress.toString() - ) - - is RotatingNanoIpRoutePlanner -> RotatingNanoIpRoutePlannerStatus( - ipBlockStatus, - failingAddressesStatus, - planner.currentBlock.toString(), - planner.addressIndexInBlock.toString() - ) - - else -> GenericRoutePlannerStatus(ipBlockStatus, failingAddressesStatus) - } - } } data class RoutePlannerStatus( - val `class`: String?, - val details: IRoutePlannerStatus? + val `class`: String?, + val details: IRoutePlannerStatus? ) { - interface IRoutePlannerStatus + interface IRoutePlannerStatus } @Serializable data class GenericRoutePlannerStatus( - @SerialName("ip_block") - val ipBlock: IpBlockStatus, + @SerialName("ip_block") + val ipBlock: IpBlockStatus, - @SerialName("failing_addresses") - val failingAddresses: List + @SerialName("failing_addresses") + val failingAddresses: List ) : RoutePlannerStatus.IRoutePlannerStatus @Serializable data class RotatingIpRoutePlannerStatus( - @SerialName("ip_block") - val ipBlock: IpBlockStatus, + @SerialName("ip_block") + val ipBlock: IpBlockStatus, - @SerialName("failing_addresses") - val failingAddresses: List, + @SerialName("failing_addresses") + val failingAddresses: List, - @SerialName("rotate_index") - val rotateIndex: String, + @SerialName("rotate_index") + val rotateIndex: String, - @SerialName("ip_index") - val ipIndex: String, + @SerialName("ip_index") + val ipIndex: String, - @SerialName("current_address") - val currentAddress: String + @SerialName("current_address") + val currentAddress: String ) : RoutePlannerStatus.IRoutePlannerStatus @Serializable data class FailingAddress( - @SerialName("failing_address") - val failingAddress: String, + @SerialName("failing_address") + val failingAddress: String, - @SerialName("failing_timestamp") - val failingTimestamp: Long, + @SerialName("failing_timestamp") + val failingTimestamp: Long, - @SerialName("failing_time") - val failingTime: String + @SerialName("failing_time") + val failingTime: String ) : RoutePlannerStatus.IRoutePlannerStatus @Serializable data class NanoIpRoutePlannerStatus( - @SerialName("ip_block") - val ipBlock: IpBlockStatus, + @SerialName("ip_block") + val ipBlock: IpBlockStatus, - @SerialName("failing_addresses") - val failingAddresses: List, + @SerialName("failing_addresses") + val failingAddresses: List, - @SerialName("current_address_index") - val currentAddressIndex: String + @SerialName("current_address_index") + val currentAddressIndex: String ) : RoutePlannerStatus.IRoutePlannerStatus @Serializable data class RotatingNanoIpRoutePlannerStatus( - @SerialName("ip_block") - val ipBlock: IpBlockStatus, + @SerialName("ip_block") + val ipBlock: IpBlockStatus, - @SerialName("failing_addresses") - val failingAddresses: List, + @SerialName("failing_addresses") + val failingAddresses: List, - @SerialName("block_index") - val blockIndex: String, + @SerialName("block_index") + val blockIndex: String, - @SerialName("current_address_index") - val currentAddressIndex: String + @SerialName("current_address_index") + val currentAddressIndex: String ) : RoutePlannerStatus.IRoutePlannerStatus @Serializable diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt b/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt index 37c3d53..88ddfcb 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt @@ -21,56 +21,56 @@ import io.ktor.http.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* -import obsidian.server.Application import kotlinx.serialization.Serializable +import obsidian.server.Application import obsidian.server.io.RoutePlannerStatus import obsidian.server.io.RoutePlannerUtil.getDetailBlock import java.net.InetAddress fun Routing.planner() { - val routePlanner = Application.players.routePlanner + val routePlanner = Application.players.routePlanner - route("/routeplanner") { - authenticate { - get("/status") { - routePlanner - ?: return@get context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled()) + route("/routeplanner") { + authenticate { + get("/status") { + routePlanner + ?: return@get context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled()) - /* respond with route planner status */ - val status = RoutePlannerStatus( - Application.players::class.simpleName, - getDetailBlock(Application.players.routePlanner!!) - ) + /* respond with route planner status */ + val status = RoutePlannerStatus( + Application.players::class.simpleName, + getDetailBlock(Application.players.routePlanner!!) + ) - context.respond(status) - } + context.respond(status) + } - route("/free") { + route("/free") { - post("/address") { - routePlanner - ?: return@post context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled()) + post("/address") { + routePlanner + ?: return@post context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled()) - /* free address. */ - val body = context.receive() - val address = InetAddress.getByName(body.address) - routePlanner.freeAddress(address) + /* free address. */ + val body = context.receive() + val address = InetAddress.getByName(body.address) + routePlanner.freeAddress(address) - /* respond with 204 */ - context.respond(HttpStatusCode.NoContent) - } + /* respond with 204 */ + context.respond(HttpStatusCode.NoContent) + } - post("/all") { - /* free all addresses. */ - routePlanner ?: return@post context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled()) - routePlanner.freeAllAddresses() + post("/all") { + /* free all addresses. */ + routePlanner ?: return@post context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled()) + routePlanner.freeAllAddresses() - /* respond with 204 */ - context.respond(HttpStatusCode.NoContent) + /* respond with 204 */ + context.respond(HttpStatusCode.NoContent) + } + } } - } } - } } @Serializable diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt index 5c3e06e..36b5caa 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt @@ -18,7 +18,6 @@ package obsidian.server.io.rest import io.ktor.application.* import io.ktor.auth.* -import io.ktor.features.* import io.ktor.http.* import io.ktor.http.HttpStatusCode.Companion.BadRequest import io.ktor.http.HttpStatusCode.Companion.NotFound @@ -30,109 +29,105 @@ import io.ktor.util.pipeline.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import moe.kyokobot.koe.VoiceServerInfo -import obsidian.server.Application.config import obsidian.server.io.Handlers import obsidian.server.io.Magma -import obsidian.server.io.Magma.clientName +import obsidian.server.io.Magma.ClientName import obsidian.server.io.Magma.userId import obsidian.server.io.MagmaClient import obsidian.server.io.ws.CurrentTrack import obsidian.server.io.ws.Frames import obsidian.server.player.PlayerUpdates.Companion.currentTrackFor import obsidian.server.player.filter.Filters -import obsidian.server.config.spec.Obsidian -import obsidian.server.io.Magma.ClientName -import org.slf4j.LoggerFactory -import kotlin.text.Typography.ndash object Players { - private val ClientAttr = AttributeKey("MagmaClient") - private val GuildAttr = AttributeKey("Guild-Id") - - fun Routing.players() = this.authenticate { - this.route("/players/{guild}") { - /** - * Extracts useful information from each application call. - */ - intercept(ApplicationCallPipeline.Call) { - /* get the guild id */ - val guildId = call.parameters["guild"]?.toLongOrNull() - ?: return@intercept respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) - - call.attributes.put(GuildAttr, guildId) - - /* extract user id from the http request */ - val userId = call.request.userId() - ?: return@intercept respondAndFinish(BadRequest, Response("Missing 'User-Id' header or query parameter.")) - - call.attributes.put(ClientAttr, Magma.getClient(userId, call.attributes.getOrNull(ClientName))) - } - - /** - * - */ - get { - val guildId = call.attributes[GuildAttr] - - /* get the requested player */ - val player = call.attributes[ClientAttr].players[guildId] - ?: return@get respondAndFinish(NotFound, Response("Unknown player for guild '$guildId'")) - - /* respond */ - val response = GetPlayerResponse(currentTrackFor(player), player.filters, player.frameLossTracker.payload) - call.respond(response) - } - - /** - * - */ - put("/submit-voice-server") { - val vsi = call.receive().vsi - Handlers.submitVoiceServer(call.attributes[ClientAttr], call.attributes[GuildAttr], vsi) - call.respond(Response("successfully queued connection", success = true)) - } - - /** - * - */ - put("/filters") { - val filters = call.receive() - Handlers.configure(call.attributes[ClientAttr], call.attributes[GuildAttr], filters) - call.respond(Response("applied filters", success = true)) - } - - /** - * - */ - put("/seek") { - val (position) = call.receive() - Handlers.seek(context.attributes[ClientAttr], call.attributes[GuildAttr], position) - call.respond(Response("seeked to $position", success = true)) - } - - /** - * - */ - post("/play") { - val client = call.attributes[ClientAttr] - - /* connect to the voice server described in the request body */ - val (track, start, end, noReplace) = call.receive() - Handlers.playTrack(client, call.attributes[GuildAttr], track, start, end, noReplace) - - /* respond */ - call.respond(Response("playback has started", success = true)) - } - - /** - * - */ - post("/stop") { - Handlers.stopTrack(call.attributes[ClientAttr], call.attributes[GuildAttr]) - call.respond(Response("stopped the current track, if any.", success = true)) - } + private val ClientAttr = AttributeKey("MagmaClient") + private val GuildAttr = AttributeKey("Guild-Id") + + fun Routing.players() = this.authenticate { + this.route("/players/{guild}") { + /** + * Extracts useful information from each application call. + */ + intercept(ApplicationCallPipeline.Call) { + /* get the guild id */ + val guildId = call.parameters["guild"]?.toLongOrNull() + ?: return@intercept respondAndFinish(BadRequest, Response("Invalid or missing guild parameter.")) + + call.attributes.put(GuildAttr, guildId) + + /* extract user id from the http request */ + val userId = call.request.userId() + ?: return@intercept respondAndFinish(BadRequest, Response("Missing 'User-Id' header or query parameter.")) + + call.attributes.put(ClientAttr, Magma.getClient(userId, call.attributes.getOrNull(ClientName))) + } + + /** + * + */ + get { + val guildId = call.attributes[GuildAttr] + + /* get the requested player */ + val player = call.attributes[ClientAttr].players[guildId] + ?: return@get respondAndFinish(NotFound, Response("Unknown player for guild '$guildId'")) + + /* respond */ + val response = + GetPlayerResponse(currentTrackFor(player), player.filters, player.frameLossTracker.payload) + call.respond(response) + } + + /** + * + */ + put("/submit-voice-server") { + val vsi = call.receive().vsi + Handlers.submitVoiceServer(call.attributes[ClientAttr], call.attributes[GuildAttr], vsi) + call.respond(Response("successfully queued connection", success = true)) + } + + /** + * + */ + put("/filters") { + val filters = call.receive() + Handlers.configure(call.attributes[ClientAttr], call.attributes[GuildAttr], filters) + call.respond(Response("applied filters", success = true)) + } + + /** + * + */ + put("/seek") { + val (position) = call.receive() + Handlers.seek(context.attributes[ClientAttr], call.attributes[GuildAttr], position) + call.respond(Response("seeked to $position", success = true)) + } + + /** + * + */ + post("/play") { + val client = call.attributes[ClientAttr] + + /* connect to the voice server described in the request body */ + val (track, start, end, noReplace) = call.receive() + Handlers.playTrack(client, call.attributes[GuildAttr], track, start, end, noReplace) + + /* respond */ + call.respond(Response("playback has started", success = true)) + } + + /** + * + */ + post("/stop") { + Handlers.stopTrack(call.attributes[ClientAttr], call.attributes[GuildAttr]) + call.respond(Response("stopped the current track, if any.", success = true)) + } + } } - } } @@ -141,11 +136,11 @@ object Players { */ @Serializable data class SubmitVoiceServer(@SerialName("session_id") val sessionId: String, val token: String, val endpoint: String) { - /** - * The voice server info instance - */ - val vsi: VoiceServerInfo - get() = VoiceServerInfo(sessionId, endpoint, token) + /** + * The voice server info instance + */ + val vsi: VoiceServerInfo + get() = VoiceServerInfo(sessionId, endpoint, token) } /** @@ -159,16 +154,16 @@ data class Seek(val position: Long) */ @Serializable data class PlayTrack( - val track: String, - @SerialName("start_time") val startTime: Long? = null, - @SerialName("end_time") val endTime: Long? = null, - @SerialName("no_replace") val noReplace: Boolean = false + val track: String, + @SerialName("start_time") val startTime: Long? = null, + @SerialName("end_time") val endTime: Long? = null, + @SerialName("no_replace") val noReplace: Boolean = false ) @Serializable data class StopTrackResponse( - val track: Track?, - val success: Boolean + val track: Track?, + val success: Boolean ) /** @@ -176,9 +171,9 @@ data class StopTrackResponse( */ @Serializable data class GetPlayerResponse( - @SerialName("current_track") val currentTrack: CurrentTrack, - val filters: Filters?, - val frames: Frames + @SerialName("current_track") val currentTrack: CurrentTrack, + val filters: Filters?, + val frames: Frames ) /** @@ -191,9 +186,9 @@ data class Response(val message: String, val success: Boolean = false) * */ suspend inline fun PipelineContext.respondAndFinish( - statusCode: HttpStatusCode, - message: T + statusCode: HttpStatusCode, + message: T ) { - call.respond(statusCode, message) - finish() + call.respond(statusCode, message) + finish() } diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 34c0c28..33a94df 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -41,72 +41,72 @@ import org.slf4j.LoggerFactory private val logger: Logger = LoggerFactory.getLogger("Routing.tracks") fun Routing.tracks() { - authenticate { - get { data -> - val result = AudioLoader(Application.players) - .load(data.identifier) - .await() - - if (result.exception != null) { - logger.error("Track loading failed", result.exception) - } - - val playlist = result.playlistName?.let { - LoadTracks.Response.PlaylistInfo(name = it, selectedTrack = result.selectedTrack) - } - - val exception = if (result.loadResultType == LoadType.LOAD_FAILED && result.exception != null) { - LoadTracks.Response.Exception( - message = result.exception!!.localizedMessage, - severity = result.exception!!.severity - ) - } else { - null - } - - val response = LoadTracks.Response( - tracks = result.tracks.map(::getTrack), - type = result.loadResultType, - playlistInfo = playlist, - exception = exception - ) - - context.respond(response) - } - - get { - val track = TrackUtil.decode(it.track) - context.respond(getTrackInfo(track)) - } - - post("/decodetracks") { - val body = call.receive() - context.respond(body.tracks.map(::getTrackInfo)) + authenticate { + get { data -> + val result = AudioLoader(Application.players) + .load(data.identifier) + .await() + + if (result.exception != null) { + logger.error("Track loading failed", result.exception) + } + + val playlist = result.playlistName?.let { + LoadTracks.Response.PlaylistInfo(name = it, selectedTrack = result.selectedTrack) + } + + val exception = if (result.loadResultType == LoadType.LOAD_FAILED && result.exception != null) { + LoadTracks.Response.Exception( + message = result.exception!!.localizedMessage, + severity = result.exception!!.severity + ) + } else { + null + } + + val response = LoadTracks.Response( + tracks = result.tracks.map(::getTrack), + type = result.loadResultType, + playlistInfo = playlist, + exception = exception + ) + + context.respond(response) + } + + get { + val track = TrackUtil.decode(it.track) + context.respond(getTrackInfo(track)) + } + + post("/decodetracks") { + val body = call.receive() + context.respond(body.tracks.map(::getTrackInfo)) + } } - } } /** * */ private fun getTrack(audioTrack: AudioTrack): Track = - Track(track = audioTrack, info = getTrackInfo(audioTrack)) + Track(track = audioTrack, info = getTrackInfo(audioTrack)) /** * */ private fun getTrackInfo(audioTrack: AudioTrack): Track.Info = - Track.Info( - title = audioTrack.info.title, - uri = audioTrack.info.uri, - identifier = audioTrack.info.identifier, - author = audioTrack.info.author, - length = audioTrack.duration, - isSeekable = audioTrack.isSeekable, - isStream = audioTrack.info.isStream, - position = audioTrack.position, - sourceName = audioTrack.sourceManager?.sourceName ?: "unknown" - ) + Track.Info( + title = audioTrack.info.title, + uri = audioTrack.info.uri, + identifier = audioTrack.info.identifier, + author = audioTrack.info.author, + length = audioTrack.duration, + isSeekable = audioTrack.isSeekable, + isStream = audioTrack.info.isStream, + position = audioTrack.position, + sourceName = audioTrack.sourceManager?.sourceName ?: "unknown" + ) /** * @@ -125,58 +125,58 @@ data class DecodeTrack(val track: String) */ @Location("/loadtracks") data class LoadTracks(val identifier: String) { - @Serializable - data class Response( - @SerialName("load_type") - val type: LoadType, - @SerialName("playlist_info") - val playlistInfo: PlaylistInfo?, - val tracks: List, - val exception: Exception? - ) { @Serializable - data class Exception( - val message: String, - val severity: FriendlyException.Severity - ) + data class Response( + @SerialName("load_type") + val type: LoadType, + @SerialName("playlist_info") + val playlistInfo: PlaylistInfo?, + val tracks: List, + val exception: Exception? + ) { + @Serializable + data class Exception( + val message: String, + val severity: FriendlyException.Severity + ) - @Serializable - data class PlaylistInfo( - val name: String, - @SerialName("selected_track") - val selectedTrack: Int? - ) - } + @Serializable + data class PlaylistInfo( + val name: String, + @SerialName("selected_track") + val selectedTrack: Int? + ) + } } @Serializable data class Track( - @Serializable(with = AudioTrackSerializer::class) - val track: AudioTrack, - val info: Info + @Serializable(with = AudioTrackSerializer::class) + val track: AudioTrack, + val info: Info ) { - @Serializable - data class Info( - val title: String, - val author: String, - val uri: String, - val identifier: String, - val length: Long, - val position: Long, - @SerialName("is_stream") - val isStream: Boolean, - @SerialName("is_seekable") - val isSeekable: Boolean, - @SerialName("source_name") - val sourceName: String - ) + @Serializable + data class Info( + val title: String, + val author: String, + val uri: String, + val identifier: String, + val length: Long, + val position: Long, + @SerialName("is_stream") + val isStream: Boolean, + @SerialName("is_seekable") + val isSeekable: Boolean, + @SerialName("source_name") + val sourceName: String + ) } object AudioTrackListSerializer : JsonTransformingSerializer>(ListSerializer(AudioTrackSerializer)) { - override fun transformDeserialize(element: JsonElement): JsonElement = - if (element !is JsonArray) { - JsonArray(listOf(element)) - } else { - element - } + override fun transformDeserialize(element: JsonElement): JsonElement = + if (element !is JsonArray) { + JsonArray(listOf(element)) + } else { + element + } } diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/CloseReasons.kt b/Server/src/main/kotlin/obsidian/server/io/ws/CloseReasons.kt index c424f3f..6903b4e 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/CloseReasons.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/CloseReasons.kt @@ -19,9 +19,9 @@ package obsidian.server.io.ws import io.ktor.http.cio.websocket.* object CloseReasons { - val INVALID_AUTHORIZATION = CloseReason(4001, "Invalid or missing authorization header or query parameter.") - val MISSING_CLIENT_NAME = CloseReason(4002, "Missing 'Client-Name' header or query parameter") - val MISSING_USER_ID = CloseReason(4003, "Missing 'User-Id' header or query parameter") - val DUPLICATE_SESSION = CloseReason(4005, "A session for the supplied user already exists.") - // 4006 + val INVALID_AUTHORIZATION = CloseReason(4001, "Invalid or missing authorization header or query parameter.") + val MISSING_CLIENT_NAME = CloseReason(4002, "Missing 'Client-Name' header or query parameter") + val MISSING_USER_ID = CloseReason(4003, "Missing 'User-Id' header or query parameter") + val DUPLICATE_SESSION = CloseReason(4005, "A session for the supplied user already exists.") + // 4006 } diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt index 6052319..816ca93 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt @@ -30,60 +30,60 @@ import kotlinx.serialization.json.JsonObject import obsidian.server.util.kxs.AudioTrackSerializer sealed class Dispatch { - companion object : SerializationStrategy { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Dispatch") { - element("op", Op.descriptor) - element("d", JsonObject.serializer().descriptor) - } - - override fun serialize(encoder: Encoder, value: Dispatch) { - with(encoder.beginStructure(descriptor)) { - when (value) { - is Stats -> { - encodeSerializableElement(descriptor, 0, Op, Op.STATS) - encodeSerializableElement(descriptor, 1, Stats.serializer(), value) - } - - is PlayerUpdate -> { - encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_UPDATE) - encodeSerializableElement(descriptor, 1, PlayerUpdate.serializer(), value) - } - - is TrackStartEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) - encodeSerializableElement(descriptor, 1, TrackStartEvent.serializer(), value) - } - - is TrackEndEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) - encodeSerializableElement(descriptor, 1, TrackEndEvent.serializer(), value) - } - - is TrackExceptionEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) - encodeSerializableElement(descriptor, 1, TrackExceptionEvent.serializer(), value) - } - - is TrackStuckEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) - encodeSerializableElement(descriptor, 1, TrackStuckEvent.serializer(), value) - } - - is WebSocketOpenEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) - encodeSerializableElement(descriptor, 1, WebSocketOpenEvent.serializer(), value) - } - - is WebSocketClosedEvent -> { - encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) - encodeSerializableElement(descriptor, 1, WebSocketClosedEvent.serializer(), value) - } + companion object : SerializationStrategy { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Dispatch") { + element("op", Op.descriptor) + element("d", JsonObject.serializer().descriptor) } - endStructure(descriptor) - } + override fun serialize(encoder: Encoder, value: Dispatch) { + with(encoder.beginStructure(descriptor)) { + when (value) { + is Stats -> { + encodeSerializableElement(descriptor, 0, Op, Op.STATS) + encodeSerializableElement(descriptor, 1, Stats.serializer(), value) + } + + is PlayerUpdate -> { + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_UPDATE) + encodeSerializableElement(descriptor, 1, PlayerUpdate.serializer(), value) + } + + is TrackStartEvent -> { + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) + encodeSerializableElement(descriptor, 1, TrackStartEvent.serializer(), value) + } + + is TrackEndEvent -> { + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) + encodeSerializableElement(descriptor, 1, TrackEndEvent.serializer(), value) + } + + is TrackExceptionEvent -> { + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) + encodeSerializableElement(descriptor, 1, TrackExceptionEvent.serializer(), value) + } + + is TrackStuckEvent -> { + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) + encodeSerializableElement(descriptor, 1, TrackStuckEvent.serializer(), value) + } + + is WebSocketOpenEvent -> { + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) + encodeSerializableElement(descriptor, 1, WebSocketOpenEvent.serializer(), value) + } + + is WebSocketClosedEvent -> { + encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) + encodeSerializableElement(descriptor, 1, WebSocketClosedEvent.serializer(), value) + } + } + + endStructure(descriptor) + } + } } - } } @@ -91,188 +91,188 @@ sealed class Dispatch { @Serializable data class PlayerUpdate( - @Serializable(with = LongAsStringSerializer::class) - @SerialName("guild_id") - val guildId: Long, - val frames: Frames, - val filters: obsidian.server.player.filter.Filters?, - @SerialName("current_track") - val currentTrack: CurrentTrack, - val timestamp: Long + @Serializable(with = LongAsStringSerializer::class) + @SerialName("guild_id") + val guildId: Long, + val frames: Frames, + val filters: obsidian.server.player.filter.Filters?, + @SerialName("current_track") + val currentTrack: CurrentTrack, + val timestamp: Long ) : Dispatch() @Serializable data class CurrentTrack( - val track: String, - val position: Long, - val paused: Boolean, + val track: String, + val position: Long, + val paused: Boolean, ) @Serializable data class Frames( - val lost: Int, - val sent: Int, - val usable: Boolean + val lost: Int, + val sent: Int, + val usable: Boolean ) // Player Event lol @Serializable sealed class PlayerEvent : Dispatch() { - abstract val guildId: Long - abstract val type: PlayerEventType + abstract val guildId: Long + abstract val type: PlayerEventType } @Serializable data class WebSocketOpenEvent( - @Serializable(with = LongAsStringSerializer::class) - @SerialName("guild_id") - override val guildId: Long, - val ssrc: Int, - val target: String + @Serializable(with = LongAsStringSerializer::class) + @SerialName("guild_id") + override val guildId: Long, + val ssrc: Int, + val target: String ) : PlayerEvent() { - override val type: PlayerEventType = PlayerEventType.WEBSOCKET_OPEN + override val type: PlayerEventType = PlayerEventType.WEBSOCKET_OPEN } @Serializable data class WebSocketClosedEvent( - @Serializable(with = LongAsStringSerializer::class) - @SerialName("guild_id") - override val guildId: Long, - val reason: String?, - val code: Int, - @SerialName("by_remote") - val byRemote: Boolean + @Serializable(with = LongAsStringSerializer::class) + @SerialName("guild_id") + override val guildId: Long, + val reason: String?, + val code: Int, + @SerialName("by_remote") + val byRemote: Boolean ) : PlayerEvent() { - override val type: PlayerEventType = PlayerEventType.WEBSOCKET_CLOSED + override val type: PlayerEventType = PlayerEventType.WEBSOCKET_CLOSED } @Serializable data class TrackStartEvent( - @Serializable(with = LongAsStringSerializer::class) - @SerialName("guild_id") - override val guildId: Long, + @Serializable(with = LongAsStringSerializer::class) + @SerialName("guild_id") + override val guildId: Long, - @Serializable(with = AudioTrackSerializer::class) - val track: AudioTrack + @Serializable(with = AudioTrackSerializer::class) + val track: AudioTrack ) : PlayerEvent() { - override val type: PlayerEventType = PlayerEventType.TRACK_START + override val type: PlayerEventType = PlayerEventType.TRACK_START } @Serializable data class TrackEndEvent( - @Serializable(with = LongAsStringSerializer::class) - @SerialName("guild_id") - override val guildId: Long, + @Serializable(with = LongAsStringSerializer::class) + @SerialName("guild_id") + override val guildId: Long, - @Serializable(with = AudioTrackSerializer::class) - val track: AudioTrack?, + @Serializable(with = AudioTrackSerializer::class) + val track: AudioTrack?, - @SerialName("reason") - val endReason: AudioTrackEndReason + @SerialName("reason") + val endReason: AudioTrackEndReason ) : PlayerEvent() { - override val type: PlayerEventType = PlayerEventType.TRACK_END + override val type: PlayerEventType = PlayerEventType.TRACK_END } @Serializable data class TrackStuckEvent( - @Serializable(with = LongAsStringSerializer::class) - @SerialName("guild_id") - override val guildId: Long, + @Serializable(with = LongAsStringSerializer::class) + @SerialName("guild_id") + override val guildId: Long, - @SerialName("threshold_ms") - val thresholdMs: Long, + @SerialName("threshold_ms") + val thresholdMs: Long, - @Serializable(with = AudioTrackSerializer::class) - val track: AudioTrack? + @Serializable(with = AudioTrackSerializer::class) + val track: AudioTrack? ) : PlayerEvent() { - override val type: PlayerEventType = PlayerEventType.TRACK_STUCK + override val type: PlayerEventType = PlayerEventType.TRACK_STUCK } @Serializable data class TrackExceptionEvent( - @Serializable(with = LongAsStringSerializer::class) - @SerialName("guild_id") - override val guildId: Long, + @Serializable(with = LongAsStringSerializer::class) + @SerialName("guild_id") + override val guildId: Long, - @Serializable(with = AudioTrackSerializer::class) - val track: AudioTrack?, - val exception: Exception + @Serializable(with = AudioTrackSerializer::class) + val track: AudioTrack?, + val exception: Exception ) : PlayerEvent() { - override val type: PlayerEventType = PlayerEventType.TRACK_EXCEPTION - - @Serializable - data class Exception( - val message: String?, - val severity: FriendlyException.Severity, - val cause: String? - ) { - companion object { - /** - * Creates an [Exception] object from the supplied [FriendlyException] - * - * @param exc - * The friendly exception to use - */ - fun fromFriendlyException(exc: FriendlyException): Exception = Exception( - message = exc.message, - severity = exc.severity, - cause = exc.cause?.message - ) + override val type: PlayerEventType = PlayerEventType.TRACK_EXCEPTION + + @Serializable + data class Exception( + val message: String?, + val severity: FriendlyException.Severity, + val cause: String? + ) { + companion object { + /** + * Creates an [Exception] object from the supplied [FriendlyException] + * + * @param exc + * The friendly exception to use + */ + fun fromFriendlyException(exc: FriendlyException): Exception = Exception( + message = exc.message, + severity = exc.severity, + cause = exc.cause?.message + ) + } } - } } @Serializable data class Stats( - val memory: Memory, - val cpu: CPU, - val threads: Threads, - val frames: List, - val players: Players? + val memory: Memory, + val cpu: CPU, + val threads: Threads, + val frames: List, + val players: Players? ) : Dispatch() { - @Serializable - data class Memory( - @SerialName("heap_used") val heapUsed: Usage, - @SerialName("non_heap_used") val nonHeapUsed: Usage - ) { @Serializable - data class Usage(val init: Long, val max: Long, val committed: Long, val used: Long) - } - - @Serializable - data class Players(val active: Int, val total: Int) - - @Serializable - data class CPU( - val cores: Int, - @SerialName("system_load") val systemLoad: Double, - @SerialName("process_load") val processLoad: Double - ) - - @Serializable - data class Threads( - val running: Int, - val daemon: Int, - val peak: Int, - @SerialName("total_started") val totalStarted: Long - ) - - @Serializable - data class FrameStats( - @Serializable(with = LongAsStringSerializer::class) - @SerialName("guild_id") - val guildId: Long, - val usable: Boolean, - val lost: Int, - val sent: Int - ) + data class Memory( + @SerialName("heap_used") val heapUsed: Usage, + @SerialName("non_heap_used") val nonHeapUsed: Usage + ) { + @Serializable + data class Usage(val init: Long, val max: Long, val committed: Long, val used: Long) + } + + @Serializable + data class Players(val active: Int, val total: Int) + + @Serializable + data class CPU( + val cores: Int, + @SerialName("system_load") val systemLoad: Double, + @SerialName("process_load") val processLoad: Double + ) + + @Serializable + data class Threads( + val running: Int, + val daemon: Int, + val peak: Int, + @SerialName("total_started") val totalStarted: Long + ) + + @Serializable + data class FrameStats( + @Serializable(with = LongAsStringSerializer::class) + @SerialName("guild_id") + val guildId: Long, + val usable: Boolean, + val lost: Int, + val sent: Int + ) } enum class PlayerEventType { - WEBSOCKET_OPEN, - WEBSOCKET_CLOSED, - TRACK_START, - TRACK_END, - TRACK_STUCK, - TRACK_EXCEPTION + WEBSOCKET_OPEN, + WEBSOCKET_CLOSED, + TRACK_START, + TRACK_END, + TRACK_STUCK, + TRACK_EXCEPTION } diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt index fdb4cda..f2d3126 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt @@ -26,36 +26,36 @@ import kotlinx.serialization.encoding.Encoder @Serializable(with = Op.Serializer::class) enum class Op(val code: Short) { - SUBMIT_VOICE_UPDATE(0), - STATS(1), + SUBMIT_VOICE_UPDATE(0), + STATS(1), - SETUP_RESUMING(2), - SETUP_DISPATCH_BUFFER(3), + SETUP_RESUMING(2), + SETUP_DISPATCH_BUFFER(3), - PLAYER_EVENT(4), - PLAYER_UPDATE(5), + PLAYER_EVENT(4), + PLAYER_UPDATE(5), - PLAY_TRACK(6), - STOP_TRACK(7), - PAUSE(8), - FILTERS(9), - SEEK(10), - DESTROY(11), - CONFIGURE(12), + PLAY_TRACK(6), + STOP_TRACK(7), + PAUSE(8), + FILTERS(9), + SEEK(10), + DESTROY(11), + CONFIGURE(12), - UNKNOWN(-1); + UNKNOWN(-1); - companion object Serializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("MagmaOperation", PrimitiveKind.SHORT) + companion object Serializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("MagmaOperation", PrimitiveKind.SHORT) - override fun deserialize(decoder: Decoder): Op { - val code = decoder.decodeShort() - return values().firstOrNull { it.code == code } ?: UNKNOWN - } + override fun deserialize(decoder: Decoder): Op { + val code = decoder.decodeShort() + return values().firstOrNull { it.code == code } ?: UNKNOWN + } - override fun serialize(encoder: Encoder, value: Op) { - encoder.encodeShort(value.code) + override fun serialize(encoder: Encoder, value: Op) { + encoder.encodeShort(value.code) + } } - } } diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt index a3ffe3d..a94f1e3 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt @@ -29,144 +29,152 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject sealed class Operation { - companion object : DeserializationStrategy { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Operation") { - element("op", Op.descriptor) - element("d", JsonObject.serializer().descriptor, isOptional = true) - } - - @ExperimentalSerializationApi - override fun deserialize(decoder: Decoder): Operation? { - var op: Op? = null - var data: Operation? = null - - with(decoder.beginStructure(descriptor)) { - loop@ while (true) { - val idx = decodeElementIndex(descriptor) - fun decode(serializer: DeserializationStrategy) = - decodeSerializableElement(descriptor, idx, serializer) - - when (idx) { - CompositeDecoder.DECODE_DONE -> break@loop - - 0 -> - op = Op.deserialize(decoder) - - 1 -> - data = when (op) { - Op.SUBMIT_VOICE_UPDATE -> - decode(SubmitVoiceUpdate.serializer()) - - Op.PLAY_TRACK -> - decode(PlayTrack.serializer()) - - Op.STOP_TRACK -> - decode(StopTrack.serializer()) - - Op.PAUSE -> - decode(Pause.serializer()) - - Op.FILTERS -> - decode(Filters.serializer()) - - Op.SEEK -> - decode(Seek.serializer()) - - Op.DESTROY -> - decode(Destroy.serializer()) - - Op.SETUP_RESUMING -> - decode(SetupResuming.serializer()) - - Op.SETUP_DISPATCH_BUFFER -> - decode(SetupDispatchBuffer.serializer()) - - Op.CONFIGURE -> - decode(Configure.serializer()) + companion object : DeserializationStrategy { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Operation") { + element("op", Op.descriptor) + element("d", JsonObject.serializer().descriptor, isOptional = true) + } - else -> if (data == null) { - val element = decodeNullableSerializableElement(descriptor, idx, JsonElement.serializer().nullable) - error("Unknown 'd' field for operation ${op?.name}: $element") - } else { - decodeNullableSerializableElement(descriptor, idx, JsonElement.serializer().nullable) - data + @ExperimentalSerializationApi + override fun deserialize(decoder: Decoder): Operation? { + var op: Op? = null + var data: Operation? = null + + with(decoder.beginStructure(descriptor)) { + loop@ while (true) { + val idx = decodeElementIndex(descriptor) + fun decode(serializer: DeserializationStrategy) = + decodeSerializableElement(descriptor, idx, serializer) + + when (idx) { + CompositeDecoder.DECODE_DONE -> break@loop + + 0 -> + op = Op.deserialize(decoder) + + 1 -> + data = when (op) { + Op.SUBMIT_VOICE_UPDATE -> + decode(SubmitVoiceUpdate.serializer()) + + Op.PLAY_TRACK -> + decode(PlayTrack.serializer()) + + Op.STOP_TRACK -> + decode(StopTrack.serializer()) + + Op.PAUSE -> + decode(Pause.serializer()) + + Op.FILTERS -> + decode(Filters.serializer()) + + Op.SEEK -> + decode(Seek.serializer()) + + Op.DESTROY -> + decode(Destroy.serializer()) + + Op.SETUP_RESUMING -> + decode(SetupResuming.serializer()) + + Op.SETUP_DISPATCH_BUFFER -> + decode(SetupDispatchBuffer.serializer()) + + Op.CONFIGURE -> + decode(Configure.serializer()) + + else -> if (data == null) { + val element = decodeNullableSerializableElement( + descriptor, + idx, + JsonElement.serializer().nullable + ) + error("Unknown 'd' field for operation ${op?.name}: $element") + } else { + decodeNullableSerializableElement( + descriptor, + idx, + JsonElement.serializer().nullable + ) + data + } + } + } } - } - } - } - endStructure(descriptor) - return data - } + endStructure(descriptor) + return data + } + } } - } } @Serializable data class PlayTrack( - val track: String, + val track: String, - @SerialName("guild_id") - val guildId: Long, + @SerialName("guild_id") + val guildId: Long, - @SerialName("no_replace") - val noReplace: Boolean = false, + @SerialName("no_replace") + val noReplace: Boolean = false, - @SerialName("start_time") - val startTime: Long = 0, + @SerialName("start_time") + val startTime: Long = 0, - @SerialName("end_time") - val endTime: Long = 0 + @SerialName("end_time") + val endTime: Long = 0 ) : Operation() @Serializable data class StopTrack( - @SerialName("guild_id") - val guildId: Long + @SerialName("guild_id") + val guildId: Long ) : Operation() @Serializable data class SubmitVoiceUpdate( - val endpoint: String, - val token: String, + val endpoint: String, + val token: String, - @SerialName("guild_id") - val guildId: Long, + @SerialName("guild_id") + val guildId: Long, - @SerialName("session_id") - val sessionId: String, + @SerialName("session_id") + val sessionId: String, ) : Operation() @Serializable data class Pause( - @SerialName("guild_id") - val guildId: Long, - val state: Boolean = true + @SerialName("guild_id") + val guildId: Long, + val state: Boolean = true ) : Operation() @Serializable data class Filters( - @SerialName("guild_id") - val guildId: Long, - val filters: obsidian.server.player.filter.Filters + @SerialName("guild_id") + val guildId: Long, + val filters: obsidian.server.player.filter.Filters ) : Operation() @Serializable data class Seek( - @SerialName("guild_id") - val guildId: Long, - val position: Long + @SerialName("guild_id") + val guildId: Long, + val position: Long ) : Operation() @Serializable data class Configure( - @SerialName("guild_id") - val guildId: Long, - val pause: Boolean?, - val filters: obsidian.server.player.filter.Filters?, - @SerialName("send_player_updates") - val sendPlayerUpdates: Boolean? + @SerialName("guild_id") + val guildId: Long, + val pause: Boolean?, + val filters: obsidian.server.player.filter.Filters?, + @SerialName("send_player_updates") + val sendPlayerUpdates: Boolean? ) : Operation() @Serializable diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt index c6073a1..6c04e4d 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt @@ -23,93 +23,93 @@ import obsidian.server.util.CpuTimer import java.lang.management.ManagementFactory object StatsTask { - private val cpuTimer = CpuTimer() - private var OS_BEAN_CLASS: Class<*>? = null - - init { - try { - OS_BEAN_CLASS = Class.forName("com.sun.management.OperatingSystemMXBean") - } catch (ex: Exception) { - // no-op + private val cpuTimer = CpuTimer() + private var OS_BEAN_CLASS: Class<*>? = null + + init { + try { + OS_BEAN_CLASS = Class.forName("com.sun.management.OperatingSystemMXBean") + } catch (ex: Exception) { + // no-op + } } - } - - fun getRunnable(wsh: WebSocketHandler): Runnable { - return Runnable { - wsh.launch { - val stats = build(wsh.client) - wsh.send(stats) - } + + fun getRunnable(wsh: WebSocketHandler): Runnable { + return Runnable { + wsh.launch { + val stats = build(wsh.client) + wsh.send(stats) + } + } } - } - fun build(client: MagmaClient?): Stats { - /* memory stats. */ - val memory = ManagementFactory.getMemoryMXBean().let { bean -> - val heapUsed = bean.heapMemoryUsage.let { - Stats.Memory.Usage(committed = it.committed, max = it.max, init = it.init, used = it.used) - } + fun build(client: MagmaClient?): Stats { + /* memory stats. */ + val memory = ManagementFactory.getMemoryMXBean().let { bean -> + val heapUsed = bean.heapMemoryUsage.let { + Stats.Memory.Usage(committed = it.committed, max = it.max, init = it.init, used = it.used) + } - val nonHeapUsed = bean.nonHeapMemoryUsage.let { - Stats.Memory.Usage(committed = it.committed, max = it.max, init = it.init, used = it.used) - } + val nonHeapUsed = bean.nonHeapMemoryUsage.let { + Stats.Memory.Usage(committed = it.committed, max = it.max, init = it.init, used = it.used) + } - Stats.Memory(heapUsed = heapUsed, nonHeapUsed = nonHeapUsed) - } + Stats.Memory(heapUsed = heapUsed, nonHeapUsed = nonHeapUsed) + } - /* cpu stats */ - val os = ManagementFactory.getOperatingSystemMXBean() - val cpu = Stats.CPU( - cores = os.availableProcessors, - processLoad = cpuTimer.systemRecentCpuUsage, - systemLoad = cpuTimer.processRecentCpuUsage - ) - - /* threads */ - val threads = with(ManagementFactory.getThreadMXBean()) { - Stats.Threads( - running = threadCount, - daemon = daemonThreadCount, - peak = peakThreadCount, - totalStarted = totalStartedThreadCount - ) - } + /* cpu stats */ + val os = ManagementFactory.getOperatingSystemMXBean() + val cpu = Stats.CPU( + cores = os.availableProcessors, + processLoad = cpuTimer.systemRecentCpuUsage, + systemLoad = cpuTimer.processRecentCpuUsage + ) - /* player count */ - val players: Stats.Players = when (client) { - null -> { - var (active, total) = Pair(0, 0) - for ((_, c) in Magma.clients) { - c.players.forEach { (_, p) -> - total++ - if (p.playing) { - active++ - } - } + /* threads */ + val threads = with(ManagementFactory.getThreadMXBean()) { + Stats.Threads( + running = threadCount, + daemon = daemonThreadCount, + peak = peakThreadCount, + totalStarted = totalStartedThreadCount + ) } - Stats.Players(active = active, total = total) - } + /* player count */ + val players: Stats.Players = when (client) { + null -> { + var (active, total) = Pair(0, 0) + for ((_, c) in Magma.clients) { + c.players.forEach { (_, p) -> + total++ + if (p.playing) { + active++ + } + } + } + + Stats.Players(active = active, total = total) + } - else -> Stats.Players( - active = client.players.count { (_, l) -> l.playing }, - total = client.players.size - ) - } + else -> Stats.Players( + active = client.players.count { (_, l) -> l.playing }, + total = client.players.size + ) + } - /* frames */ - val frames: List = client?.let { - it.players.map { (_, player) -> - Stats.FrameStats( - usable = player.frameLossTracker.dataUsable, - guildId = player.guildId, - sent = player.frameLossTracker.success.sum(), - lost = player.frameLossTracker.loss.sum(), - ) - } - } ?: emptyList() + /* frames */ + val frames: List = client?.let { + it.players.map { (_, player) -> + Stats.FrameStats( + usable = player.frameLossTracker.dataUsable, + guildId = player.guildId, + sent = player.frameLossTracker.success.sum(), + lost = player.frameLossTracker.loss.sum(), + ) + } + } ?: emptyList() - return Stats(cpu = cpu, memory = memory, threads = threads, frames = frames, players = players) - } + return Stats(cpu = cpu, memory = memory, threads = threads, frames = frames, players = players) + } } diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt index b7c7c57..dacc289 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt @@ -39,262 +39,262 @@ import kotlin.time.ExperimentalTime class WebSocketHandler(val client: MagmaClient, private var session: WebSocketServerSession) : CoroutineScope { - /** - * Resume key - */ - var resumeKey: String? = null - - /** - * Stats interval. - */ - private var stats = - Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Stats-Dispatcher %d", daemon = true)) - - /** - * Whether this magma client is active - */ - private var active: Boolean = false - - /** - * Resume timeout - */ - private var resumeTimeout: Long? = null - - /** - * Timeout future - */ - private var resumeTimeoutFuture: ScheduledFuture<*>? = null - - /** - * The dispatch buffer timeout - */ - private var bufferTimeout: Long? = null - - /** - * The dispatch buffer - */ - private var dispatchBuffer: ConcurrentLinkedQueue? = null - - /** - * Events flow lol - idk kotlin - */ - private val events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) - - override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + Job() - - init { - /* websocket and rest operations */ - on { - val vsi = VoiceServerInfo(sessionId, endpoint, token) - Handlers.submitVoiceServer(client, guildId, vsi) - } + /** + * Resume key + */ + var resumeKey: String? = null + + /** + * Stats interval. + */ + private var stats = + Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Stats-Dispatcher %d", daemon = true)) + + /** + * Whether this magma client is active + */ + private var active: Boolean = false + + /** + * Resume timeout + */ + private var resumeTimeout: Long? = null + + /** + * Timeout future + */ + private var resumeTimeoutFuture: ScheduledFuture<*>? = null + + /** + * The dispatch buffer timeout + */ + private var bufferTimeout: Long? = null + + /** + * The dispatch buffer + */ + private var dispatchBuffer: ConcurrentLinkedQueue? = null + + /** + * Events flow lol - idk kotlin + */ + private val events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) + + override val coroutineContext: CoroutineContext + get() = Dispatchers.IO + Job() + + init { + /* websocket and rest operations */ + on { + val vsi = VoiceServerInfo(sessionId, endpoint, token) + Handlers.submitVoiceServer(client, guildId, vsi) + } - on { - Handlers.configure(client, guildId, filters = filters) - } + on { + Handlers.configure(client, guildId, filters = filters) + } - on { - Handlers.configure(client, guildId, pause = state) - } + on { + Handlers.configure(client, guildId, pause = state) + } - on { - Handlers.configure(client, guildId, filters, pause, sendPlayerUpdates) - } + on { + Handlers.configure(client, guildId, filters, pause, sendPlayerUpdates) + } - on { - Handlers.seek(client, guildId, position) - } + on { + Handlers.seek(client, guildId, position) + } - on { - Handlers.playTrack(client, guildId, track, startTime, endTime, noReplace) - } + on { + Handlers.playTrack(client, guildId, track, startTime, endTime, noReplace) + } - on { - Handlers.stopTrack(client, guildId) - } + on { + Handlers.stopTrack(client, guildId) + } - on { - Handlers.destroy(client, guildId) - } + on { + Handlers.destroy(client, guildId) + } + + /* websocket-only operations */ + on { + resumeKey = key + resumeTimeout = timeout - /* websocket-only operations */ - on { - resumeKey = key - resumeTimeout = timeout + log.debug("${client.displayName} - Resuming has been configured; key= $key, timeout= $timeout") + } + + on { + bufferTimeout = timeout + log.debug("${client.displayName} - Dispatch buffer timeout: $timeout") + } - log.debug("${client.displayName} - Resuming has been configured; key= $key, timeout= $timeout") } - on { - bufferTimeout = timeout - log.debug("${client.displayName} - Dispatch buffer timeout: $timeout") + /** + * + */ + @OptIn(ExperimentalTime::class) + suspend fun listen() { + active = true + + /* starting sending stats. */ + val statsRunnable = StatsTask.getRunnable(this) + stats.scheduleAtFixedRate(statsRunnable, 0, 1, TimeUnit.MINUTES) + + /* listen for incoming frames. */ + session.incoming + .asFlow() + .buffer(Channel.UNLIMITED) + .collect { + when (it) { + is Frame.Binary, is Frame.Text -> handleIncomingFrame(it) + else -> { // no-op + } + } + } + + log.info("${client.displayName} - web-socket session has closed.") + + /* connection has been closed. */ + active = false } - } - - /** - * - */ - @OptIn(ExperimentalTime::class) - suspend fun listen() { - active = true - - /* starting sending stats. */ - val statsRunnable = StatsTask.getRunnable(this) - stats.scheduleAtFixedRate(statsRunnable, 0, 1, TimeUnit.MINUTES) - - /* listen for incoming frames. */ - session.incoming - .asFlow() - .buffer(Channel.UNLIMITED) - .collect { - when (it) { - is Frame.Binary, is Frame.Text -> handleIncomingFrame(it) - else -> { // no-op - } + /** + * Handles + */ + suspend fun handleClose() { + if (resumeKey != null) { + if (bufferTimeout?.takeIf { it > 0 } != null) { + dispatchBuffer = ConcurrentLinkedQueue() + } + + val runnable = Runnable { + runBlocking { + client.shutdown() + } + } + + resumeTimeoutFuture = cleanupExecutor.schedule(runnable, resumeTimeout!!, TimeUnit.MILLISECONDS) + log.info("${client.displayName} - Session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") + return } - } - - log.info("${client.displayName} - web-socket session has closed.") - - /* connection has been closed. */ - active = false - } - - /** - * Handles - */ - suspend fun handleClose() { - if (resumeKey != null) { - if (bufferTimeout?.takeIf { it > 0 } != null) { - dispatchBuffer = ConcurrentLinkedQueue() - } - - val runnable = Runnable { - runBlocking { - client.shutdown() - } - } - resumeTimeoutFuture = cleanupExecutor.schedule(runnable, resumeTimeout!!, TimeUnit.MILLISECONDS) - log.info("${client.displayName} - Session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") - return + client.shutdown() } - client.shutdown() - } + /** + * Resumes this session + */ + suspend fun resume(session: WebSocketServerSession) { + log.info("${client.displayName} - session has been resumed") - /** - * Resumes this session - */ - suspend fun resume(session: WebSocketServerSession) { - log.info("${client.displayName} - session has been resumed") + this.session = session + this.active = true + this.resumeTimeoutFuture?.cancel(false) - this.session = session - this.active = true - this.resumeTimeoutFuture?.cancel(false) + dispatchBuffer?.let { + for (payload in dispatchBuffer!!) { + send(payload) + } + } - dispatchBuffer?.let { - for (payload in dispatchBuffer!!) { - send(payload) - } + listen() } - listen() - } - - /** - * Send a JSON payload to the client. - * - * @param dispatch The dispatch instance - */ - fun send(dispatch: Dispatch) { - val json = json.encodeToString(Dispatch.Companion, dispatch) - if (!session.isActive) { - dispatchBuffer?.offer(json) - return + /** + * Send a JSON payload to the client. + * + * @param dispatch The dispatch instance + */ + fun send(dispatch: Dispatch) { + val json = json.encodeToString(Dispatch.Companion, dispatch) + if (!session.isActive) { + dispatchBuffer?.offer(json) + return + } + + send(json, dispatch::class.simpleName) } - send(json, dispatch::class.simpleName) - } - - /** - * Shuts down this websocket handler - */ - suspend fun shutdown() { - stats.shutdownNow() - - /* cancel this coroutine context */ - try { - currentCoroutineContext().cancelChildren() - currentCoroutineContext().cancel() - } catch (ex: Exception) { - log.warn("${client.displayName} - Error occurred while cancelling this coroutine scope") + /** + * Shuts down this websocket handler + */ + suspend fun shutdown() { + stats.shutdownNow() + + /* cancel this coroutine context */ + try { + currentCoroutineContext().cancelChildren() + currentCoroutineContext().cancel() + } catch (ex: Exception) { + log.warn("${client.displayName} - Error occurred while cancelling this coroutine scope") + } + + /* close the websocket session, if not already */ + if (active) { + session.close(CloseReason(1000, "shutting down")) + } } - /* close the websocket session, if not already */ - if (active) { - session.close(CloseReason(1000, "shutting down")) + /** + * Sends a JSON encoded dispatch payload to the client + * + * @param json JSON encoded dispatch payload + */ + private fun send(json: String, payloadName: String? = null) { + try { + log.trace("${client.displayName} ${payloadName?.let { "$it " } ?: ""}<<< $json") + session.outgoing.trySend(Frame.Text(json)) + } catch (ex: Exception) { + log.error("${client.displayName} - An exception occurred while sending a json payload", ex) + } } - } - - /** - * Sends a JSON encoded dispatch payload to the client - * - * @param json JSON encoded dispatch payload - */ - private fun send(json: String, payloadName: String? = null) { - try { - log.trace("${client.displayName} ${payloadName?.let { "$it " } ?: ""}<<< $json") - session.outgoing.trySend(Frame.Text(json)) - } catch (ex: Exception) { - log.error("${client.displayName} - An exception occurred while sending a json payload", ex) + + /** + * Convenience method that calls [block] whenever [T] gets emitted on [events] + */ + private inline fun on(crossinline block: suspend T.() -> Unit) { + events.filterIsInstance() + .onEach { + launch { + try { + block.invoke(it) + } catch (ex: Exception) { + log.error("${client.displayName} - An exception occurred while handling a command", ex) + } + } + } + .launchIn(this) } - } - - /** - * Convenience method that calls [block] whenever [T] gets emitted on [events] - */ - private inline fun on(crossinline block: suspend T.() -> Unit) { - events.filterIsInstance() - .onEach { - launch { - try { - block.invoke(it) - } catch (ex: Exception) { - log.error("${client.displayName} - An exception occurred while handling a command", ex) - } + + /** + * Handles an incoming [Frame]. + * + * @param frame The received text or binary frame. + */ + private fun handleIncomingFrame(frame: Frame) { + val data = frame.data.toString(Charset.defaultCharset()) + + try { + log.info("${client.displayName} >>> $data") + json.decodeFromString(Operation, data)?.let { events.tryEmit(it) } + } catch (ex: Exception) { + log.error("${client.displayName} - An exception occurred while handling an incoming frame", ex) } - } - .launchIn(this) - } - - /** - * Handles an incoming [Frame]. - * - * @param frame The received text or binary frame. - */ - private fun handleIncomingFrame(frame: Frame) { - val data = frame.data.toString(Charset.defaultCharset()) - - try { - log.info("${client.displayName} >>> $data") - json.decodeFromString(Operation, data)?.let { events.tryEmit(it) } - } catch (ex: Exception) { - log.error("${client.displayName} - An exception occurred while handling an incoming frame", ex) - } - } - - companion object { - fun ReceiveChannel.asFlow() = flow { - try { - for (event in this@asFlow) emit(event) - } catch (ex: CancellationException) { - // no-op - } } - private val log: Logger = LoggerFactory.getLogger(MagmaClient::class.java) - } + companion object { + fun ReceiveChannel.asFlow() = flow { + try { + for (event in this@asFlow) emit(event) + } catch (ex: CancellationException) { + // no-op + } + } + + private val log: Logger = LoggerFactory.getLogger(MagmaClient::class.java) + } } diff --git a/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt b/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt index 3bc75ed..b58dc04 100644 --- a/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt +++ b/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt @@ -25,107 +25,107 @@ import obsidian.server.util.ByteRingBuffer import java.util.concurrent.TimeUnit class FrameLossTracker : AudioEventAdapter() { - /** - * - */ - var success = ByteRingBuffer(60) - - /** - * - */ - var loss = ByteRingBuffer(60) - - /** - * - */ - val dataUsable: Boolean - get() { - if (lastTrackStarted - lastTrackEnded > ACCEPTABLE_TRACK_SWITCH_TIME && lastTrackEnded != Long.MAX_VALUE) { - return false - } - - return TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - playingSince) >= 60 + /** + * + */ + var success = ByteRingBuffer(60) + + /** + * + */ + var loss = ByteRingBuffer(60) + + /** + * + */ + val dataUsable: Boolean + get() { + if (lastTrackStarted - lastTrackEnded > ACCEPTABLE_TRACK_SWITCH_TIME && lastTrackEnded != Long.MAX_VALUE) { + return false + } + + return TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - playingSince) >= 60 + } + + /** + * + */ + val payload: Frames + get() = Frames( + sent = success.sum(), + lost = loss.sum(), + usable = dataUsable + ) + + private var curSuccess: Byte = 0 + private var curLoss: Byte = 0 + + private var lastUpdate: Long = 0 + private var playingSince = Long.MAX_VALUE + + private var lastTrackEnded = Long.MAX_VALUE + private var lastTrackStarted = Long.MAX_VALUE / 2 + + /** + * Increments the amount of successful frames. + */ + fun success() { + checkTime() + curSuccess++ } - /** - * - */ - val payload: Frames - get() = Frames( - sent = success.sum(), - lost = loss.sum(), - usable = dataUsable - ) - - private var curSuccess: Byte = 0 - private var curLoss: Byte = 0 - - private var lastUpdate: Long = 0 - private var playingSince = Long.MAX_VALUE - - private var lastTrackEnded = Long.MAX_VALUE - private var lastTrackStarted = Long.MAX_VALUE / 2 - - /** - * Increments the amount of successful frames. - */ - fun success() { - checkTime() - curSuccess++ - } - - /** - * Increments the amount of frame losses. - */ - fun loss() { - checkTime() - curLoss++ - } - - private fun checkTime() { - val now = System.nanoTime() - if (now - lastUpdate > ONE_SECOND) { - lastUpdate = now - - /* update success & loss buffers */ - success.put(curSuccess) - loss.put(curLoss) - - /* reset current success & loss */ - curSuccess = 0 - curLoss = 0 + /** + * Increments the amount of frame losses. + */ + fun loss() { + checkTime() + curLoss++ } - } - private fun start() { - lastTrackStarted = System.nanoTime() - if (lastTrackStarted - playingSince > ACCEPTABLE_TRACK_SWITCH_TIME || playingSince == Long.MAX_VALUE) { - playingSince = lastTrackStarted + private fun checkTime() { + val now = System.nanoTime() + if (now - lastUpdate > ONE_SECOND) { + lastUpdate = now - /* clear success & loss buffers */ - success.clear() - loss.clear() + /* update success & loss buffers */ + success.put(curSuccess) + loss.put(curLoss) + + /* reset current success & loss */ + curSuccess = 0 + curLoss = 0 + } } - } - private fun end() { - lastTrackEnded = System.nanoTime() - } + private fun start() { + lastTrackStarted = System.nanoTime() + if (lastTrackStarted - playingSince > ACCEPTABLE_TRACK_SWITCH_TIME || playingSince == Long.MAX_VALUE) { + playingSince = lastTrackStarted - /* listeners */ - override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack?, reason: AudioTrackEndReason?) = end() - override fun onTrackStart(player: AudioPlayer?, track: AudioTrack?) = start() - override fun onPlayerPause(player: AudioPlayer?) = end() - override fun onPlayerResume(player: AudioPlayer?) = start() + /* clear success & loss buffers */ + success.clear() + loss.clear() + } + } - companion object { - const val ONE_SECOND = 1e9 - const val ACCEPTABLE_TRACK_SWITCH_TIME = 1e8 + private fun end() { + lastTrackEnded = System.nanoTime() + } - /** - * Number of packets expected to be sent over one minute. - * *3000* packets with *20ms* of audio each - */ - const val EXPECTED_PACKET_COUNT_PER_MIN = 60 * 1000 / 20 - } + /* listeners */ + override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack?, reason: AudioTrackEndReason?) = end() + override fun onTrackStart(player: AudioPlayer?, track: AudioTrack?) = start() + override fun onPlayerPause(player: AudioPlayer?) = end() + override fun onPlayerResume(player: AudioPlayer?) = start() + + companion object { + const val ONE_SECOND = 1e9 + const val ACCEPTABLE_TRACK_SWITCH_TIME = 1e8 + + /** + * Number of packets expected to be sent over one minute. + * *3000* packets with *20ms* of audio each + */ + const val EXPECTED_PACKET_COUNT_PER_MIN = 60 * 1000 / 20 + } } diff --git a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt index e85cfe3..5dc0db1 100644 --- a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt +++ b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt @@ -40,126 +40,126 @@ import java.net.InetAddress import java.util.function.Predicate class ObsidianAPM : DefaultAudioPlayerManager() { - private val enabledSources = mutableListOf() - - /** - * The route planner. - */ - val routePlanner: AbstractRoutePlanner? by lazy { - val ipBlockList = config[Obsidian.Lavaplayer.RateLimit.ipBlocks] - if (ipBlockList.isEmpty()) { - return@lazy null - } - - val ipBlocks = ipBlockList.map { - when { - Ipv6Block.isIpv6CidrBlock(it) -> Ipv6Block(it) - Ipv4Block.isIpv4CidrBlock(it) -> Ipv4Block(it) - else -> throw RuntimeException("Invalid IP Block '$it', make sure to provide a valid CIDR notation") - } - } + private val enabledSources = mutableListOf() + + /** + * The route planner. + */ + val routePlanner: AbstractRoutePlanner? by lazy { + val ipBlockList = config[Obsidian.Lavaplayer.RateLimit.ipBlocks] + if (ipBlockList.isEmpty()) { + return@lazy null + } - val blacklisted = config[Obsidian.Lavaplayer.RateLimit.excludedIps].map { - InetAddress.getByName(it) - } + val ipBlocks = ipBlockList.map { + when { + Ipv6Block.isIpv6CidrBlock(it) -> Ipv6Block(it) + Ipv4Block.isIpv4CidrBlock(it) -> Ipv4Block(it) + else -> throw RuntimeException("Invalid IP Block '$it', make sure to provide a valid CIDR notation") + } + } - val filter = Predicate { !blacklisted.contains(it) } - val searchTriggersFail = config[Obsidian.Lavaplayer.RateLimit.searchTriggersFail] + val blacklisted = config[Obsidian.Lavaplayer.RateLimit.excludedIps].map { + InetAddress.getByName(it) + } - return@lazy when (config[Obsidian.Lavaplayer.RateLimit.strategy]) { - "rotate-on-ban" -> RotatingIpRoutePlanner(ipBlocks, filter, searchTriggersFail) - "load-balance" -> BalancingIpRoutePlanner(ipBlocks, filter, searchTriggersFail) - "rotating-nano-switch" -> RotatingNanoIpRoutePlanner(ipBlocks, filter, searchTriggersFail) - "nano-switch" -> NanoIpRoutePlanner(ipBlocks, searchTriggersFail) - else -> throw RuntimeException("Unknown strategy!") - } - } - - init { - configuration.apply { - isFilterHotSwapEnabled = true - if (config[Obsidian.Lavaplayer.nonAllocating]) { - logger.info("Using the non-allocating audio frame buffer.") - setFrameBufferFactory(::NonAllocatingAudioFrameBuffer) - } - } + val filter = Predicate { !blacklisted.contains(it) } + val searchTriggersFail = config[Obsidian.Lavaplayer.RateLimit.searchTriggersFail] - if (config[Obsidian.Lavaplayer.gcMonitoring]) { - enableGcMonitoring() + return@lazy when (config[Obsidian.Lavaplayer.RateLimit.strategy]) { + "rotate-on-ban" -> RotatingIpRoutePlanner(ipBlocks, filter, searchTriggersFail) + "load-balance" -> BalancingIpRoutePlanner(ipBlocks, filter, searchTriggersFail) + "rotating-nano-switch" -> RotatingNanoIpRoutePlanner(ipBlocks, filter, searchTriggersFail) + "nano-switch" -> NanoIpRoutePlanner(ipBlocks, searchTriggersFail) + else -> throw RuntimeException("Unknown strategy!") + } } - registerSources() - } + init { + configuration.apply { + isFilterHotSwapEnabled = true + if (config[Obsidian.Lavaplayer.nonAllocating]) { + logger.info("Using the non-allocating audio frame buffer.") + setFrameBufferFactory(::NonAllocatingAudioFrameBuffer) + } + } - private fun registerSources() { - config[Obsidian.Lavaplayer.enabledSources] - .forEach { source -> - when (source.lowercase()) { - "youtube" -> { - val youtube = YoutubeAudioSourceManager(config[Obsidian.Lavaplayer.YouTube.allowSearch]).apply { - setPlaylistPageCount(config[Obsidian.Lavaplayer.YouTube.playlistPageLimit]) + if (config[Obsidian.Lavaplayer.gcMonitoring]) { + enableGcMonitoring() + } - if (routePlanner != null) { - val rotator = YoutubeIpRotatorSetup(routePlanner) - .forSource(this) + registerSources() + } - val retryLimit = config[Obsidian.Lavaplayer.RateLimit.retryLimit] - if (retryLimit <= 0) { - rotator.withRetryLimit(if (retryLimit == 0) Int.MAX_VALUE else retryLimit) + private fun registerSources() { + config[Obsidian.Lavaplayer.enabledSources] + .forEach { source -> + when (source.lowercase()) { + "youtube" -> { + val youtube = YoutubeAudioSourceManager(config[Obsidian.Lavaplayer.YouTube.allowSearch]).apply { + setPlaylistPageCount(config[Obsidian.Lavaplayer.YouTube.playlistPageLimit]) + + if (routePlanner != null) { + val rotator = YoutubeIpRotatorSetup(routePlanner) + .forSource(this) + + val retryLimit = config[Obsidian.Lavaplayer.RateLimit.retryLimit] + if (retryLimit <= 0) { + rotator.withRetryLimit(if (retryLimit == 0) Int.MAX_VALUE else retryLimit) + } + + rotator.setup() + } + } + + registerSourceManager(youtube) + } + + "soundcloud" -> { + val dataReader = DefaultSoundCloudDataReader() + val htmlDataLoader = DefaultSoundCloudHtmlDataLoader() + val formatHandler = DefaultSoundCloudFormatHandler() + + registerSourceManager( + SoundCloudAudioSourceManager( + config[Obsidian.Lavaplayer.allowScSearch], + dataReader, + htmlDataLoader, + formatHandler, + DefaultSoundCloudPlaylistLoader(htmlDataLoader, dataReader, formatHandler) + ) + ) + } + + "nico" -> { + val email = config[Obsidian.Lavaplayer.Nico.email] + val password = config[Obsidian.Lavaplayer.Nico.password] + + if (email.isNotBlank() && password.isNotBlank()) { + registerSourceManager(NicoAudioSourceManager(email, password)) + } + } + + "bandcamp" -> registerSourceManager(BandcampAudioSourceManager()) + "twitch" -> registerSourceManager(TwitchStreamAudioSourceManager()) + "vimeo" -> registerSourceManager(VimeoAudioSourceManager()) + "http" -> registerSourceManager(HttpAudioSourceManager()) + "local" -> registerSourceManager(LocalAudioSourceManager()) + "yarn" -> registerSourceManager(GetyarnAudioSourceManager()) + + else -> logger.warn("Unknown source \"$source\"") } - - rotator.setup() - } - } - - registerSourceManager(youtube) - } - - "soundcloud" -> { - val dataReader = DefaultSoundCloudDataReader() - val htmlDataLoader = DefaultSoundCloudHtmlDataLoader() - val formatHandler = DefaultSoundCloudFormatHandler() - - registerSourceManager( - SoundCloudAudioSourceManager( - config[Obsidian.Lavaplayer.allowScSearch], - dataReader, - htmlDataLoader, - formatHandler, - DefaultSoundCloudPlaylistLoader(htmlDataLoader, dataReader, formatHandler) - ) - ) - } - - "nico" -> { - val email = config[Obsidian.Lavaplayer.Nico.email] - val password = config[Obsidian.Lavaplayer.Nico.password] - - if (email.isNotBlank() && password.isNotBlank()) { - registerSourceManager(NicoAudioSourceManager(email, password)) } - } - "bandcamp" -> registerSourceManager(BandcampAudioSourceManager()) - "twitch" -> registerSourceManager(TwitchStreamAudioSourceManager()) - "vimeo" -> registerSourceManager(VimeoAudioSourceManager()) - "http" -> registerSourceManager(HttpAudioSourceManager()) - "local" -> registerSourceManager(LocalAudioSourceManager()) - "yarn" -> registerSourceManager(GetyarnAudioSourceManager()) - - else -> logger.warn("Unknown source \"$source\"") - } - } - - logger.info("Enabled sources: ${enabledSources.joinToString(", ")}") - } + logger.info("Enabled sources: ${enabledSources.joinToString(", ")}") + } - override fun registerSourceManager(sourceManager: AudioSourceManager) { - super.registerSourceManager(sourceManager) - enabledSources.add(sourceManager.sourceName) - } + override fun registerSourceManager(sourceManager: AudioSourceManager) { + super.registerSourceManager(sourceManager) + enabledSources.add(sourceManager.sourceName) + } - companion object { - private val logger: Logger = LoggerFactory.getLogger(ObsidianAPM::class.java) - } + companion object { + private val logger: Logger = LoggerFactory.getLogger(ObsidianAPM::class.java) + } } diff --git a/Server/src/main/kotlin/obsidian/server/player/Player.kt b/Server/src/main/kotlin/obsidian/server/player/Player.kt index bfad7c9..a928bf2 100644 --- a/Server/src/main/kotlin/obsidian/server/player/Player.kt +++ b/Server/src/main/kotlin/obsidian/server/player/Player.kt @@ -38,172 +38,172 @@ import java.nio.ByteBuffer class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { - /** - * Handles all updates for this player. - */ - val updates: PlayerUpdates by lazy { - PlayerUpdates(this) - } - - /** - * Audio player for receiving frames. - */ - val audioPlayer: AudioPlayer by lazy { - players.createPlayer() - .addEventListener(frameLossTracker) - .addEventListener(updates) - .addEventListener(this) - } - - /** - * Frame loss tracker. - */ - val frameLossTracker = FrameLossTracker() - - /** - * Whether the player is currently playing a track. - */ - val playing: Boolean - get() = audioPlayer.playingTrack != null && !audioPlayer.isPaused - - /** - * The current filters that are enabled. - */ - var filters: Filters? = null - set(value) { - field = value - value?.applyTo(this) + /** + * Handles all updates for this player. + */ + val updates: PlayerUpdates by lazy { + PlayerUpdates(this) } - /** - * Plays the provided [track] and dispatches a Player Update - */ - fun play(track: AudioTrack) { - audioPlayer.playTrack(track) - updates.sendUpdate() - } - - /** - * Convenience method for seeking to a specific position in the current track. - */ - fun seekTo(position: Long) { - require(audioPlayer.playingTrack != null) { - "A track must be playing in order to seek." + /** + * Audio player for receiving frames. + */ + val audioPlayer: AudioPlayer by lazy { + players.createPlayer() + .addEventListener(frameLossTracker) + .addEventListener(updates) + .addEventListener(this) } - require(audioPlayer.playingTrack.isSeekable) { - "The playing track is not seekable." + /** + * Frame loss tracker. + */ + val frameLossTracker = FrameLossTracker() + + /** + * Whether the player is currently playing a track. + */ + val playing: Boolean + get() = audioPlayer.playingTrack != null && !audioPlayer.isPaused + + /** + * The current filters that are enabled. + */ + var filters: Filters? = null + set(value) { + field = value + value?.applyTo(this) + } + + /** + * Plays the provided [track] and dispatches a Player Update + */ + fun play(track: AudioTrack) { + audioPlayer.playTrack(track) + updates.sendUpdate() } - require(position in 0..audioPlayer.playingTrack.duration) { - "The given position must be within 0 and the current playing track's duration." + /** + * Convenience method for seeking to a specific position in the current track. + */ + fun seekTo(position: Long) { + require(audioPlayer.playingTrack != null) { + "A track must be playing in order to seek." + } + + require(audioPlayer.playingTrack.isSeekable) { + "The playing track is not seekable." + } + + require(position in 0..audioPlayer.playingTrack.duration) { + "The given position must be within 0 and the current playing track's duration." + } + + audioPlayer.playingTrack.position = position } - audioPlayer.playingTrack.position = position - } - - /** - * - */ - fun provideTo(connection: MediaConnection) { - connection.audioSender = OpusFrameProvider(connection) - } - - /** - * - */ - override fun onTrackStuck(player: AudioPlayer?, track: AudioTrack?, thresholdMs: Long) { - client.websocket?.let { - val event = TrackStuckEvent( - guildId = guildId, - thresholdMs = thresholdMs, - track = track - ) - - it.send(event) + /** + * + */ + fun provideTo(connection: MediaConnection) { + connection.audioSender = OpusFrameProvider(connection) } - } - - /** - * - */ - override fun onTrackException(player: AudioPlayer?, track: AudioTrack, exception: FriendlyException) { - client.websocket?.let { - val event = TrackExceptionEvent( - guildId = guildId, - track = track, - exception = TrackExceptionEvent.Exception.fromFriendlyException(exception) - ) - - it.send(event) + + /** + * + */ + override fun onTrackStuck(player: AudioPlayer?, track: AudioTrack?, thresholdMs: Long) { + client.websocket?.let { + val event = TrackStuckEvent( + guildId = guildId, + thresholdMs = thresholdMs, + track = track + ) + + it.send(event) + } } - } - - /** - * - */ - override fun onTrackStart(player: AudioPlayer?, track: AudioTrack) { - client.websocket?.let { - val event = TrackStartEvent( - guildId = guildId, - track = track - ) - - it.send(event) + + /** + * + */ + override fun onTrackException(player: AudioPlayer?, track: AudioTrack, exception: FriendlyException) { + client.websocket?.let { + val event = TrackExceptionEvent( + guildId = guildId, + track = track, + exception = TrackExceptionEvent.Exception.fromFriendlyException(exception) + ) + + it.send(event) + } } - } - - /** - * Sends a track end player event to the websocket connection, if any. - */ - override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack, reason: AudioTrackEndReason) { - client.websocket?.let { - val event = TrackEndEvent( - track = track, - endReason = reason, - guildId = guildId - ) - - it.send(event) + + /** + * + */ + override fun onTrackStart(player: AudioPlayer?, track: AudioTrack) { + client.websocket?.let { + val event = TrackStartEvent( + guildId = guildId, + track = track + ) + + it.send(event) + } } - } - /** - * - */ - suspend fun destroy() { - updates.stop() - audioPlayer.destroy() - } + /** + * Sends a track end player event to the websocket connection, if any. + */ + override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack, reason: AudioTrackEndReason) { + client.websocket?.let { + val event = TrackEndEvent( + track = track, + endReason = reason, + guildId = guildId + ) + + it.send(event) + } + } - inner class OpusFrameProvider(connection: MediaConnection) : OpusAudioFrameProvider(connection) { + /** + * + */ + suspend fun destroy() { + updates.stop() + audioPlayer.destroy() + } - private val frameBuffer = ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize()) - private val lastFrame = MutableAudioFrame() + inner class OpusFrameProvider(connection: MediaConnection) : OpusAudioFrameProvider(connection) { - init { - lastFrame.setBuffer(frameBuffer) - } + private val frameBuffer = ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize()) + private val lastFrame = MutableAudioFrame() - override fun canProvide(): Boolean { - val success = audioPlayer.provide(lastFrame) - if (!success) { - frameLossTracker.loss() - } + init { + lastFrame.setBuffer(frameBuffer) + } - return success - } + override fun canProvide(): Boolean { + val success = audioPlayer.provide(lastFrame) + if (!success) { + frameLossTracker.loss() + } + + return success + } - override fun retrieveOpusFrame(targetBuffer: ByteBuf) { - frameBuffer.flip() - targetBuffer.writeBytes(frameBuffer) + override fun retrieveOpusFrame(targetBuffer: ByteBuf) { + frameBuffer.flip() + targetBuffer.writeBytes(frameBuffer) + } } - } - companion object { - fun AudioPlayer.addEventListener(listener: AudioEventListener): AudioPlayer { - addListener(listener) - return this + companion object { + fun AudioPlayer.addEventListener(listener: AudioEventListener): AudioPlayer { + addListener(listener) + return this + } } - } } diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index 8569d10..2f3af98 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -17,85 +17,84 @@ package obsidian.server.player import com.sedmelluq.discord.lavaplayer.player.AudioPlayer -import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason import kotlinx.coroutines.launch import obsidian.server.Application.config +import obsidian.server.config.spec.Obsidian import obsidian.server.io.ws.CurrentTrack import obsidian.server.io.ws.PlayerUpdate -import obsidian.server.util.Interval -import obsidian.server.config.spec.Obsidian import obsidian.server.util.CoroutineAudioEventAdapter +import obsidian.server.util.Interval import obsidian.server.util.TrackUtil class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { - /** - * Whether player updates should be sent. - */ - var enabled: Boolean = true - set(value) { - field = value + /** + * Whether player updates should be sent. + */ + var enabled: Boolean = true + set(value) { + field = value - player.client.websocket?.launch { - if (value) start() else stop() - } - } + player.client.websocket?.launch { + if (value) start() else stop() + } + } - private val interval = Interval() + private val interval = Interval() - /** - * Starts sending player updates - */ - suspend fun start() { - if (!interval.started && enabled) { - interval.start(config[Obsidian.playerUpdateInterval], ::sendUpdate) + /** + * Starts sending player updates + */ + suspend fun start() { + if (!interval.started && enabled) { + interval.start(config[Obsidian.playerUpdateInterval], ::sendUpdate) + } } - } - /** - * Stops player updates from being sent - */ - suspend fun stop() { - if (interval.started) { - interval.stop() + /** + * Stops player updates from being sent + */ + suspend fun stop() { + if (interval.started) { + interval.stop() + } } - } - fun sendUpdate() { - player.client.websocket?.let { - val update = PlayerUpdate( - guildId = player.guildId, - currentTrack = currentTrackFor(player), - frames = player.frameLossTracker.payload, - filters = player.filters, - timestamp = System.currentTimeMillis() - ) + fun sendUpdate() { + player.client.websocket?.let { + val update = PlayerUpdate( + guildId = player.guildId, + currentTrack = currentTrackFor(player), + frames = player.frameLossTracker.payload, + filters = player.filters, + timestamp = System.currentTimeMillis() + ) - it.send(update) + it.send(update) + } } - } - override suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) { - start() - } + override suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) { + start() + } - override suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) { - stop() - } + override suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) { + stop() + } - companion object { - /** - * Returns a [CurrentTrack] for the provided [Player]. - * - * @param player - * Player to get the current track from - */ - fun currentTrackFor(player: Player): CurrentTrack = - CurrentTrack( - track = TrackUtil.encode(player.audioPlayer.playingTrack), - paused = player.audioPlayer.isPaused, - position = player.audioPlayer.playingTrack.position - ) - } + companion object { + /** + * Returns a [CurrentTrack] for the provided [Player]. + * + * @param player + * Player to get the current track from + */ + fun currentTrackFor(player: Player): CurrentTrack = + CurrentTrack( + track = TrackUtil.encode(player.audioPlayer.playingTrack), + paused = player.audioPlayer.isPaused, + position = player.audioPlayer.playingTrack.position + ) + } } diff --git a/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt b/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt index de85a9d..f45c161 100644 --- a/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt +++ b/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt @@ -19,9 +19,9 @@ package obsidian.server.player import com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler class TrackEndMarkerHandler(private val player: Player) : TrackMarkerHandler { - override fun handle(state: TrackMarkerHandler.MarkerState) { - if (state == TrackMarkerHandler.MarkerState.REACHED || state == TrackMarkerHandler.MarkerState.BYPASSED) { - player.audioPlayer.stopTrack() + override fun handle(state: TrackMarkerHandler.MarkerState) { + if (state == TrackMarkerHandler.MarkerState.REACHED || state == TrackMarkerHandler.MarkerState.BYPASSED) { + player.audioPlayer.stopTrack() + } } - } } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/Filter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/Filter.kt index 7e2fa88..787e23c 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/Filter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/Filter.kt @@ -21,38 +21,38 @@ import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat import kotlin.math.abs interface Filter { - /** - * Whether this filter is enabled. - */ - val enabled: Boolean - - /** - * Builds this filter's respective [AudioFilter] - * - * @param format The audio data format. - * @param downstream The audio filter used as the downstream. - * - * @return null, if this filter isn't compatible with the provided format or if this filter isn't enabled. - */ - fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? - - companion object { /** - * Minimum absolute difference for floating point values. Values whose difference to the default - * value are smaller than this are considered equal to the default. + * Whether this filter is enabled. */ - const val MINIMUM_FP_DIFF = 0.01f + val enabled: Boolean /** - * Returns true if the difference between [value] and [default] - * is greater or equal to [MINIMUM_FP_DIFF] + * Builds this filter's respective [AudioFilter] * - * @param value The value to check - * @param default Default value. + * @param format The audio data format. + * @param downstream The audio filter used as the downstream. * - * @return true if the difference is greater or equal to the minimum. + * @return null, if this filter isn't compatible with the provided format or if this filter isn't enabled. */ - fun isSet(value: Float, default: Float): Boolean = - abs(value - default) >= MINIMUM_FP_DIFF - } + fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? + + companion object { + /** + * Minimum absolute difference for floating point values. Values whose difference to the default + * value are smaller than this are considered equal to the default. + */ + const val MINIMUM_FP_DIFF = 0.01f + + /** + * Returns true if the difference between [value] and [default] + * is greater or equal to [MINIMUM_FP_DIFF] + * + * @param value The value to check + * @param default Default value. + * + * @return true if the difference is greater or equal to the minimum. + */ + fun isSet(value: Float, default: Float): Boolean = + abs(value - default) >= MINIMUM_FP_DIFF + } } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt b/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt index 2e6d57a..9fd02cb 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/Filters.kt @@ -29,69 +29,69 @@ import obsidian.server.player.filter.impl.* @Serializable data class Filters( - val volume: VolumeFilter? = null, - val equalizer: EqualizerFilter? = null, - val karaoke: KaraokeFilter? = null, - val rotation: RotationFilter? = null, - val tremolo: TremoloFilter? = null, - val vibrato: VibratoFilter? = null, - val distortion: DistortionFilter? = null, - val timescale: TimescaleFilter? = null, - @SerialName("low_pass") - val lowPass: LowPassFilter? = null, - @SerialName("channel_mix") - val channelMix: ChannelMixFilter? = null, + val volume: VolumeFilter? = null, + val equalizer: EqualizerFilter? = null, + val karaoke: KaraokeFilter? = null, + val rotation: RotationFilter? = null, + val tremolo: TremoloFilter? = null, + val vibrato: VibratoFilter? = null, + val distortion: DistortionFilter? = null, + val timescale: TimescaleFilter? = null, + @SerialName("low_pass") + val lowPass: LowPassFilter? = null, + @SerialName("channel_mix") + val channelMix: ChannelMixFilter? = null, ) { - /** - * All filters - */ - val asList: List - get() = listOfNotNull( - volume, - equalizer, - karaoke, - rotation, - tremolo, - vibrato, - distortion, - timescale, - lowPass, - channelMix - ) + /** + * All filters + */ + val asList: List + get() = listOfNotNull( + volume, + equalizer, + karaoke, + rotation, + tremolo, + vibrato, + distortion, + timescale, + lowPass, + channelMix + ) - /** - * List of all enabled filters. - */ - val enabled - get() = asList.filter { - it.enabled + /** + * List of all enabled filters. + */ + val enabled + get() = asList.filter { + it.enabled + } + + /** + * Applies all enabled filters to the audio player declared at [Player.audioPlayer]. + */ + fun applyTo(player: Player) { + val factory = FilterFactory(this) + player.audioPlayer.setFilterFactory(factory) } - /** - * Applies all enabled filters to the audio player declared at [Player.audioPlayer]. - */ - fun applyTo(player: Player) { - val factory = FilterFactory(this) - player.audioPlayer.setFilterFactory(factory) - } + class FilterFactory(private val filters: Filters) : PcmFilterFactory { + override fun buildChain( + track: AudioTrack?, + format: AudioDataFormat, + output: UniversalPcmAudioFilter + ): MutableList { + // dont remove explicit type declaration + val list = buildList { + for (filter in filters.enabled) { + val audioFilter = filter.build(format, lastOrNull() ?: output) + ?: continue - class FilterFactory(private val filters: Filters) : PcmFilterFactory { - override fun buildChain( - track: AudioTrack?, - format: AudioDataFormat, - output: UniversalPcmAudioFilter - ): MutableList { - // dont remove explicit type declaration - val list = buildList { - for (filter in filters.enabled) { - val audioFilter = filter.build(format, lastOrNull() ?: output) - ?: continue + add(audioFilter) + } + } - add(audioFilter) + return list.reversed().toMutableList() } - } - - return list.reversed().toMutableList() } - } } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt index 6e299b0..4589104 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt @@ -24,20 +24,20 @@ import obsidian.server.player.filter.Filter @Serializable data class ChannelMixFilter( - val leftToLeft: Float = 1f, - val leftToRight: Float = 0f, - val rightToRight: Float = 0f, - val rightToLeft: Float = 1f, + val leftToLeft: Float = 1f, + val leftToRight: Float = 0f, + val rightToRight: Float = 0f, + val rightToLeft: Float = 1f, ) : Filter { - override val enabled: Boolean - get() = Filter.isSet(leftToLeft, 1.0f) || Filter.isSet(leftToRight, 0.0f) || - Filter.isSet(rightToLeft, 0.0f) || Filter.isSet(rightToRight, 1.0f); + override val enabled: Boolean + get() = Filter.isSet(leftToLeft, 1.0f) || Filter.isSet(leftToRight, 0.0f) || + Filter.isSet(rightToLeft, 0.0f) || Filter.isSet(rightToRight, 1.0f); - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = - ChannelMixPcmAudioFilter(downstream).also { - it.leftToLeft = leftToLeft - it.leftToRight = leftToRight - it.rightToRight = rightToRight - it.rightToLeft = rightToLeft - } + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = + ChannelMixPcmAudioFilter(downstream).also { + it.leftToLeft = leftToLeft + it.leftToRight = leftToRight + it.rightToRight = rightToRight + it.rightToLeft = rightToLeft + } } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt index 95bbc76..44edb38 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt @@ -24,26 +24,26 @@ import obsidian.server.player.filter.Filter @Serializable data class DistortionFilter( - val sinOffset: Float = 0f, - val sinScale: Float = 1f, - val cosOffset: Float = 0f, - val cosScale: Float = 1f, - val offset: Float = 0f, - val scale: Float = 1f + val sinOffset: Float = 0f, + val sinScale: Float = 1f, + val cosOffset: Float = 0f, + val cosScale: Float = 1f, + val offset: Float = 0f, + val scale: Float = 1f ) : Filter { - override val enabled: Boolean - get() = - (Filter.isSet(sinOffset, 0f) && Filter.isSet(sinScale, 1f)) && - (Filter.isSet(cosOffset, 0f) && Filter.isSet(cosScale, 1f)) && - (Filter.isSet(offset, 0f) && Filter.isSet(scale, 1f)) + override val enabled: Boolean + get() = + (Filter.isSet(sinOffset, 0f) && Filter.isSet(sinScale, 1f)) && + (Filter.isSet(cosOffset, 0f) && Filter.isSet(cosScale, 1f)) && + (Filter.isSet(offset, 0f) && Filter.isSet(scale, 1f)) - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? { - return DistortionPcmAudioFilter(downstream, format.channelCount) - .setSinOffset(sinOffset) - .setSinScale(sinScale) - .setCosOffset(cosOffset) - .setCosScale(cosScale) - .setOffset(offset) - .setScale(scale) - } + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? { + return DistortionPcmAudioFilter(downstream, format.channelCount) + .setSinOffset(sinOffset) + .setSinScale(sinScale) + .setCosOffset(cosOffset) + .setCosScale(cosScale) + .setOffset(offset) + .setScale(scale) + } } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt index d1e626f..08cbd34 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/EqualizerFilter.kt @@ -19,29 +19,29 @@ package obsidian.server.player.filter.impl import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter import com.sedmelluq.discord.lavaplayer.filter.equalizer.Equalizer import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat -import obsidian.server.player.filter.Filter import kotlinx.serialization.Serializable +import obsidian.server.player.filter.Filter @Serializable @JvmInline value class EqualizerFilter(val bands: List) : Filter { - override val enabled: Boolean - get() = bands.any { - Filter.isSet(it.gain, 0f) - } + override val enabled: Boolean + get() = bands.any { + Filter.isSet(it.gain, 0f) + } - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? { - if (!Equalizer.isCompatible(format)) { - return null - } + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? { + if (!Equalizer.isCompatible(format)) { + return null + } - val bands = FloatArray(15) { band -> - bands.find { it.band == band }?.gain ?: 0f - } + val bands = FloatArray(15) { band -> + bands.find { it.band == band }?.gain ?: 0f + } - return Equalizer(format.channelCount, downstream, bands) - } + return Equalizer(format.channelCount, downstream, bands) + } - @Serializable - data class Band(val band: Int, val gain: Float) + @Serializable + data class Band(val band: Int, val gain: Float) } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/KaraokeFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/KaraokeFilter.kt index 7217c64..52171f9 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/KaraokeFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/KaraokeFilter.kt @@ -25,22 +25,22 @@ import obsidian.server.player.filter.Filter @Serializable data class KaraokeFilter( - val level: Float, - @SerialName("mono_level") - val monoLevel: Float, - @SerialName("filter_band") - val filterBand: Float, - @SerialName("filter_width") - val filterWidth: Float, + val level: Float, + @SerialName("mono_level") + val monoLevel: Float, + @SerialName("filter_band") + val filterBand: Float, + @SerialName("filter_width") + val filterWidth: Float, ) : Filter { - override val enabled: Boolean - get() = Filter.isSet(level, 1f) || Filter.isSet(monoLevel, 1f) - || Filter.isSet(filterBand, 220f) || Filter.isSet(filterWidth, 100f) + override val enabled: Boolean + get() = Filter.isSet(level, 1f) || Filter.isSet(monoLevel, 1f) + || Filter.isSet(filterBand, 220f) || Filter.isSet(filterWidth, 100f) - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = - KaraokePcmAudioFilter(downstream, format.channelCount, format.sampleRate) - .setLevel(level) - .setMonoLevel(monoLevel) - .setFilterBand(filterBand) - .setFilterWidth(filterWidth) + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = + KaraokePcmAudioFilter(downstream, format.channelCount, format.sampleRate) + .setLevel(level) + .setMonoLevel(monoLevel) + .setFilterBand(filterBand) + .setFilterWidth(filterWidth) } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt index 81d4bdd..9a7ae2b 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/LowPassFilter.kt @@ -25,10 +25,10 @@ import obsidian.server.player.filter.Filter @Serializable @JvmInline value class LowPassFilter(val smoothing: Float = 20f) : Filter { - override val enabled: Boolean - get() = Filter.isSet(smoothing, 20f) + override val enabled: Boolean + get() = Filter.isSet(smoothing, 20f) - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = - LowPassPcmAudioFilter(downstream, format.channelCount, 0) - .setSmoothing(smoothing) + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = + LowPassPcmAudioFilter(downstream, format.channelCount, 0) + .setSmoothing(smoothing) } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt index 625e9a8..829dca7 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/RotationFilter.kt @@ -19,17 +19,16 @@ package obsidian.server.player.filter.impl import com.github.natanbc.lavadsp.rotation.RotationPcmAudioFilter import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter @Serializable @JvmInline value class RotationFilter(val rotationHz: Float = 5f) : Filter { - override val enabled: Boolean - get() = Filter.isSet(rotationHz, 5f) + override val enabled: Boolean + get() = Filter.isSet(rotationHz, 5f) - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = - RotationPcmAudioFilter(downstream, format.sampleRate) - .setRotationSpeed(rotationHz.toDouble() /* seems like a bad idea idk. */) + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = + RotationPcmAudioFilter(downstream, format.sampleRate) + .setRotationSpeed(rotationHz.toDouble() /* seems like a bad idea idk. */) } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt index d893450..0420c6b 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/TimescaleFilter.kt @@ -27,71 +27,71 @@ import obsidian.server.util.NativeUtil @Serializable data class TimescaleFilter( - val pitch: Float = 1f, - @SerialName("pitch_octaves") - val pitchOctaves: Float? = null, - @SerialName("pitch_semi_tones") - val pitchSemiTones: Float? = null, - val speed: Float = 1f, - @SerialName("speed_change") - val speedChange: Float? = null, - val rate: Float = 1f, - @SerialName("rate_change") - val rateChange: Float? = null, + val pitch: Float = 1f, + @SerialName("pitch_octaves") + val pitchOctaves: Float? = null, + @SerialName("pitch_semi_tones") + val pitchSemiTones: Float? = null, + val speed: Float = 1f, + @SerialName("speed_change") + val speedChange: Float? = null, + val rate: Float = 1f, + @SerialName("rate_change") + val rateChange: Float? = null, ) : Filter { - override val enabled: Boolean - get() = - NativeUtil.timescaleAvailable - && (isSet(pitch, 1f) - || isSet(speed, 1f) - || isSet(rate, 1f)) + override val enabled: Boolean + get() = + NativeUtil.timescaleAvailable + && (isSet(pitch, 1f) + || isSet(speed, 1f) + || isSet(rate, 1f)) - init { - require(speed > 0) { - "'speed' must be greater than 0" - } + init { + require(speed > 0) { + "'speed' must be greater than 0" + } - require(rate > 0) { - "'rate' must be greater than 0" - } + require(rate > 0) { + "'rate' must be greater than 0" + } - require(pitch > 0) { - "'pitch' must be greater than 0" - } + require(pitch > 0) { + "'pitch' must be greater than 0" + } - if (pitchOctaves != null) { - require(!isSet(pitch, 1.0F) && pitchSemiTones == null) { - "'pitch_octaves' cannot be used in conjunction with 'pitch' and 'pitch_semi_tones'" - } - } + if (pitchOctaves != null) { + require(!isSet(pitch, 1.0F) && pitchSemiTones == null) { + "'pitch_octaves' cannot be used in conjunction with 'pitch' and 'pitch_semi_tones'" + } + } - if (pitchSemiTones != null) { - require(!isSet(pitch, 1.0F) && pitchOctaves == null) { - "'pitch_semi_tones' cannot be used in conjunction with 'pitch' and 'pitch_octaves'" - } - } + if (pitchSemiTones != null) { + require(!isSet(pitch, 1.0F) && pitchOctaves == null) { + "'pitch_semi_tones' cannot be used in conjunction with 'pitch' and 'pitch_octaves'" + } + } - if (speedChange != null) { - require(!isSet(speed, 1.0F)) { - "'speed_change' cannot be used in conjunction with 'speed'" - } - } + if (speedChange != null) { + require(!isSet(speed, 1.0F)) { + "'speed_change' cannot be used in conjunction with 'speed'" + } + } - if (rateChange != null) { - require(!isSet(rate, 1.0F)) { - "'rate_change' cannot be used in conjunction with 'rate'" - } + if (rateChange != null) { + require(!isSet(rate, 1.0F)) { + "'rate_change' cannot be used in conjunction with 'rate'" + } + } } - } - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = - TimescalePcmAudioFilter(downstream, format.channelCount, format.sampleRate).also { af -> - af.pitch = pitch.toDouble() - af.rate = rate.toDouble() - af.speed = speed.toDouble() - this.pitchOctaves?.let { af.setPitchOctaves(it.toDouble()) } - this.pitchSemiTones?.let { af.setPitchSemiTones(it.toDouble()) } - this.speedChange?.let { af.setSpeedChange(it.toDouble()) } - this.rateChange?.let { af.setRateChange(it.toDouble()) } - } + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = + TimescalePcmAudioFilter(downstream, format.channelCount, format.sampleRate).also { af -> + af.pitch = pitch.toDouble() + af.rate = rate.toDouble() + af.speed = speed.toDouble() + this.pitchOctaves?.let { af.setPitchOctaves(it.toDouble()) } + this.pitchSemiTones?.let { af.setPitchSemiTones(it.toDouble()) } + this.speedChange?.let { af.setSpeedChange(it.toDouble()) } + this.rateChange?.let { af.setRateChange(it.toDouble()) } + } } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/TremoloFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/TremoloFilter.kt index a1375b4..dabb934 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/TremoloFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/TremoloFilter.kt @@ -25,24 +25,24 @@ import obsidian.server.player.filter.Filter.Companion.isSet @Serializable data class TremoloFilter( - val frequency: Float = 2f, - val depth: Float = 0f + val frequency: Float = 2f, + val depth: Float = 0f ) : Filter { - override val enabled: Boolean - get() = isSet(frequency, 2f) || isSet(depth, 0.5f); + override val enabled: Boolean + get() = isSet(frequency, 2f) || isSet(depth, 0.5f); - init { - require(depth <= 1 && depth > 0) { - "'depth' must be greater than 0 and less than 1" - } + init { + require(depth <= 1 && depth > 0) { + "'depth' must be greater than 0 and less than 1" + } - require(frequency > 0) { - "'frequency' must be greater than 0" + require(frequency > 0) { + "'frequency' must be greater than 0" + } } - } - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = - TremoloPcmAudioFilter(downstream, format.channelCount, format.sampleRate) - .setDepth(depth) - .setFrequency(frequency) + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = + TremoloPcmAudioFilter(downstream, format.channelCount, format.sampleRate) + .setDepth(depth) + .setFrequency(frequency) } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VibratoFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VibratoFilter.kt index d4c0409..9000d61 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VibratoFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VibratoFilter.kt @@ -24,28 +24,28 @@ import obsidian.server.player.filter.Filter @Serializable data class VibratoFilter( - val frequency: Float = 2f, - val depth: Float = .5f + val frequency: Float = 2f, + val depth: Float = .5f ) : Filter { - override val enabled: Boolean - get() = Filter.isSet(frequency, 2f) || Filter.isSet(depth, 0.5f) + override val enabled: Boolean + get() = Filter.isSet(frequency, 2f) || Filter.isSet(depth, 0.5f) - init { - require(depth > 0 && depth < 1) { - "'depth' must be greater than 0 and less than 1." - } + init { + require(depth > 0 && depth < 1) { + "'depth' must be greater than 0 and less than 1." + } - require(frequency > 0 && frequency < VIBRATO_FREQUENCY_MAX_HZ) { - "'frequency' must be greater than 0 and less than $VIBRATO_FREQUENCY_MAX_HZ" + require(frequency > 0 && frequency < VIBRATO_FREQUENCY_MAX_HZ) { + "'frequency' must be greater than 0 and less than $VIBRATO_FREQUENCY_MAX_HZ" + } } - } - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = - VibratoPcmAudioFilter(downstream, format.channelCount, format.sampleRate) - .setFrequency(frequency) - .setDepth(depth) + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter = + VibratoPcmAudioFilter(downstream, format.channelCount, format.sampleRate) + .setFrequency(frequency) + .setDepth(depth) - companion object { - private const val VIBRATO_FREQUENCY_MAX_HZ = 14f - } + companion object { + private const val VIBRATO_FREQUENCY_MAX_HZ = 14f + } } diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt index d546bdc..696f4a7 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/VolumeFilter.kt @@ -25,15 +25,15 @@ import obsidian.server.player.filter.Filter @Serializable @JvmInline value class VolumeFilter(val volume: Float) : Filter { - override val enabled: Boolean - get() = Filter.isSet(volume, 1f) + override val enabled: Boolean + get() = Filter.isSet(volume, 1f) - init { - require(volume in 0.0..5.0) { - "'volume' must be 0 <= x <= 5" + init { + require(volume in 0.0..5.0) { + "'volume' must be 0 <= x <= 5" + } } - } - override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? = - VolumePcmAudioFilter(downstream).setVolume(volume) + override fun build(format: AudioDataFormat, downstream: FloatPcmAudioFilter): FloatPcmAudioFilter? = + VolumePcmAudioFilter(downstream).setVolume(volume) } diff --git a/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt b/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt index a76c5fe..e635f8f 100644 --- a/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt +++ b/Server/src/main/kotlin/obsidian/server/util/AuthorizationPipeline.kt @@ -26,30 +26,30 @@ import io.ktor.util.pipeline.* import obsidian.server.config.spec.Obsidian object AuthorizationPipeline { - /** - * The interceptor for use in [obsidianProvider] - */ - val interceptor: PipelineInterceptor = { ctx -> - val authorization = call.request.authorization() - ?: call.request.queryParameters["auth"] + /** + * The interceptor for use in [obsidianProvider] + */ + val interceptor: PipelineInterceptor = { ctx -> + val authorization = call.request.authorization() + ?: call.request.queryParameters["auth"] - if (!Obsidian.Server.validateAuth(authorization)) { - val cause = when (authorization) { - null -> AuthenticationFailedCause.NoCredentials - else -> AuthenticationFailedCause.InvalidCredentials - } + if (!Obsidian.Server.validateAuth(authorization)) { + val cause = when (authorization) { + null -> AuthenticationFailedCause.NoCredentials + else -> AuthenticationFailedCause.InvalidCredentials + } - ctx.challenge("ObsidianAuth", cause) { - call.respond(HttpStatusCode.Unauthorized) - it.complete() - } + ctx.challenge("ObsidianAuth", cause) { + call.respond(HttpStatusCode.Unauthorized) + it.complete() + } + } } - } - /** - * Adds an authentication provider used by Obsidian. - */ - fun Authentication.Configuration.obsidianProvider() = provider { - pipeline.intercept(RequestAuthentication, interceptor) - } + /** + * Adds an authentication provider used by Obsidian. + */ + fun Authentication.Configuration.obsidianProvider() = provider { + pipeline.intercept(RequestAuthentication, interceptor) + } } diff --git a/Server/src/main/kotlin/obsidian/server/util/ByteRingBuffer.kt b/Server/src/main/kotlin/obsidian/server/util/ByteRingBuffer.kt index b612029..27161e7 100644 --- a/Server/src/main/kotlin/obsidian/server/util/ByteRingBuffer.kt +++ b/Server/src/main/kotlin/obsidian/server/util/ByteRingBuffer.kt @@ -24,117 +24,117 @@ import kotlin.math.min */ class ByteRingBuffer(private var size: Int) : Iterable { - private val arr: ByteArray - private var pos = 0 + private val arr: ByteArray + private var pos = 0 - init { - require(size >= 1) { - "Size must be greater or equal to 1." + init { + require(size >= 1) { + "Size must be greater or equal to 1." + } + + arr = ByteArray(size) + } + + /** + * Returns the size of this buffer + */ + fun size() = size + + /** + * Stores a value in this buffer. If the buffer is full, the oldest value is removed. + * + * @param value Value to store + */ + fun put(value: Byte) { + arr[pos] = value + pos = (pos + 1) % arr.size + size = min(size + 1, arr.size) + } + + /** + * Clears this buffer + */ + fun clear() { + pos = 0 + size = 0 + arr.fill(0) } - arr = ByteArray(size) - } - - /** - * Returns the size of this buffer - */ - fun size() = size - - /** - * Stores a value in this buffer. If the buffer is full, the oldest value is removed. - * - * @param value Value to store - */ - fun put(value: Byte) { - arr[pos] = value - pos = (pos + 1) % arr.size - size = min(size + 1, arr.size) - } - - /** - * Clears this buffer - */ - fun clear() { - pos = 0 - size = 0 - arr.fill(0) - } - - /** - * Returns the sum of all values in this buffer. - */ - fun sum(): Int { - var sum = 0 - for (v in arr) sum += v - return sum - } - - /** - * Returns the [n]th element of this buffer. - * An index of 0 returns the oldest, - * an index of `size() - 1` returns the newest - * - * @param n Index of the wanted element - * - * @throws NoSuchElementException If [n] >= [size] - */ - fun get(n: Int): Byte { - if (n >= size) { - throw NoSuchElementException() + /** + * Returns the sum of all values in this buffer. + */ + fun sum(): Int { + var sum = 0 + for (v in arr) sum += v + return sum } - return arr[sub(pos, size - n, arr.size)] - } - - /** - * Returns the last element of this buffer. - * Equivalent to `getLast(0)` - * - * @throws NoSuchElementException If this buffer is empty. - */ - fun getLast() = getLast(0) - - /** - * Returns the [n]th last element of this buffer. - * An index of 0 returns the newest, - * an index of `size() - 1` returns the oldest. - * - * @param n Index of the wanted element. - * - * @throws NoSuchElementException If [n] >= [size] - */ - fun getLast(n: Int): Byte { - if (n >= size) { - throw NoSuchElementException() + /** + * Returns the [n]th element of this buffer. + * An index of 0 returns the oldest, + * an index of `size() - 1` returns the newest + * + * @param n Index of the wanted element + * + * @throws NoSuchElementException If [n] >= [size] + */ + fun get(n: Int): Byte { + if (n >= size) { + throw NoSuchElementException() + } + + return arr[sub(pos, size - n, arr.size)] } - return arr[sub(pos, n + 1, arr.size)] - } + /** + * Returns the last element of this buffer. + * Equivalent to `getLast(0)` + * + * @throws NoSuchElementException If this buffer is empty. + */ + fun getLast() = getLast(0) + + /** + * Returns the [n]th last element of this buffer. + * An index of 0 returns the newest, + * an index of `size() - 1` returns the oldest. + * + * @param n Index of the wanted element. + * + * @throws NoSuchElementException If [n] >= [size] + */ + fun getLast(n: Int): Byte { + if (n >= size) { + throw NoSuchElementException() + } + + return arr[sub(pos, n + 1, arr.size)] + } - override fun iterator(): Iterator = - object : Iterator { - var cursor = pos - var remaining = size + override fun iterator(): Iterator = + object : Iterator { + var cursor = pos + var remaining = size - override fun hasNext(): Boolean = - remaining > 0 + override fun hasNext(): Boolean = + remaining > 0 - override fun next(): Byte { - val v = arr[cursor] - cursor = inc(cursor, arr.size) - remaining-- + override fun next(): Byte { + val v = arr[cursor] + cursor = inc(cursor, arr.size) + remaining-- - return v - } - } + return v + } + } - companion object { - fun sub(i: Int, j: Int, mod: Int) = - (i - j).takeIf { mod < it } - ?: 0 + companion object { + fun sub(i: Int, j: Int, mod: Int) = + (i - j).takeIf { mod < it } + ?: 0 - fun inc(i: Int, mod: Int): Int = - i.inc().takeIf { mod < it } - ?: 0 - } + fun inc(i: Int, mod: Int): Int = + i.inc().takeIf { mod < it } + ?: 0 + } } diff --git a/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt index cbc759a..f656e41 100644 --- a/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt +++ b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt @@ -25,34 +25,34 @@ import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext open class CoroutineAudioEventAdapter(private val dispatcher: CoroutineDispatcher = Dispatchers.Default) : - AudioEventListener, - CoroutineScope { - - override val coroutineContext: CoroutineContext - get() = dispatcher + SupervisorJob() - - /* playback start/end */ - open suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) = Unit - open suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) = Unit - - /* exception */ - open suspend fun onTrackStuck(thresholdMs: Long, track: AudioTrack, player: AudioPlayer) = Unit - open suspend fun onTrackException(exception: FriendlyException, track: AudioTrack, player: AudioPlayer) = Unit - - /* playback state */ - open suspend fun onPlayerResume(player: AudioPlayer) = Unit - open suspend fun onPlayerPause(player: AudioPlayer) = Unit - - override fun onEvent(event: AudioEvent) { - launch { - when (event) { - is TrackStartEvent -> onTrackStart(event.track, event.player) - is TrackEndEvent -> onTrackEnd(event.track, event.endReason, event.player) - is TrackStuckEvent -> onTrackStuck(event.thresholdMs, event.track, event.player) - is TrackExceptionEvent -> onTrackException(event.exception, event.track, event.player) - is PlayerResumeEvent -> onPlayerResume(event.player) - is PlayerPauseEvent -> onPlayerPause(event.player) - } + AudioEventListener, + CoroutineScope { + + override val coroutineContext: CoroutineContext + get() = dispatcher + SupervisorJob() + + /* playback start/end */ + open suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) = Unit + open suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) = Unit + + /* exception */ + open suspend fun onTrackStuck(thresholdMs: Long, track: AudioTrack, player: AudioPlayer) = Unit + open suspend fun onTrackException(exception: FriendlyException, track: AudioTrack, player: AudioPlayer) = Unit + + /* playback state */ + open suspend fun onPlayerResume(player: AudioPlayer) = Unit + open suspend fun onPlayerPause(player: AudioPlayer) = Unit + + override fun onEvent(event: AudioEvent) { + launch { + when (event) { + is TrackStartEvent -> onTrackStart(event.track, event.player) + is TrackEndEvent -> onTrackEnd(event.track, event.endReason, event.player) + is TrackStuckEvent -> onTrackStuck(event.thresholdMs, event.track, event.player) + is TrackExceptionEvent -> onTrackException(event.exception, event.track, event.player) + is PlayerResumeEvent -> onPlayerResume(event.player) + is PlayerPauseEvent -> onPlayerPause(event.player) + } + } } - } } diff --git a/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt b/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt index 7b5efe3..844024c 100644 --- a/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt +++ b/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt @@ -23,82 +23,82 @@ import java.lang.management.OperatingSystemMXBean import java.lang.reflect.Method class CpuTimer { - val processRecentCpuUsage: Double - get() = try { - //between 0.0 and 1.0, 1.0 meaning all CPU cores were running threads of this JVM - // 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() - val cpuLoad = callDoubleGetter("getProcessCpuLoad", osBean) - cpuLoad ?: ERROR - } catch (ex: Throwable) { - logger.debug("Couldn't access process cpu time", ex) - ERROR - } - - val systemRecentCpuUsage: Double - get() = try { - //between 0.0 and 1.0, 1.0 meaning all CPU cores were running threads of this JVM - // 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() - val cpuLoad = callDoubleGetter("getSystemCpuLoad", osBean) - cpuLoad ?: ERROR - } catch (ex: Throwable) { - logger.debug("Couldn't access system cpu time", ex) - ERROR - } + val processRecentCpuUsage: Double + get() = try { + //between 0.0 and 1.0, 1.0 meaning all CPU cores were running threads of this JVM + // 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() + val cpuLoad = callDoubleGetter("getProcessCpuLoad", osBean) + cpuLoad ?: ERROR + } catch (ex: Throwable) { + logger.debug("Couldn't access process cpu time", ex) + ERROR + } + val systemRecentCpuUsage: Double + get() = try { + //between 0.0 and 1.0, 1.0 meaning all CPU cores were running threads of this JVM + // 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() + val cpuLoad = callDoubleGetter("getSystemCpuLoad", osBean) + cpuLoad ?: ERROR + } catch (ex: Throwable) { + logger.debug("Couldn't access system cpu time", ex) + ERROR + } - /** - * The operating system bean used to get statistics. - */ - private val osBean: OperatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean() - // Code below copied from Prometheus's StandardExports (Apache 2.0) with slight modifications - private fun callDoubleGetter(getterName: String, obj: Any): Double? { - return callDoubleGetter(obj.javaClass.getMethod(getterName), obj) - } + /** + * The operating system bean used to get statistics. + */ + private val osBean: OperatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean() - /** - * Attempts to call a method either directly or via one of the implemented interfaces. - * - * - * A Method object refers to a specific method declared in a specific class. The first invocation - * might happen with method == SomeConcreteClass.publicLongGetter() and will fail if - * SomeConcreteClass is not public. We then recurse over all interfaces implemented by - * SomeConcreteClass (or extended by those interfaces and so on) until we eventually invoke - * callMethod() with method == SomePublicInterface.publicLongGetter(), which will then succeed. - * - * - * There is a built-in assumption that the method will never return null (or, equivalently, that - * it returns the primitive data type, i.e. `long` rather than `Long`). If this - * assumption doesn't hold, the method might be called repeatedly and the returned value will be - * the one produced by the last call. - */ - private fun callDoubleGetter(method: Method, obj: Any): Double? { - try { - return method.invoke(obj) as Double - } catch (e: IllegalAccessException) { - // Expected, the declaring class or interface might not be public. + // Code below copied from Prometheus's StandardExports (Apache 2.0) with slight modifications + private fun callDoubleGetter(getterName: String, obj: Any): Double? { + return callDoubleGetter(obj.javaClass.getMethod(getterName), obj) } - // Iterate over all implemented/extended interfaces and attempt invoking the method with the - // same name and parameters on each. - for (clazz in method.declaringClass.interfaces) { - try { - val interfaceMethod: Method = clazz.getMethod(method.name, * method.parameterTypes) - val result = callDoubleGetter(interfaceMethod, obj) + /** + * Attempts to call a method either directly or via one of the implemented interfaces. + * + * + * A Method object refers to a specific method declared in a specific class. The first invocation + * might happen with method == SomeConcreteClass.publicLongGetter() and will fail if + * SomeConcreteClass is not public. We then recurse over all interfaces implemented by + * SomeConcreteClass (or extended by those interfaces and so on) until we eventually invoke + * callMethod() with method == SomePublicInterface.publicLongGetter(), which will then succeed. + * + * + * There is a built-in assumption that the method will never return null (or, equivalently, that + * it returns the primitive data type, i.e. `long` rather than `Long`). If this + * assumption doesn't hold, the method might be called repeatedly and the returned value will be + * the one produced by the last call. + */ + private fun callDoubleGetter(method: Method, obj: Any): Double? { + try { + return method.invoke(obj) as Double + } catch (e: IllegalAccessException) { + // Expected, the declaring class or interface might not be public. + } + + // Iterate over all implemented/extended interfaces and attempt invoking the method with the + // same name and parameters on each. + for (clazz in method.declaringClass.interfaces) { + try { + val interfaceMethod: Method = clazz.getMethod(method.name, * method.parameterTypes) + val result = callDoubleGetter(interfaceMethod, obj) - if (result != null) { - return result + if (result != null) { + return result + } + } catch (e: NoSuchMethodException) { + // Expected, class might implement multiple, unrelated interfaces. + } } - } catch (e: NoSuchMethodException) { - // Expected, class might implement multiple, unrelated interfaces. - } - } - return null - } + return null + } - companion object { - private const val ERROR = -1.0 - private val logger: Logger = LoggerFactory.getLogger(CpuTimer::class.java) - } + companion object { + private const val ERROR = -1.0 + private val logger: Logger = LoggerFactory.getLogger(CpuTimer::class.java) + } } diff --git a/Server/src/main/kotlin/obsidian/server/util/Interval.kt b/Server/src/main/kotlin/obsidian/server/util/Interval.kt index eaaccb0..b7b1f3d 100644 --- a/Server/src/main/kotlin/obsidian/server/util/Interval.kt +++ b/Server/src/main/kotlin/obsidian/server/util/Interval.kt @@ -32,64 +32,64 @@ import kotlin.coroutines.CoroutineContext * @param dispatcher The dispatchers the events will be fired on. */ class Interval(private val dispatcher: CoroutineDispatcher = Dispatchers.Default) : CoroutineScope { - /** - * The coroutine context. - */ - override val coroutineContext: CoroutineContext - get() = dispatcher + Job() + /** + * The coroutine context. + */ + override val coroutineContext: CoroutineContext + get() = dispatcher + Job() - /** - * Whether this interval has been started. - */ - var started: Boolean = false - private set + /** + * Whether this interval has been started. + */ + var started: Boolean = false + private set - /** - * The mutex. - */ - private val mutex = Mutex() + /** + * The mutex. + */ + private val mutex = Mutex() - /** - * The kotlin ticker. - */ - private var ticker: ReceiveChannel? = null + /** + * The kotlin ticker. + */ + private var ticker: ReceiveChannel? = null - /** - * Executes the provided [block] every [delay] milliseconds. - * - * @param delay The delay (in milliseconds) between every execution - * @param block The block to execute. - */ - suspend fun start(delay: Long, block: suspend () -> Unit) { - coroutineScope { - stop() - mutex.withLock { - ticker = ticker(delay) - launch { - started = true - ticker?.consumeEach { - try { - block() - } catch (exception: Exception) { - logger.error("Ran into an exception.", exception) + /** + * Executes the provided [block] every [delay] milliseconds. + * + * @param delay The delay (in milliseconds) between every execution + * @param block The block to execute. + */ + suspend fun start(delay: Long, block: suspend () -> Unit) { + coroutineScope { + stop() + mutex.withLock { + ticker = ticker(delay) + launch { + started = true + ticker?.consumeEach { + try { + block() + } catch (exception: Exception) { + logger.error("Ran into an exception.", exception) + } + } + } } - } } - } } - } - /** - * Stops the this interval. - */ - suspend fun stop() { - mutex.withLock { - ticker?.cancel() - started = false + /** + * Stops the this interval. + */ + suspend fun stop() { + mutex.withLock { + ticker?.cancel() + started = false + } } - } - companion object { - private val logger: Logger = LoggerFactory.getLogger(Interval::class.java) - } + companion object { + private val logger: Logger = LoggerFactory.getLogger(Interval::class.java) + } } diff --git a/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt index b140d31..1122be0 100644 --- a/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt @@ -32,69 +32,74 @@ import org.slf4j.LoggerFactory object KoeUtil { - private val log: Logger = LoggerFactory.getLogger(KoeUtil::class.java) + private val log: Logger = LoggerFactory.getLogger(KoeUtil::class.java) - /** - * The koe instance - */ - val koe by lazy { - val options = KoeOptions.builder() - options.setFramePollerFactory(framePollerFactory) - options.setByteBufAllocator(allocator) - options.setGatewayVersion(gatewayVersion) - options.setHighPacketPriority(config[Obsidian.Koe.highPacketPriority]) + /** + * The koe instance + */ + val koe by lazy { + val options = KoeOptions.builder() + options.setFramePollerFactory(framePollerFactory) + options.setByteBufAllocator(allocator) + options.setGatewayVersion(gatewayVersion) + options.setHighPacketPriority(config[Obsidian.Koe.highPacketPriority]) - Koe.koe(options.create()) - } + Koe.koe(options.create()) + } - /** - * Gateway version to use - */ - private val gatewayVersion: GatewayVersion by lazy { - when (config[Obsidian.Koe.gatewayVersion]) { - 5 -> GatewayVersion.V5 - 4 -> GatewayVersion.V4 - else -> { - log.info("Invalid gateway version, defaulting to v5.") - GatewayVersion.V5 - } + /** + * Gateway version to use + */ + private val gatewayVersion: GatewayVersion by lazy { + when (config[Obsidian.Koe.gatewayVersion]) { + 5 -> GatewayVersion.V5 + 4 -> GatewayVersion.V4 + else -> { + log.info("Invalid gateway version, defaulting to v5.") + GatewayVersion.V5 + } + } } - } - /** - * The frame poller to use. - */ - private val framePollerFactory: FramePollerFactory by lazy { - when { - NativeUtil.udpQueueAvailable && config[Obsidian.Koe.UdpQueue.enabled] -> { - log.info("Enabling udp-queue") - UdpQueueFramePollerFactory(config[Obsidian.Koe.UdpQueue.bufferDuration], config[Obsidian.Koe.UdpQueue.poolSize]) - } + /** + * The frame poller to use. + */ + private val framePollerFactory: FramePollerFactory by lazy { + when { + NativeUtil.udpQueueAvailable && config[Obsidian.Koe.UdpQueue.enabled] -> { + log.info("Enabling udp-queue") + UdpQueueFramePollerFactory( + config[Obsidian.Koe.UdpQueue.bufferDuration], + config[Obsidian.Koe.UdpQueue.poolSize] + ) + } - else -> { - if (config[Obsidian.Koe.UdpQueue.enabled]) { - log.warn("This system and/or architecture appears to not support native audio sending, " - + "GC pauses may cause your bot to stutter during playback.") - } + else -> { + if (config[Obsidian.Koe.UdpQueue.enabled]) { + log.warn( + "This system and/or architecture appears to not support native audio sending, " + + "GC pauses may cause your bot to stutter during playback." + ) + } - NettyFramePollerFactory() - } + NettyFramePollerFactory() + } + } } - } - /** - * The byte-buf allocator to use - */ - private val allocator: ByteBufAllocator by lazy { - when (val configured = config[Obsidian.Koe.byteAllocator]) { - "pooled", "default" -> PooledByteBufAllocator.DEFAULT - "netty-default" -> ByteBufAllocator.DEFAULT - "unpooled" -> UnpooledByteBufAllocator.DEFAULT - else -> { - log.warn("Unknown byte-buf allocator '${configured}', defaulting to 'pooled'.") - PooledByteBufAllocator.DEFAULT - } + /** + * The byte-buf allocator to use + */ + private val allocator: ByteBufAllocator by lazy { + when (val configured = config[Obsidian.Koe.byteAllocator]) { + "pooled", "default" -> PooledByteBufAllocator.DEFAULT + "netty-default" -> ByteBufAllocator.DEFAULT + "unpooled" -> UnpooledByteBufAllocator.DEFAULT + else -> { + log.warn("Unknown byte-buf allocator '${configured}', defaulting to 'pooled'.") + PooledByteBufAllocator.DEFAULT + } + } } - } } diff --git a/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt b/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt index 64ab309..743260b 100644 --- a/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt +++ b/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt @@ -20,39 +20,41 @@ import ch.qos.logback.classic.Level import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.pattern.CompositeConverter -fun interface Convert { fun take(str: String): String } +fun interface Convert { + fun take(str: String): String +} class LogbackColorConverter : CompositeConverter() { - override fun transform(event: ILoggingEvent, element: String): String { - val option = ANSI_COLORS[firstOption] - ?: ANSI_COLORS[LEVELS[event.level.toInt()]] - ?: ANSI_COLORS["green"] - - return option!!.take(element) - } - - companion object { - val Number.ansi: String - get() = "\u001b[${this}m" - - private val ANSI_COLORS = mutableMapOf( - "gray" to Convert { t -> "${90.ansi}$t${39.ansi}" }, - "faint" to Convert { t -> "${2.ansi}$t${22.ansi}" } - ) - - init { - val names = listOf("red", "green", "yellow", "blue", "magenta", "cyan") - for ((idx, code) in (31..36).withIndex()) { - ANSI_COLORS[names[idx]] = Convert { t -> "${code.ansi}$t${39.ansi}" } - } + override fun transform(event: ILoggingEvent, element: String): String { + val option = ANSI_COLORS[firstOption] + ?: ANSI_COLORS[LEVELS[event.level.toInt()]] + ?: ANSI_COLORS["green"] + + return option!!.take(element) } - private val LEVELS = mapOf( - Level.ERROR_INTEGER to "red", - Level.WARN_INTEGER to "yellow", - Level.DEBUG_INTEGER to "blue", - Level.INFO_INTEGER to "faint", - Level.TRACE_INTEGER to "magenta" - ) - } + companion object { + val Number.ansi: String + get() = "\u001b[${this}m" + + private val ANSI_COLORS = mutableMapOf( + "gray" to Convert { t -> "${90.ansi}$t${39.ansi}" }, + "faint" to Convert { t -> "${2.ansi}$t${22.ansi}" } + ) + + init { + val names = listOf("red", "green", "yellow", "blue", "magenta", "cyan") + for ((idx, code) in (31..36).withIndex()) { + ANSI_COLORS[names[idx]] = Convert { t -> "${code.ansi}$t${39.ansi}" } + } + } + + private val LEVELS = mapOf( + Level.ERROR_INTEGER to "red", + Level.WARN_INTEGER to "yellow", + Level.DEBUG_INTEGER to "blue", + Level.INFO_INTEGER to "faint", + Level.TRACE_INTEGER to "magenta" + ) + } } diff --git a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt index fe49d29..9abafbb 100644 --- a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt @@ -30,96 +30,96 @@ import org.slf4j.LoggerFactory */ object NativeUtil { - var timescaleAvailable: Boolean = false - var udpQueueAvailable: Boolean = false - - /* private shit */ - private val logger: Logger = LoggerFactory.getLogger(NativeUtil::class.java) - - // loaders - private val CONNECTOR_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "connector") - private val UDP_QUEUE_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "udpqueue") - - // class names - private const val LOAD_RESULT_NAME = "com.sedmelluq.lava.common.natives.NativeLibraryLoader\$LoadResult" - - private var LOAD_RESULT: Any? = try { - val ctor = Class.forName(LOAD_RESULT_NAME) - .getDeclaredConstructor(Boolean::class.javaPrimitiveType, RuntimeException::class.java) - - ctor.isAccessible = true - ctor.newInstance(true, null) - } catch (e: ReflectiveOperationException) { - logger.error("Unable to create successful load result"); - null; - } - - /** - * Loads native library shit - */ - fun load() { - loadConnector() - udpQueueAvailable = loadUdpQueue() - timescaleAvailable = loadTimescale() - } - - /** - * Loads the timescale libraries - */ - fun loadTimescale(): Boolean = try { - TimescaleNativeLibLoader.loadTimescaleLibrary() - logger.info("Timescale loaded") - true - } catch (ex: Exception) { - logger.warn("Timescale failed to load", ex) - false - } - - /** - * Loads the lp-cross version of lavaplayer's loader - */ - private fun loadConnector() { - try { - CONNECTOR_LOADER.load() - - val loadersField = ConnectorNativeLibLoader::class.java.getDeclaredField("loaders") - loadersField.isAccessible = true - - for (i in 0 until 2) { - // wtf natan - markLoaded(java.lang.reflect.Array.get(loadersField.get(null), i)) - } - - logger.info("Connector loaded") + var timescaleAvailable: Boolean = false + var udpQueueAvailable: Boolean = false + + /* private shit */ + private val logger: Logger = LoggerFactory.getLogger(NativeUtil::class.java) + + // loaders + private val CONNECTOR_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "connector") + private val UDP_QUEUE_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "udpqueue") + + // class names + private const val LOAD_RESULT_NAME = "com.sedmelluq.lava.common.natives.NativeLibraryLoader\$LoadResult" + + private var LOAD_RESULT: Any? = try { + val ctor = Class.forName(LOAD_RESULT_NAME) + .getDeclaredConstructor(Boolean::class.javaPrimitiveType, RuntimeException::class.java) + + ctor.isAccessible = true + ctor.newInstance(true, null) + } catch (e: ReflectiveOperationException) { + logger.error("Unable to create successful load result"); + null; + } + + /** + * Loads native library shit + */ + fun load() { + loadConnector() + udpQueueAvailable = loadUdpQueue() + timescaleAvailable = loadTimescale() + } + + /** + * Loads the timescale libraries + */ + fun loadTimescale(): Boolean = try { + TimescaleNativeLibLoader.loadTimescaleLibrary() + logger.info("Timescale loaded") + true } catch (ex: Exception) { - logger.error("Connected failed to load", ex) + logger.warn("Timescale failed to load", ex) + false + } + + /** + * Loads the lp-cross version of lavaplayer's loader + */ + private fun loadConnector() { + try { + CONNECTOR_LOADER.load() + + val loadersField = ConnectorNativeLibLoader::class.java.getDeclaredField("loaders") + loadersField.isAccessible = true + + for (i in 0 until 2) { + // wtf natan + markLoaded(java.lang.reflect.Array.get(loadersField.get(null), i)) + } + + logger.info("Connector loaded") + } catch (ex: Exception) { + logger.error("Connected failed to load", ex) + } } - } - - /** - * Loads udp-queue natives - */ - private fun loadUdpQueue() = try { - /* Load the lp-cross version of the library. */ - UDP_QUEUE_LOADER.load() - - /* mark lavaplayer's loader as loaded to avoid failing when loading mpg123 on windows/attempting to load connector again. */ - with(UdpQueueManagerLibrary::class.java.getDeclaredField("nativeLoader")) { - isAccessible = true - markLoaded(get(null)) + + /** + * Loads udp-queue natives + */ + private fun loadUdpQueue() = try { + /* Load the lp-cross version of the library. */ + UDP_QUEUE_LOADER.load() + + /* mark lavaplayer's loader as loaded to avoid failing when loading mpg123 on windows/attempting to load connector again. */ + with(UdpQueueManagerLibrary::class.java.getDeclaredField("nativeLoader")) { + isAccessible = true + markLoaded(get(null)) + } + + /* return true */ + logger.info("Loaded udp-queue library.") + true + } catch (ex: Throwable) { + logger.warn("Error loading udp-queue library.", ex) + false } - /* return true */ - logger.info("Loaded udp-queue library.") - true - } catch (ex: Throwable) { - logger.warn("Error loading udp-queue library.", ex) - false - } - - private fun markLoaded(loader: Any) { - val previousResultField = loader.javaClass.getDeclaredField("previousResult") - previousResultField.isAccessible = true - previousResultField[loader] = LOAD_RESULT - } + private fun markLoaded(loader: Any) { + val previousResultField = loader.javaClass.getDeclaredField("previousResult") + previousResultField.isAccessible = true + previousResultField[loader] = LOAD_RESULT + } } diff --git a/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt b/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt index d2c10e1..caee27b 100644 --- a/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt +++ b/Server/src/main/kotlin/obsidian/server/util/ThreadFactory.kt @@ -21,12 +21,12 @@ import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicInteger fun threadFactory(name: String, daemon: Boolean = false, priority: Int? = null): ThreadFactory { - val counter = AtomicInteger() - return ThreadFactory { runnable -> - Thread(System.getSecurityManager()?.threadGroup ?: Thread.currentThread().threadGroup, runnable).apply { - this.name = name.format(Locale.ROOT, counter.getAndIncrement()) - this.isDaemon = if (!isDaemon) daemon else true - priority?.let { this.priority = it } + val counter = AtomicInteger() + return ThreadFactory { runnable -> + Thread(System.getSecurityManager()?.threadGroup ?: Thread.currentThread().threadGroup, runnable).apply { + this.name = name.format(Locale.ROOT, counter.getAndIncrement()) + this.isDaemon = if (!isDaemon) daemon else true + priority?.let { this.priority = it } + } } - } } diff --git a/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt b/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt index 0c81815..3e23516 100644 --- a/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt @@ -25,46 +25,46 @@ import java.io.ByteArrayOutputStream import java.util.* object TrackUtil { - /** - * Base64 decoder used by [decode] - */ - private val decoder: Base64.Decoder by lazy { - Base64.getDecoder() - } + /** + * Base64 decoder used by [decode] + */ + private val decoder: Base64.Decoder by lazy { + Base64.getDecoder() + } - /** - * Base64 encoder used by [encode] - */ - private val encoder: Base64.Encoder by lazy { - Base64.getEncoder() - } + /** + * Base64 encoder used by [encode] + */ + private val encoder: Base64.Encoder by lazy { + Base64.getEncoder() + } - /** - * Decodes a base64 encoded string into a usable [AudioTrack] - * - * @param encodedTrack The base64 encoded string. - * - * @return The decoded [AudioTrack] - */ - fun decode(encodedTrack: String): AudioTrack { - val inputStream = ByteArrayInputStream(decoder.decode(encodedTrack)) - return inputStream.use { - players.decodeTrack(MessageInput(it))!!.decodedTrack + /** + * Decodes a base64 encoded string into a usable [AudioTrack] + * + * @param encodedTrack The base64 encoded string. + * + * @return The decoded [AudioTrack] + */ + fun decode(encodedTrack: String): AudioTrack { + val inputStream = ByteArrayInputStream(decoder.decode(encodedTrack)) + return inputStream.use { + players.decodeTrack(MessageInput(it))!!.decodedTrack + } } - } - /** - * Encodes a [AudioTrack] into a base64 encoded string. - * - * @param track The audio track to encode. - * - * @return The base64 encoded string - */ - fun encode(track: AudioTrack): String { - val outputStream = ByteArrayOutputStream() - return outputStream.use { - players.encodeTrack(MessageOutput(it), track) - encoder.encodeToString(it.toByteArray()) + /** + * Encodes a [AudioTrack] into a base64 encoded string. + * + * @param track The audio track to encode. + * + * @return The base64 encoded string + */ + fun encode(track: AudioTrack): String { + val outputStream = ByteArrayOutputStream() + return outputStream.use { + players.encodeTrack(MessageOutput(it), track) + encoder.encodeToString(it.toByteArray()) + } } - } } diff --git a/Server/src/main/kotlin/obsidian/server/util/VersionInfo.kt b/Server/src/main/kotlin/obsidian/server/util/VersionInfo.kt index a392613..f1a8581 100644 --- a/Server/src/main/kotlin/obsidian/server/util/VersionInfo.kt +++ b/Server/src/main/kotlin/obsidian/server/util/VersionInfo.kt @@ -17,18 +17,18 @@ package obsidian.server.util object VersionInfo { - private val stream = VersionInfo::class.java.classLoader.getResourceAsStream("version.txt") - private val versionTxt = stream?.reader()?.readText()?.split('\n') + private val stream = VersionInfo::class.java.classLoader.getResourceAsStream("version.txt") + private val versionTxt = stream?.reader()?.readText()?.split('\n') - /** - * Current version of Mixtape. - */ - val VERSION = versionTxt?.get(0) - ?: "1.0.0" + /** + * Current version of Mixtape. + */ + val VERSION = versionTxt?.get(0) + ?: "1.0.0" - /** - * Current git revision. - */ - val GIT_REVISION = versionTxt?.get(1) - ?: "unknown" + /** + * Current git revision. + */ + val GIT_REVISION = versionTxt?.get(1) + ?: "unknown" } diff --git a/Server/src/main/kotlin/obsidian/server/util/kxs/AudioTrackSerializer.kt b/Server/src/main/kotlin/obsidian/server/util/kxs/AudioTrackSerializer.kt index 52c82b0..b28c7a0 100644 --- a/Server/src/main/kotlin/obsidian/server/util/kxs/AudioTrackSerializer.kt +++ b/Server/src/main/kotlin/obsidian/server/util/kxs/AudioTrackSerializer.kt @@ -26,14 +26,14 @@ import kotlinx.serialization.encoding.Encoder import obsidian.server.util.TrackUtil object AudioTrackSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("AudioTrack", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("AudioTrack", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: AudioTrack) { - encoder.encodeString(TrackUtil.encode(value)) - } + override fun serialize(encoder: Encoder, value: AudioTrack) { + encoder.encodeString(TrackUtil.encode(value)) + } - override fun deserialize(decoder: Decoder): AudioTrack { - return TrackUtil.decode(decoder.decodeString()) - } + override fun deserialize(decoder: Decoder): AudioTrack { + return TrackUtil.decode(decoder.decodeString()) + } } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt index 0b75852..84ff22f 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt @@ -24,68 +24,67 @@ import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist import com.sedmelluq.discord.lavaplayer.track.AudioTrack import kotlinx.coroutines.CompletableDeferred import org.slf4j.LoggerFactory -import java.util.* import java.util.concurrent.atomic.AtomicBoolean class AudioLoader(private val audioPlayerManager: AudioPlayerManager) : AudioLoadResultHandler { - private val loadResult: CompletableDeferred = CompletableDeferred() - private val used = AtomicBoolean(false) + private val loadResult: CompletableDeferred = CompletableDeferred() + private val used = AtomicBoolean(false) - fun load(identifier: String?): CompletableDeferred { - val isUsed = used.getAndSet(true) - check(!isUsed) { - "This loader can only be used once per instance" - } + fun load(identifier: String?): CompletableDeferred { + val isUsed = used.getAndSet(true) + check(!isUsed) { + "This loader can only be used once per instance" + } - logger.trace("Loading item with identifier $identifier") - audioPlayerManager.loadItem(identifier, this) + logger.trace("Loading item with identifier $identifier") + audioPlayerManager.loadItem(identifier, this) - return loadResult - } + return loadResult + } - override fun trackLoaded(audioTrack: AudioTrack) { - logger.info("Loaded track ${audioTrack.info.title}") + override fun trackLoaded(audioTrack: AudioTrack) { + logger.info("Loaded track ${audioTrack.info.title}") - val result = ArrayList() - result.add(audioTrack) - loadResult.complete(LoadResult(LoadType.TRACK_LOADED, result, null, null)) - } + val result = ArrayList() + result.add(audioTrack) + loadResult.complete(LoadResult(LoadType.TRACK_LOADED, result, null, null)) + } - override fun playlistLoaded(audioPlaylist: AudioPlaylist) { - logger.info("Loaded playlist ${audioPlaylist.name}") + override fun playlistLoaded(audioPlaylist: AudioPlaylist) { + logger.info("Loaded playlist ${audioPlaylist.name}") - var playlistName: String? = null - var selectedTrack: Int? = null + var playlistName: String? = null + var selectedTrack: Int? = null - if (!audioPlaylist.isSearchResult) { - playlistName = audioPlaylist.name - selectedTrack = audioPlaylist.tracks.indexOf(audioPlaylist.selectedTrack) - } + if (!audioPlaylist.isSearchResult) { + playlistName = audioPlaylist.name + selectedTrack = audioPlaylist.tracks.indexOf(audioPlaylist.selectedTrack) + } - val status: LoadType = if (audioPlaylist.isSearchResult) { - LoadType.SEARCH_RESULT - } else { - LoadType.PLAYLIST_LOADED - } + val status: LoadType = if (audioPlaylist.isSearchResult) { + LoadType.SEARCH_RESULT + } else { + LoadType.PLAYLIST_LOADED + } - val loadedItems = audioPlaylist.tracks - loadResult.complete(LoadResult(status, loadedItems, playlistName, selectedTrack)) - } + val loadedItems = audioPlaylist.tracks + loadResult.complete(LoadResult(status, loadedItems, playlistName, selectedTrack)) + } - override fun noMatches() { - logger.info("No matches found") + override fun noMatches() { + logger.info("No matches found") - loadResult.complete(NO_MATCHES) - } + loadResult.complete(NO_MATCHES) + } - override fun loadFailed(e: FriendlyException) { - logger.error("Load failed", e) + override fun loadFailed(e: FriendlyException) { + logger.error("Load failed", e) - loadResult.complete(LoadResult(e)) - } + loadResult.complete(LoadResult(e)) + } - companion object { - private val logger = LoggerFactory.getLogger(AudioLoader::class.java) - private val NO_MATCHES: LoadResult = LoadResult(LoadType.NO_MATCHES, emptyList(), null, null) - } + companion object { + private val logger = LoggerFactory.getLogger(AudioLoader::class.java) + private val NO_MATCHES: LoadResult = LoadResult(LoadType.NO_MATCHES, emptyList(), null, null) + } } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt index 870cd1c..d01adc8 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt @@ -21,34 +21,34 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrack class LoadResult { - var loadResultType: LoadType - private set - - var tracks: List - private set - - var playlistName: String? - private set - - var selectedTrack: Int? - private set - - var exception: FriendlyException? - private set - - constructor(loadResultType: LoadType, tracks: List, playlistName: String?, selectedTrack: Int?) { - this.loadResultType = loadResultType - this.tracks = tracks - this.playlistName = playlistName - this.selectedTrack = selectedTrack - exception = null - } - - constructor(exception: FriendlyException?) { - loadResultType = LoadType.LOAD_FAILED - tracks = emptyList() - playlistName = null - selectedTrack = null - this.exception = exception - } + var loadResultType: LoadType + private set + + var tracks: List + private set + + var playlistName: String? + private set + + var selectedTrack: Int? + private set + + var exception: FriendlyException? + private set + + constructor(loadResultType: LoadType, tracks: List, playlistName: String?, selectedTrack: Int?) { + this.loadResultType = loadResultType + this.tracks = tracks + this.playlistName = playlistName + this.selectedTrack = selectedTrack + exception = null + } + + constructor(exception: FriendlyException?) { + loadResultType = LoadType.LOAD_FAILED + tracks = emptyList() + playlistName = null + selectedTrack = null + this.exception = exception + } } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt b/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt index fc3b91f..ce7d276 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt @@ -17,9 +17,9 @@ package obsidian.server.util.search enum class LoadType { - TRACK_LOADED, - PLAYLIST_LOADED, - SEARCH_RESULT, - NO_MATCHES, - LOAD_FAILED + TRACK_LOADED, + PLAYLIST_LOADED, + SEARCH_RESULT, + NO_MATCHES, + LOAD_FAILED } diff --git a/Server/src/main/resources/logback.xml b/Server/src/main/resources/logback.xml index f4dc39c..e138db5 100644 --- a/Server/src/main/resources/logback.xml +++ b/Server/src/main/resources/logback.xml @@ -1,9 +1,11 @@ - + - %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){gray} %clr([%35.-35thread]){magenta} %clr(%-35.35logger{39}){cyan} %highlight(%-6level) %msg%n + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){gray} %clr([%35.-35thread]){magenta} + %clr(%-35.35logger{39}){cyan} %highlight(%-6level) %msg%n + diff --git a/Server/src/main/resources/version.txt b/Server/src/main/resources/version.txt index 0a1e116..e02d82d 100644 --- a/Server/src/main/resources/version.txt +++ b/Server/src/main/resources/version.txt @@ -1,2 +1,2 @@ 2.0.0 -4f0ae8a \ No newline at end of file +2ddec6c \ No newline at end of file diff --git a/Server/src/test/resources/routePlanner.http b/Server/src/test/resources/routePlanner.http index 6a1983a..9247bd5 100644 --- a/Server/src/test/resources/routePlanner.http +++ b/Server/src/test/resources/routePlanner.http @@ -8,7 +8,7 @@ Accept: application/json Content-Type: application/json { - "address": "" + "address": "" } ### diff --git a/Server/src/test/resources/tracks.http b/Server/src/test/resources/tracks.http index 9b46ce6..f1fdffe 100644 --- a/Server/src/test/resources/tracks.http +++ b/Server/src/test/resources/tracks.http @@ -4,10 +4,10 @@ POST http://localhost:3030/decodetracks Content-Type: application/json { - "tracks": [ - "QAAAigIAKkhhbHNleSwgRG9taW5pYyBGaWtlIC0gRG9taW5pYydzIEludGVybHVkZQAGSGFsc2V5AAAAAAABZ2AAC1BiS05sOHBPVlRFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9UGJLTmw4cE9WVEUAB3lvdXR1YmUAAAAAAAAAAA==", - "QAAAagIABGRhcmsADGJsb29keSB3aGl0ZQAAAAAAAW8wAAthSjFOS0NST1pDcwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWFKMU5LQ1JPWkNzAAd5b3V0dWJlAAAAAAAAAAA=" - ] + "tracks": [ + "QAAAigIAKkhhbHNleSwgRG9taW5pYyBGaWtlIC0gRG9taW5pYydzIEludGVybHVkZQAGSGFsc2V5AAAAAAABZ2AAC1BiS05sOHBPVlRFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9UGJLTmw4cE9WVEUAB3lvdXR1YmUAAAAAAAAAAA==", + "QAAAagIABGRhcmsADGJsb29keSB3aGl0ZQAAAAAAAW8wAAthSjFOS0NST1pDcwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWFKMU5LQ1JPWkNzAAd5b3V0dWJlAAAAAAAAAAA=" + ] } ### /decodetracks single @@ -16,10 +16,10 @@ POST http://localhost:3030/decodetracks Content-Type: application/json { - "tracks": "QAAAagIABGRhcmsADGJsb29keSB3aGl0ZQAAAAAAAW8wAAthSjFOS0NST1pDcwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWFKMU5LQ1JPWkNzAAd5b3V0dWJlAAAAAAAAAAA=" + "tracks": "QAAAagIABGRhcmsADGJsb29keSB3aGl0ZQAAAAAAAW8wAAthSjFOS0NST1pDcwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWFKMU5LQ1JPWkNzAAd5b3V0dWJlAAAAAAAAAAA=" } ### /loadtracks GET http://localhost:3030/loadtracks?identifier=ytsearch:zacari%20mood -Authorization: cockandballs \ No newline at end of file +Authorization: cockandballs diff --git a/build.gradle.kts b/build.gradle.kts index e53fc78..cfbbc11 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,24 +1,24 @@ plugins { - idea - java + idea + java } allprojects { - group = "obsidian" - apply(plugin = "idea") + group = "obsidian" + apply(plugin = "idea") } subprojects { - buildscript { - repositories { - gradlePluginPortal() - mavenCentral() - } + buildscript { + repositories { + gradlePluginPortal() + mavenCentral() + } - dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10") + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10") + } } - } - apply(plugin = "java") + apply(plugin = "java") } diff --git a/docs/README.md b/docs/README.md index 9aa1d0b..491e5d9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@ # Obsidian API Documentation -Welcome to the Obsidian API Documentation! This document describes mostly everything about the Web Socket and HTTP server. +Welcome to the Obsidian API Documentation! This document describes mostly everything about the Web Socket and HTTP +server. ###### What's Magma? diff --git a/docs/ws-rest.md b/docs/ws-rest.md index 0354f0c..d03f8b8 100644 --- a/docs/ws-rest.md +++ b/docs/ws-rest.md @@ -1,6 +1,7 @@ -# WebSocket & REST +# WebSocket & REST -Magma is the name of our WebSocket & REST server, we use [Ktor](https://ktor.io) because it is completely in Kotlin and is super fast! +Magma is the name of our WebSocket & REST server, we use [Ktor](https://ktor.io) because it is completely in Kotlin and +is super fast! ## Conventions @@ -10,5 +11,6 @@ Conventions used by Obsidian. Everything should use `snake_case`, this includes payloads that are being sent and received. -For payloads that are being sent, the serialization library used by Obsidian can detect pascal & camel case fields. This does not mean you should use said naming conventions. +For payloads that are being sent, the serialization library used by Obsidian can detect pascal & camel case fields. +This does not mean you should use said naming conventions. diff --git a/obsidian.yml b/obsidian.yml index aeef006..102ef38 100644 --- a/obsidian.yml +++ b/obsidian.yml @@ -1,32 +1,32 @@ obsidian: - server: - host: 0.0.0.0 - port: 3030 - password: "" - require-client-name: false - player-updates: - interval: 5000 - send-filters: false + server: + host: 0.0.0.0 + port: 3030 + password: "" + require-client-name: false + player-updates: + interval: 5000 + send-filters: false - lavaplayer: - gc-monitoring: true - non-allocating: false - enabled-sources: [ "youtube", "yarn", "bandcamp", "twitch", "vimeo", "nico", "soundcloud", "local", "http" ] - allow-scsearch: true - rate-limit: - ip-blocks: [ ] - excluded-ips: [ ] - strategy: "rotate-on-ban" # rotate-on-ban | load-balance | nano-switch | rotating-nano-switch - search-triggers-fail: true # Whether a search 429 should trigger marking the ip as failing. - retry-limit: -1 - youtube: - allow-search: true - playlist-page-limit: 6 + lavaplayer: + gc-monitoring: true + non-allocating: false + enabled-sources: [ "youtube", "yarn", "bandcamp", "twitch", "vimeo", "nico", "soundcloud", "local", "http" ] + allow-scsearch: true + rate-limit: + ip-blocks: [ ] + excluded-ips: [ ] + strategy: "rotate-on-ban" # rotate-on-ban | load-balance | nano-switch | rotating-nano-switch + search-triggers-fail: true # Whether a search 429 should trigger marking the ip as failing. + retry-limit: -1 + youtube: + allow-search: true + playlist-page-limit: 6 logging: - level: - root: INFO - obsidian: INFO + level: + root: INFO + obsidian: INFO - file: - max-history: 30 + file: + max-history: 30 diff --git a/settings.gradle.kts b/settings.gradle.kts index f4ca834..cb0949f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,32 +3,32 @@ rootProject.name = "Obsidian-Root" include(":Server") dependencyResolutionManagement { - repositories { - maven { - url = uri("https://dimensional.jfrog.io/artifactory/maven") - name = "Jfrog Dimensional" - } + repositories { + maven { + url = uri("https://dimensional.jfrog.io/artifactory/maven") + name = "Jfrog Dimensional" + } - maven { - url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap/") - name = "Ktor EAP" - } + maven { + url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap/") + name = "Ktor EAP" + } - maven { - url = uri("https://oss.sonatype.org/content/repositories/snapshots/") - name = "Sonatype" - } + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + name = "Sonatype" + } - maven{ - url = uri("https://m2.dv8tion.net/releases") - name = "Dv8tion" - } + maven { + url = uri("https://m2.dv8tion.net/releases") + name = "Dv8tion" + } - maven{ - url = uri("https://jitpack.io") - name = "Jitpack" - } + maven { + url = uri("https://jitpack.io") + name = "Jitpack" + } - mavenCentral() - } + mavenCentral() + } } From 654cfcb1e7f17772cfa221ba4eddef0e823bd10a Mon Sep 17 00:00:00 2001 From: melike2d Date: Sun, 1 Aug 2021 14:16:02 -0700 Subject: [PATCH 21/46] :recycle: refactor track loading --- .../kotlin/obsidian/server/io/rest/tracks.kt | 14 ++--- .../server/util/search/AudioLoader.kt | 57 ++++++------------- .../obsidian/server/util/search/LoadResult.kt | 38 ++++--------- 3 files changed, 33 insertions(+), 76 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 33a94df..10d125a 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -43,8 +43,8 @@ private val logger: Logger = LoggerFactory.getLogger("Routing.tracks") fun Routing.tracks() { authenticate { get { data -> - val result = AudioLoader(Application.players) - .load(data.identifier) + val result = AudioLoader + .load(data.identifier, Application.players) .await() if (result.exception != null) { @@ -52,14 +52,11 @@ fun Routing.tracks() { } val playlist = result.playlistName?.let { - LoadTracks.Response.PlaylistInfo(name = it, selectedTrack = result.selectedTrack) + LoadTracks.Response.PlaylistInfo(name = it, selectedTrack = result.selectedTrack, url = data.identifier) } val exception = if (result.loadResultType == LoadType.LOAD_FAILED && result.exception != null) { - LoadTracks.Response.Exception( - message = result.exception!!.localizedMessage, - severity = result.exception!!.severity - ) + LoadTracks.Response.Exception(message = result.exception!!.localizedMessage, severity = result.exception!!.severity) } else { null } @@ -144,7 +141,8 @@ data class LoadTracks(val identifier: String) { data class PlaylistInfo( val name: String, @SerialName("selected_track") - val selectedTrack: Int? + val selectedTrack: Int?, + val url: String ) } } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt index 84ff22f..b4acb67 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt @@ -24,67 +24,44 @@ import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist import com.sedmelluq.discord.lavaplayer.track.AudioTrack import kotlinx.coroutines.CompletableDeferred import org.slf4j.LoggerFactory -import java.util.concurrent.atomic.AtomicBoolean -class AudioLoader(private val audioPlayerManager: AudioPlayerManager) : AudioLoadResultHandler { - private val loadResult: CompletableDeferred = CompletableDeferred() - private val used = AtomicBoolean(false) +class AudioLoader(private val deferred: CompletableDeferred) : AudioLoadResultHandler { - fun load(identifier: String?): CompletableDeferred { - val isUsed = used.getAndSet(true) - check(!isUsed) { - "This loader can only be used once per instance" - } - - logger.trace("Loading item with identifier $identifier") - audioPlayerManager.loadItem(identifier, this) + companion object { + private val logger = LoggerFactory.getLogger(AudioLoader::class.java) - return loadResult + fun load(identifier: String, playerManager: AudioPlayerManager) = CompletableDeferred().also { + val handler = AudioLoader(it) + playerManager.loadItem(identifier, handler) + } } override fun trackLoaded(audioTrack: AudioTrack) { logger.info("Loaded track ${audioTrack.info.title}") - - val result = ArrayList() - result.add(audioTrack) - loadResult.complete(LoadResult(LoadType.TRACK_LOADED, result, null, null)) + deferred.complete(LoadResult(LoadType.TRACK_LOADED, listOf(audioTrack), null, null)) } override fun playlistLoaded(audioPlaylist: AudioPlaylist) { - logger.info("Loaded playlist ${audioPlaylist.name}") - - var playlistName: String? = null - var selectedTrack: Int? = null + logger.info("Loaded playlist \"${audioPlaylist.name}\"") - if (!audioPlaylist.isSearchResult) { - playlistName = audioPlaylist.name - selectedTrack = audioPlaylist.tracks.indexOf(audioPlaylist.selectedTrack) - } - - val status: LoadType = if (audioPlaylist.isSearchResult) { - LoadType.SEARCH_RESULT + val result = if (audioPlaylist.isSearchResult) { + LoadResult(LoadType.SEARCH_RESULT, audioPlaylist.tracks, null, null) } else { - LoadType.PLAYLIST_LOADED + val selectedTrack = audioPlaylist.tracks.indexOf(audioPlaylist.selectedTrack) + LoadResult(LoadType.PLAYLIST_LOADED, audioPlaylist.tracks, audioPlaylist.name, selectedTrack) } - val loadedItems = audioPlaylist.tracks - loadResult.complete(LoadResult(status, loadedItems, playlistName, selectedTrack)) + deferred.complete(result) } override fun noMatches() { logger.info("No matches found") - - loadResult.complete(NO_MATCHES) + deferred.complete(LoadResult()) } override fun loadFailed(e: FriendlyException) { - logger.error("Load failed", e) - - loadResult.complete(LoadResult(e)) + logger.error("Failed to load", e) + deferred.complete(LoadResult(e)) } - companion object { - private val logger = LoggerFactory.getLogger(AudioLoader::class.java) - private val NO_MATCHES: LoadResult = LoadResult(LoadType.NO_MATCHES, emptyList(), null, null) - } } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt index d01adc8..47b39f5 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt @@ -19,36 +19,18 @@ package obsidian.server.util.search import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack - -class LoadResult { - var loadResultType: LoadType - private set - - var tracks: List - private set - - var playlistName: String? - private set - - var selectedTrack: Int? - private set - - var exception: FriendlyException? +class LoadResult( + val loadResultType: LoadType = LoadType.NO_MATCHES, + val tracks: List = emptyList(), + val playlistName: String? = null, + val selectedTrack: Int? = null, +) { + + var exception: FriendlyException? = null private set - constructor(loadResultType: LoadType, tracks: List, playlistName: String?, selectedTrack: Int?) { - this.loadResultType = loadResultType - this.tracks = tracks - this.playlistName = playlistName - this.selectedTrack = selectedTrack - exception = null - } - - constructor(exception: FriendlyException?) { - loadResultType = LoadType.LOAD_FAILED - tracks = emptyList() - playlistName = null - selectedTrack = null + constructor(exception: FriendlyException) : this(LoadType.LOAD_FAILED) { this.exception = exception } + } From ff54d1057a4583c0c5eab76aa772b256a6504d72 Mon Sep 17 00:00:00 2001 From: melike2d Date: Sun, 1 Aug 2021 14:16:19 -0700 Subject: [PATCH 22/46] :goal_net: use better errors for route planner --- .../src/main/kotlin/obsidian/server/player/ObsidianAPM.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt index 5dc0db1..871f62a 100644 --- a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt +++ b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt @@ -55,7 +55,7 @@ class ObsidianAPM : DefaultAudioPlayerManager() { when { Ipv6Block.isIpv6CidrBlock(it) -> Ipv6Block(it) Ipv4Block.isIpv4CidrBlock(it) -> Ipv4Block(it) - else -> throw RuntimeException("Invalid IP Block '$it', make sure to provide a valid CIDR notation") + else -> throw IllegalArgumentException("Invalid IP Block '$it', make sure to provide a valid CIDR notation") } } @@ -66,12 +66,12 @@ class ObsidianAPM : DefaultAudioPlayerManager() { val filter = Predicate { !blacklisted.contains(it) } val searchTriggersFail = config[Obsidian.Lavaplayer.RateLimit.searchTriggersFail] - return@lazy when (config[Obsidian.Lavaplayer.RateLimit.strategy]) { + return@lazy when (config[Obsidian.Lavaplayer.RateLimit.strategy].trim()) { "rotate-on-ban" -> RotatingIpRoutePlanner(ipBlocks, filter, searchTriggersFail) "load-balance" -> BalancingIpRoutePlanner(ipBlocks, filter, searchTriggersFail) "rotating-nano-switch" -> RotatingNanoIpRoutePlanner(ipBlocks, filter, searchTriggersFail) "nano-switch" -> NanoIpRoutePlanner(ipBlocks, searchTriggersFail) - else -> throw RuntimeException("Unknown strategy!") + else -> throw IllegalArgumentException("Unknown Strategy!") } } From e26835857906458ef580bfbd799a3ca5dd1f79ae Mon Sep 17 00:00:00 2001 From: melike2d Date: Sun, 1 Aug 2021 14:18:14 -0700 Subject: [PATCH 23/46] :fire: remove redundant error --- Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 10d125a..70f3ca9 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -47,10 +47,6 @@ fun Routing.tracks() { .load(data.identifier, Application.players) .await() - if (result.exception != null) { - logger.error("Track loading failed", result.exception) - } - val playlist = result.playlistName?.let { LoadTracks.Response.PlaylistInfo(name = it, selectedTrack = result.selectedTrack, url = data.identifier) } From 925a9e52fa082e8901e091a7ba1aba410043964b Mon Sep 17 00:00:00 2001 From: melike2d Date: Sun, 1 Aug 2021 14:30:09 -0700 Subject: [PATCH 24/46] :bug: replace camel case with snake case - channel mix filter - distortion filter --- .../server/player/filter/impl/ChannelMixFilter.kt | 5 +++++ .../server/player/filter/impl/DistortionFilter.kt | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt index 4589104..c567aca 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/ChannelMixFilter.kt @@ -19,14 +19,19 @@ package obsidian.server.player.filter.impl import com.github.natanbc.lavadsp.channelmix.ChannelMixPcmAudioFilter import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter @Serializable data class ChannelMixFilter( + @SerialName("left_to_left") val leftToLeft: Float = 1f, + @SerialName("left_to_right") val leftToRight: Float = 0f, + @SerialName("right_to_right") val rightToRight: Float = 0f, + @SerialName("right_to_left") val rightToLeft: Float = 1f, ) : Filter { override val enabled: Boolean diff --git a/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt b/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt index 44edb38..3866bf8 100644 --- a/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt +++ b/Server/src/main/kotlin/obsidian/server/player/filter/impl/DistortionFilter.kt @@ -19,15 +19,24 @@ package obsidian.server.player.filter.impl import com.github.natanbc.lavadsp.distortion.DistortionPcmAudioFilter import com.sedmelluq.discord.lavaplayer.filter.FloatPcmAudioFilter import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import obsidian.server.player.filter.Filter @Serializable data class DistortionFilter( + @SerialName("sin_offset") val sinOffset: Float = 0f, + @SerialName("sin_scale") val sinScale: Float = 1f, + @SerialName("cos_offset") val cosOffset: Float = 0f, + @SerialName("cos_scale") val cosScale: Float = 1f, + @SerialName("tan_offset") + val tanOffset: Float = 0f, + @SerialName("tan_scale") + val tanScale: Float = 1f, val offset: Float = 0f, val scale: Float = 1f ) : Filter { @@ -43,6 +52,8 @@ data class DistortionFilter( .setSinScale(sinScale) .setCosOffset(cosOffset) .setCosScale(cosScale) + .setTanOffset(tanOffset) + .setTanScale(tanScale) .setOffset(offset) .setScale(scale) } From 706d5ca07b4543599b749c3d8702cd9174db7979 Mon Sep 17 00:00:00 2001 From: melike2d Date: Sun, 1 Aug 2021 14:30:41 -0700 Subject: [PATCH 25/46] :bug: fix broken repository resolution --- Server/build.gradle.kts | 12 +++++------- build.gradle.kts | 31 ++++++++++++++++++++++++++++++- settings.gradle.kts | 31 ------------------------------- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index abef902..13bc572 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -19,10 +19,10 @@ application { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.20") // standard library - implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.20") // reflection + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.21") // standard library + implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.21") // reflection implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") // core coroutine library - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") // json serialization + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") // json serialization val ktorVersion = "1.6.1" implementation("io.ktor:ktor-server-core:$ktorVersion") // ktor server core @@ -31,9 +31,7 @@ dependencies { implementation("io.ktor:ktor-serialization:$ktorVersion") // ktor serialization implementation("io.ktor:ktor-websockets:$ktorVersion") // ktor websockets - implementation("moe.kyokobot.koe:core:master-SNAPSHOT") { // discord send system - exclude(group = "org.slf4j", module = "slf4j-api") - } + implementation("moe.kyokobot.koe:core:koe-v2-SNAPSHOT") // discord send system implementation("com.sedmelluq:lavaplayer:1.3.78") { // yes exclude(group = "com.sedmelluq", module = "lavaplayer-natives") @@ -47,7 +45,7 @@ dependencies { implementation("com.github.natanbc:native-loader:0.7.2") // native loader implementation("com.github.natanbc:lp-cross:0.1.3-1") // lp-cross natives - implementation("ch.qos.logback:logback-classic:1.2.3") // slf4j logging backend + implementation("ch.qos.logback:logback-classic:1.2.5") // slf4j logging backend val konfVersion = "1.1.2" implementation("com.github.uchuhimo.konf:konf-core:$konfVersion") // konf core shit diff --git a/build.gradle.kts b/build.gradle.kts index cfbbc11..0f58512 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,9 +16,38 @@ subprojects { } dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20") } } + repositories { + maven { + url = uri("https://dimensional.jfrog.io/artifactory/maven") + name = "Jfrog Dimensional" + } + + maven { + url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap/") + name = "Ktor EAP" + } + + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + name = "Sonatype" + } + + maven { + url = uri("https://m2.dv8tion.net/releases") + name = "Dv8tion" + } + + maven { + url = uri("https://jitpack.io") + name = "Jitpack" + } + + mavenCentral() + } + apply(plugin = "java") } diff --git a/settings.gradle.kts b/settings.gradle.kts index cb0949f..5056dcb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,34 +1,3 @@ rootProject.name = "Obsidian-Root" include(":Server") - -dependencyResolutionManagement { - repositories { - maven { - url = uri("https://dimensional.jfrog.io/artifactory/maven") - name = "Jfrog Dimensional" - } - - maven { - url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap/") - name = "Ktor EAP" - } - - maven { - url = uri("https://oss.sonatype.org/content/repositories/snapshots/") - name = "Sonatype" - } - - maven { - url = uri("https://m2.dv8tion.net/releases") - name = "Dv8tion" - } - - maven { - url = uri("https://jitpack.io") - name = "Jitpack" - } - - mavenCentral() - } -} From 5cdd30af69b3c833f4c587b87e04c20faeb6d272 Mon Sep 17 00:00:00 2001 From: melike2d Date: Sun, 1 Aug 2021 14:31:07 -0700 Subject: [PATCH 26/46] :see_no_evil: ignore version.txt --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 229c211..6549c59 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ target/ .project .classpath +version.txt From 30a01b1e7de4695bfb6d519adedf667e43eea22b Mon Sep 17 00:00:00 2001 From: melike2d Date: Sun, 1 Aug 2021 14:31:58 -0700 Subject: [PATCH 27/46] :fire: remove version.txt --- Server/src/main/resources/version.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 Server/src/main/resources/version.txt diff --git a/Server/src/main/resources/version.txt b/Server/src/main/resources/version.txt deleted file mode 100644 index e02d82d..0000000 --- a/Server/src/main/resources/version.txt +++ /dev/null @@ -1,2 +0,0 @@ -2.0.0 -2ddec6c \ No newline at end of file From 5dca0352816c6490dd42e5f3648139e9aed3fe73 Mon Sep 17 00:00:00 2001 From: melike2d Date: Sun, 1 Aug 2021 14:32:12 -0700 Subject: [PATCH 28/46] :art: clearer variable names --- .../src/main/kotlin/obsidian/server/io/Handlers.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt index 24edefd..bb0ba58 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt @@ -59,20 +59,20 @@ object Handlers { return } - val track = TrackUtil.decode(track) + val audioTrack = TrackUtil.decode(track) /* handle start and end times */ - if (startTime != null && startTime in 0..track.duration) { - track.position = startTime + if (startTime != null && startTime in 0..audioTrack.duration) { + audioTrack.position = startTime } - if (endTime != null && endTime in 0..track.duration) { + if (endTime != null && endTime in 0..audioTrack.duration) { val handler = TrackEndMarkerHandler(player) val marker = TrackMarker(endTime, handler) - track.setMarker(marker) + audioTrack.setMarker(marker) } - player.play(track) + player.play(audioTrack) } fun stopTrack(client: MagmaClient, guildId: Long) { From e27f04d4ca0b2a067aab2ebbab18b18d60335ede Mon Sep 17 00:00:00 2001 From: melike2d Date: Mon, 2 Aug 2021 06:14:58 -0700 Subject: [PATCH 29/46] :bug: fix udp queue frame poller - Koe v2 uses an AtomicInteger --- .../kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt index 6639026..3f0d6d3 100644 --- a/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt +++ b/Server/src/main/kotlin/moe/kyokobot/koe/codec/udpqueue/UdpQueueFramePoller.kt @@ -20,15 +20,15 @@ import moe.kyokobot.koe.MediaConnection import moe.kyokobot.koe.codec.AbstractFramePoller import moe.kyokobot.koe.codec.OpusCodec import moe.kyokobot.koe.internal.handler.DiscordUDPConnection -import moe.kyokobot.koe.media.IntReference import java.net.InetSocketAddress import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger class UdpQueueFramePoller(connection: MediaConnection, private val manager: QueueManagerPool.UdpQueueWrapper) : AbstractFramePoller(connection) { private var lastFrame: Long = 0 - private val timestamp: IntReference = IntReference() + private val timestamp = AtomicInteger() override fun start() { check(!polling) { @@ -63,11 +63,11 @@ class UdpQueueFramePoller(connection: MediaConnection, private val manager: Queu /* retrieve a frame so we can compare */ val start = buf.writerIndex() - sender.retrieve(OpusCodec.INSTANCE, buf, timestamp) + sender.retrieve(OpusCodec.INSTANCE, buf, timestamp, null) /* create a packet */ val packet = - handler.createPacket(OpusCodec.PAYLOAD_TYPE, timestamp.get(), buf, buf.writerIndex() - start, false) + handler.createPacket(OpusCodec.PAYLOAD_TYPE, timestamp.get(), connection.gatewayConnection!!.audioSSRC, buf, buf.writerIndex() - start, false) if (packet != null) { manager.queuePacket(packet.nioBuffer(), handler.serverAddress as InetSocketAddress) From 361194a5b45325254451c6f81ab199b318f48a0b Mon Sep 17 00:00:00 2001 From: melike2d Date: Mon, 2 Aug 2021 06:15:12 -0700 Subject: [PATCH 30/46] :memo: general changes --- .../kotlin/obsidian/server/Application.kt | 16 +++-- .../obsidian/server/io/RoutePlannerUtil.kt | 6 +- .../kotlin/obsidian/server/io/rest/planner.kt | 3 +- .../kotlin/obsidian/server/io/rest/players.kt | 57 ++++++++++++++++ .../kotlin/obsidian/server/io/rest/tracks.kt | 67 +++++++++---------- .../obsidian/server/player/PlayerUpdates.kt | 4 +- .../server/util/CoroutineAudioEventAdapter.kt | 16 ++--- .../kotlin/obsidian/server/util/CpuTimer.kt | 2 - .../server/util/LogbackColorConverter.kt | 24 ++++--- .../server/util/search/AudioLoader.kt | 2 +- 10 files changed, 127 insertions(+), 70 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/Application.kt b/Server/src/main/kotlin/obsidian/server/Application.kt index a542a89..5a6002f 100644 --- a/Server/src/main/kotlin/obsidian/server/Application.kt +++ b/Server/src/main/kotlin/obsidian/server/Application.kt @@ -69,7 +69,7 @@ object Application { /** * Logger */ - val log: org.slf4j.Logger = LoggerFactory.getLogger(Application::class.java) + val logger: org.slf4j.Logger = LoggerFactory.getLogger(Application::class.java) /** * Json parser used by ktor and us. @@ -80,6 +80,10 @@ object Application { ignoreUnknownKeys = true } + init { + logger.info("Obsidian version: ${VersionInfo.VERSION}, commit: ${VersionInfo.GIT_REVISION}") + } + @JvmStatic fun main(args: Array) = runBlocking { @@ -90,22 +94,22 @@ object Application { try { val type = SystemType.detect(SystemNativeLibraryProperties(null, "nativeloader.")) - log.info("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") - log.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}") + logger.info("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") + logger.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}") } catch (e: Exception) { val message = "Unable to load system info" + if (e is UnsatisfiedLinkError || e is RuntimeException && e.cause is UnsatisfiedLinkError) ", this isn't an error" else "." - log.warn(message, e) + logger.warn(message, e) } try { - log.info("Loading Native Libraries") + logger.info("Loading Native Libraries") NativeUtil.timescaleAvailable = true NativeUtil.load() } catch (ex: Exception) { - log.error("Fatal exception while loading native libraries.", ex) + logger.error("Fatal exception while loading native libraries.", ex) exitProcess(1) } diff --git a/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt b/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt index 758e530..9173b5f 100644 --- a/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt @@ -64,10 +64,8 @@ object RoutePlannerUtil { } } -data class RoutePlannerStatus( - val `class`: String?, - val details: IRoutePlannerStatus? -) { +@Serializable +data class RoutePlannerStatus(val `class`: String?, val details: IRoutePlannerStatus?) { interface IRoutePlannerStatus } diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt b/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt index 88ddfcb..0c1f60c 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/planner.kt @@ -30,7 +30,7 @@ import java.net.InetAddress fun Routing.planner() { val routePlanner = Application.players.routePlanner - route("/routeplanner") { + route("/planner") { authenticate { get("/status") { routePlanner @@ -46,7 +46,6 @@ fun Routing.planner() { } route("/free") { - post("/address") { routePlanner ?: return@post context.respond(HttpStatusCode.NotImplemented, RoutePlannerDisabled()) diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt index 36b5caa..ff349ae 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/players.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/players.kt @@ -62,6 +62,34 @@ object Players { call.attributes.put(ClientAttr, Magma.getClient(userId, call.attributes.getOrNull(ClientName))) } + /** + * + */ + delete { + val guildId = call.attributes[GuildAttr] + + /* get the requested player */ + call.attributes[ClientAttr].players[guildId] + ?: return@delete respondAndFinish(NotFound, Response("Unknown player for guild '$guildId'")) + + /* destroy the player. */ + Handlers.destroy(call.attributes[ClientAttr], guildId); + call.respond(Response("Successfully destroyed player", success = true)) + } + + put { + val configure = call.receive() + Handlers.configure( + call.attributes[ClientAttr], + call.attributes[GuildAttr], + filters = configure.filters, + pause = configure.pause, + sendPlayerUpdates = configure.sendPlayerUpdates + ) + + call.respond(Response("Successfully configured the player", success = true)) + } + /** * */ @@ -78,6 +106,15 @@ object Players { call.respond(response) } + /* + * + */ + put("/pause") { + val (state) = call.receive() + Handlers.configure(call.attributes[ClientAttr], call.attributes[GuildAttr], pause = state) + call.respond(Response("Successfully ${if (state) "paused" else "resumed"} the player.", success = true)) + } + /** * */ @@ -149,6 +186,23 @@ data class SubmitVoiceServer(@SerialName("session_id") val sessionId: String, va @Serializable data class Seek(val position: Long) +/** + * Body for PUT `/player/{guild}/pause` + */ +@Serializable +data class Pause(val state: Boolean) + +/** + * + */ +@Serializable +data class Configure( + val filters: Filters?, + val pause: Boolean?, + @SerialName("send_player_updates") + val sendPlayerUpdates: Boolean? +) + /** * */ @@ -160,6 +214,9 @@ data class PlayTrack( @SerialName("no_replace") val noReplace: Boolean = false ) +/** + * + */ @Serializable data class StopTrackResponse( val track: Track?, diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 70f3ca9..1edb33d 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -38,44 +38,43 @@ import obsidian.server.util.search.LoadType import org.slf4j.Logger import org.slf4j.LoggerFactory -private val logger: Logger = LoggerFactory.getLogger("Routing.tracks") - -fun Routing.tracks() { - authenticate { - get { data -> - val result = AudioLoader - .load(data.identifier, Application.players) - .await() - - val playlist = result.playlistName?.let { - LoadTracks.Response.PlaylistInfo(name = it, selectedTrack = result.selectedTrack, url = data.identifier) - } - - val exception = if (result.loadResultType == LoadType.LOAD_FAILED && result.exception != null) { - LoadTracks.Response.Exception(message = result.exception!!.localizedMessage, severity = result.exception!!.severity) - } else { - null - } - - val response = LoadTracks.Response( - tracks = result.tracks.map(::getTrack), - type = result.loadResultType, - playlistInfo = playlist, - exception = exception - ) - - context.respond(response) +fun Routing.tracks() = authenticate { + get { data -> + val result = AudioLoader + .load(data.identifier, Application.players) + .await() + + val playlist = result.playlistName?.let { + LoadTracks.Response.PlaylistInfo(name = it, selectedTrack = result.selectedTrack, url = data.identifier) } - get { - val track = TrackUtil.decode(it.track) - context.respond(getTrackInfo(track)) + val exception = if (result.loadResultType == LoadType.LOAD_FAILED && result.exception != null) { + LoadTracks.Response.Exception( + message = result.exception!!.localizedMessage, + severity = result.exception!!.severity + ) + } else { + null } - post("/decodetracks") { - val body = call.receive() - context.respond(body.tracks.map(::getTrackInfo)) - } + val response = LoadTracks.Response( + tracks = result.tracks.map(::getTrack), + type = result.loadResultType, + playlistInfo = playlist, + exception = exception + ) + + context.respond(response) + } + + get { + val track = TrackUtil.decode(it.track) + context.respond(getTrackInfo(track)) + } + + post("/decodetracks") { + val body = call.receive() + context.respond(body.tracks.map(::getTrackInfo)) } } diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index 2f3af98..7b98c0d 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -75,11 +75,11 @@ class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { } } - override suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) { + override suspend fun onTrackStart(player: AudioPlayer, track: AudioTrack) { start() } - override suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) { + override suspend fun onTrackEnd(player: AudioPlayer, track: AudioTrack, reason: AudioTrackEndReason) { stop() } diff --git a/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt index f656e41..133106c 100644 --- a/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt +++ b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt @@ -32,12 +32,12 @@ open class CoroutineAudioEventAdapter(private val dispatcher: CoroutineDispatche get() = dispatcher + SupervisorJob() /* playback start/end */ - open suspend fun onTrackStart(track: AudioTrack, player: AudioPlayer) = Unit - open suspend fun onTrackEnd(track: AudioTrack, reason: AudioTrackEndReason, player: AudioPlayer) = Unit + open suspend fun onTrackStart(player: AudioPlayer, track: AudioTrack) = Unit + open suspend fun onTrackEnd(player: AudioPlayer, track: AudioTrack, reason: AudioTrackEndReason) = Unit /* exception */ - open suspend fun onTrackStuck(thresholdMs: Long, track: AudioTrack, player: AudioPlayer) = Unit - open suspend fun onTrackException(exception: FriendlyException, track: AudioTrack, player: AudioPlayer) = Unit + open suspend fun onTrackStuck(player: AudioPlayer, track: AudioTrack, thresholdMs: Long) = Unit + open suspend fun onTrackException(player: AudioPlayer, track: AudioTrack, exception: FriendlyException) = Unit /* playback state */ open suspend fun onPlayerResume(player: AudioPlayer) = Unit @@ -46,10 +46,10 @@ open class CoroutineAudioEventAdapter(private val dispatcher: CoroutineDispatche override fun onEvent(event: AudioEvent) { launch { when (event) { - is TrackStartEvent -> onTrackStart(event.track, event.player) - is TrackEndEvent -> onTrackEnd(event.track, event.endReason, event.player) - is TrackStuckEvent -> onTrackStuck(event.thresholdMs, event.track, event.player) - is TrackExceptionEvent -> onTrackException(event.exception, event.track, event.player) + is TrackStartEvent -> onTrackStart(event.player, event.track) + is TrackEndEvent -> onTrackEnd(event.player, event.track, event.endReason) + is TrackStuckEvent -> onTrackStuck(event.player, event.track, event.thresholdMs) + is TrackExceptionEvent -> onTrackException(event.player, event.track, event.exception) is PlayerResumeEvent -> onPlayerResume(event.player) is PlayerPauseEvent -> onPlayerPause(event.player) } diff --git a/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt b/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt index 844024c..e57ae1f 100644 --- a/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt +++ b/Server/src/main/kotlin/obsidian/server/util/CpuTimer.kt @@ -59,14 +59,12 @@ class CpuTimer { /** * Attempts to call a method either directly or via one of the implemented interfaces. * - * * A Method object refers to a specific method declared in a specific class. The first invocation * might happen with method == SomeConcreteClass.publicLongGetter() and will fail if * SomeConcreteClass is not public. We then recurse over all interfaces implemented by * SomeConcreteClass (or extended by those interfaces and so on) until we eventually invoke * callMethod() with method == SomePublicInterface.publicLongGetter(), which will then succeed. * - * * There is a built-in assumption that the method will never return null (or, equivalently, that * it returns the primitive data type, i.e. `long` rather than `Long`). If this * assumption doesn't hold, the method might be called repeatedly and the returned value will be diff --git a/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt b/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt index 743260b..b0a10cd 100644 --- a/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt +++ b/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt @@ -20,18 +20,7 @@ import ch.qos.logback.classic.Level import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.pattern.CompositeConverter -fun interface Convert { - fun take(str: String): String -} - class LogbackColorConverter : CompositeConverter() { - override fun transform(event: ILoggingEvent, element: String): String { - val option = ANSI_COLORS[firstOption] - ?: ANSI_COLORS[LEVELS[event.level.toInt()]] - ?: ANSI_COLORS["green"] - - return option!!.take(element) - } companion object { val Number.ansi: String @@ -57,4 +46,17 @@ class LogbackColorConverter : CompositeConverter() { Level.TRACE_INTEGER to "magenta" ) } + + override fun transform(event: ILoggingEvent, element: String): String { + val option = ANSI_COLORS[firstOption] + ?: ANSI_COLORS[LEVELS[event.level.toInt()]] + ?: ANSI_COLORS["green"] + + return option!!.take(element) + } + + fun interface Convert { + fun take(str: String): String + } + } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt index b4acb67..1c9bcda 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt @@ -42,7 +42,7 @@ class AudioLoader(private val deferred: CompletableDeferred) : Audio } override fun playlistLoaded(audioPlaylist: AudioPlaylist) { - logger.info("Loaded playlist \"${audioPlaylist.name}\"") + logger.info("Loaded playlist: ${audioPlaylist.name}") val result = if (audioPlaylist.isSearchResult) { LoadResult(LoadType.SEARCH_RESULT, audioPlaylist.tracks, null, null) From 1f2b98be96f37d349156f01fad0974c8d9579e00 Mon Sep 17 00:00:00 2001 From: melike2d Date: Mon, 2 Aug 2021 06:15:33 -0700 Subject: [PATCH 31/46] :loud_sound: remove line break in logs --- Server/src/main/resources/logback.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Server/src/main/resources/logback.xml b/Server/src/main/resources/logback.xml index e138db5..3c99e7f 100644 --- a/Server/src/main/resources/logback.xml +++ b/Server/src/main/resources/logback.xml @@ -3,8 +3,7 @@ - %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){gray} %clr([%35.-35thread]){magenta} - %clr(%-35.35logger{39}){cyan} %highlight(%-6level) %msg%n + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){gray} %clr([%35.-35thread]){magenta} %clr(%-35.35logger{39}){cyan} %highlight(%-6level) %msg%n From e9bd0d9b9842eefd758ac3f5ffc0469ae6479aa4 Mon Sep 17 00:00:00 2001 From: melike2d Date: Sat, 23 Oct 2021 23:06:57 -0700 Subject: [PATCH 32/46] :sparkles: use custom lavaplayer --- Server/build.gradle.kts | 32 +++++---- .../kotlin/obsidian/server/Application.kt | 17 ++--- .../obsidian/server/config/spec/Obsidian.kt | 5 ++ .../kotlin/obsidian/server/io/Handlers.kt | 9 +-- .../main/kotlin/obsidian/server/io/Magma.kt | 24 +++---- .../kotlin/obsidian/server/io/MagmaClient.kt | 11 +-- .../obsidian/server/io/RoutePlannerUtil.kt | 5 +- .../kotlin/obsidian/server/io/rest/tracks.kt | 64 ++++++++++++----- .../kotlin/obsidian/server/io/ws/Dispatch.kt | 16 +++++ .../main/kotlin/obsidian/server/io/ws/Op.kt | 2 + .../obsidian/server/io/ws/WebSocketHandler.kt | 52 +++++++------- .../server/player/FrameLossTracker.kt | 12 ++-- .../obsidian/server/player/ObsidianAPM.kt | 69 ++++++++++--------- .../kotlin/obsidian/server/player/Player.kt | 33 ++++----- .../obsidian/server/player/PlayerUpdates.kt | 6 +- .../server/util/CoroutineAudioEventAdapter.kt | 22 +++--- .../kotlin/obsidian/server/util/KoeUtil.kt | 17 ++--- .../server/util/LogbackColorConverter.kt | 2 - .../kotlin/obsidian/server/util/NativeUtil.kt | 9 ++- .../kotlin/obsidian/server/util/TrackUtil.kt | 2 +- .../server/util/search/AudioLoader.kt | 57 ++++++++------- .../obsidian/server/util/search/LoadResult.kt | 10 +-- .../obsidian/server/util/search/LoadType.kt | 9 ++- 23 files changed, 261 insertions(+), 224 deletions(-) diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 13bc572..1c0a150 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -5,8 +5,8 @@ import java.io.ByteArrayOutputStream plugins { application id("com.github.johnrengelman.shadow") version "7.0.0" - kotlin("jvm") version "1.5.10" - kotlin("plugin.serialization") version "1.5.10" + kotlin("jvm") version "1.5.30" + kotlin("plugin.serialization") version "1.5.30" } apply(plugin = "kotlin") @@ -19,25 +19,28 @@ application { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.21") // standard library - implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.21") // reflection - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") // core coroutine library - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") // json serialization - - val ktorVersion = "1.6.1" + /* kotlin */ + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.30") // standard library + implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.30") // reflection + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") // core coroutine library + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0") // json serialization + + /* server */ + val ktorVersion = "1.6.4" implementation("io.ktor:ktor-server-core:$ktorVersion") // ktor server core implementation("io.ktor:ktor-server-cio:$ktorVersion") // ktor cio engine implementation("io.ktor:ktor-locations:$ktorVersion") // ktor locations implementation("io.ktor:ktor-serialization:$ktorVersion") // ktor serialization implementation("io.ktor:ktor-websockets:$ktorVersion") // ktor websockets + /* audio */ implementation("moe.kyokobot.koe:core:koe-v2-SNAPSHOT") // discord send system - implementation("com.sedmelluq:lavaplayer:1.3.78") { // yes + implementation("com.sedmelluq:lavaplayer:1.5.2") { // yes exclude(group = "com.sedmelluq", module = "lavaplayer-natives") } - implementation("com.sedmelluq:lavaplayer-ext-youtube-rotator:0.2.3") { // ip rotation + implementation("com.sedmelluq:lavaplayer-ext-ip-rotator:0.3.0") { // ip rotation exclude(group = "com.sedmelluq", module = "lavaplayer") } @@ -45,7 +48,12 @@ dependencies { implementation("com.github.natanbc:native-loader:0.7.2") // native loader implementation("com.github.natanbc:lp-cross:0.1.3-1") // lp-cross natives - implementation("ch.qos.logback:logback-classic:1.2.5") // slf4j logging backend + /* logging */ + implementation("ch.qos.logback:logback-classic:1.2.6") // slf4j logging backend + implementation("io.github.microutils:kotlin-logging-jvm:2.0.10") // logging + + /* misc */ + implementation("fun.dimensional:cuid:1.0.2") // CUIDs val konfVersion = "1.1.2" implementation("com.github.uchuhimo.konf:konf-core:$konfVersion") // konf core shit @@ -59,7 +67,7 @@ tasks.withType { tasks.withType { kotlinOptions { - jvmTarget = "13" + jvmTarget = "11" incremental = true freeCompilerArgs = listOf( "-Xopt-in=kotlin.ExperimentalStdlibApi", diff --git a/Server/src/main/kotlin/obsidian/server/Application.kt b/Server/src/main/kotlin/obsidian/server/Application.kt index 5a6002f..4ae9582 100644 --- a/Server/src/main/kotlin/obsidian/server/Application.kt +++ b/Server/src/main/kotlin/obsidian/server/Application.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import mu.KotlinLogging import obsidian.server.config.spec.Logging import obsidian.server.config.spec.Obsidian import obsidian.server.io.Magma @@ -69,7 +70,7 @@ object Application { /** * Logger */ - val logger: org.slf4j.Logger = LoggerFactory.getLogger(Application::class.java) + val logger = KotlinLogging.logger { } /** * Json parser used by ktor and us. @@ -81,12 +82,11 @@ object Application { } init { - logger.info("Obsidian version: ${VersionInfo.VERSION}, commit: ${VersionInfo.GIT_REVISION}") + logger.info { "Obsidian version: ${VersionInfo.VERSION}, commit: ${VersionInfo.GIT_REVISION}" } } @JvmStatic fun main(args: Array) = runBlocking { - /* setup logging */ configureLogging() @@ -94,22 +94,22 @@ object Application { try { val type = SystemType.detect(SystemNativeLibraryProperties(null, "nativeloader.")) - logger.info("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") - logger.info("Processor Information: ${NativeLibLoader.loadSystemInfo()}") + logger.info { ("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") } + logger.info { ("Processor Information: ${NativeLibLoader.loadSystemInfo()}") } } catch (e: Exception) { val message = "Unable to load system info" + if (e is UnsatisfiedLinkError || e is RuntimeException && e.cause is UnsatisfiedLinkError) ", this isn't an error" else "." - logger.warn(message, e) + logger.warn(e) { message } } try { - logger.info("Loading Native Libraries") + logger.info { "Loading Native Libraries" } NativeUtil.timescaleAvailable = true NativeUtil.load() } catch (ex: Exception) { - logger.error("Fatal exception while loading native libraries.", ex) + logger.error(ex) { "Fatal exception while loading native libraries." } exitProcess(1) } @@ -160,6 +160,7 @@ object Application { } } + server.start(wait = true) shutdown() } diff --git a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt index 38ce373..03a02c7 100644 --- a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt +++ b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt @@ -50,6 +50,11 @@ object Obsidian : ConfigSpec() { */ val auth by optional("") + /** + * Path used for the websocket. + */ + val wsPath by optional("/", "ws-path") + /** * Used to validate a string given as authorization. * diff --git a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt index bb0ba58..6aa8bf3 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt @@ -18,15 +18,13 @@ package obsidian.server.io import com.sedmelluq.discord.lavaplayer.track.TrackMarker import moe.kyokobot.koe.VoiceServerInfo +import mu.KotlinLogging import obsidian.server.player.TrackEndMarkerHandler import obsidian.server.player.filter.Filters import obsidian.server.util.TrackUtil -import org.slf4j.Logger -import org.slf4j.LoggerFactory object Handlers { - - private val log: Logger = LoggerFactory.getLogger(Handlers::class.java) + private val log = KotlinLogging.logger { } fun submitVoiceServer(client: MagmaClient, guildId: Long, vsi: VoiceServerInfo) { val connection = client.mediaConnectionFor(guildId) @@ -55,7 +53,7 @@ object Handlers { ) { val player = client.playerFor(guildId) if (player.audioPlayer.playingTrack != null && noReplace) { - log.info("${client.displayName} - skipping PLAY_TRACK operation") + log.info { "${client.displayName} - skipping PLAY_TRACK operation" } return } @@ -96,5 +94,4 @@ object Handlers { filters?.let { player.filters = it } sendPlayerUpdates?.let { player.updates.enabled = it } } - } diff --git a/Server/src/main/kotlin/obsidian/server/io/Magma.kt b/Server/src/main/kotlin/obsidian/server/io/Magma.kt index 38ca074..81365a6 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Magma.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Magma.kt @@ -27,6 +27,7 @@ import io.ktor.routing.* import io.ktor.util.* import io.ktor.websocket.* import kotlinx.coroutines.isActive +import mu.KotlinLogging import obsidian.server.Application.config import obsidian.server.config.spec.Obsidian import obsidian.server.io.rest.Players.players @@ -38,14 +39,11 @@ import obsidian.server.io.ws.CloseReasons import obsidian.server.io.ws.StatsTask import obsidian.server.io.ws.WebSocketHandler import obsidian.server.util.threadFactory -import org.slf4j.Logger -import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService object Magma { - val ClientName = AttributeKey("ClientName") /** @@ -57,9 +55,9 @@ object Magma { * Executor used for cleaning up un-resumed sessions. */ val cleanupExecutor: ScheduledExecutorService = - Executors.newSingleThreadScheduledExecutor(threadFactory("Obsidian Magma-Cleanup")) + Executors.newSingleThreadScheduledExecutor(threadFactory("obsidian-client-cleanup")) - private val log: Logger = LoggerFactory.getLogger(Magma::class.java) + private val log = KotlinLogging.logger { } /** * Adds REST endpoint routes and websocket route @@ -83,9 +81,11 @@ object Magma { val clientName = call.request.clientName() /* log out the request */ - log.info(with(call.request) { - "${clientName ?: origin.remoteHost} ${Typography.ndash} ${httpMethod.value.padEnd(4, ' ')} $uri" - }) + log.info { + with(call.request) { + "${clientName ?: origin.remoteHost} ${Typography.ndash} ${httpMethod.value.padEnd(4, ' ')} $uri" + } + } /* check if a client name is required, if so check if there was a provided client name */ if (clientName == null && config[Obsidian.requireClientName]) { @@ -107,7 +107,7 @@ object Magma { /* check if client names are required, if so check if one was supplied */ val clientName = request.clientName() if (config[Obsidian.requireClientName] && clientName.isNullOrBlank()) { - log.warn("${request.local.remoteHost} - missing 'Client-Name' header/query parameter.") + log.warn { "${request.local.remoteHost} - missing 'Client-Name' header/query parameter." } return@webSocket close(CloseReasons.MISSING_CLIENT_NAME) } @@ -119,11 +119,11 @@ object Magma { ?: request.queryParameters["auth"] if (!Obsidian.Server.validateAuth(auth)) { - log.warn("$display - authentication failed") + log.warn { "$display - authentication failed" } return@webSocket close(CloseReasons.INVALID_AUTHORIZATION) } - log.info("$display - incoming connection") + log.info { "$display - incoming connection" } /* check for user id */ val userId = request.userId() @@ -210,7 +210,7 @@ object Magma { try { wsh.listen() } catch (ex: Exception) { - log.error("${client.displayName} - An error occurred while listening for frames.", ex) + log.error(ex) { "${client.displayName} - An error occurred while listening for frames." } if (wss.isActive) { wss.close(CloseReason(4006, ex.message ?: ex.cause?.message ?: "unknown error")) } diff --git a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt index 04389d3..400a5c6 100644 --- a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt +++ b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt @@ -25,13 +25,10 @@ import obsidian.server.io.ws.WebSocketHandler import obsidian.server.io.ws.WebSocketOpenEvent import obsidian.server.player.Player import obsidian.server.util.KoeUtil -import org.slf4j.Logger -import org.slf4j.LoggerFactory import java.net.InetSocketAddress import java.util.concurrent.ConcurrentHashMap -class MagmaClient(val userId: Long) { - +class MagmaClient(private val userId: Long) { /** * The name of this client. */ @@ -116,7 +113,7 @@ class MagmaClient(val userId: Long) { koe.close() } - inner class EventAdapterImpl(val connection: MediaConnection) : KoeEventAdapter() { + inner class EventAdapterImpl(private val connection: MediaConnection) : KoeEventAdapter() { override fun gatewayReady(target: InetSocketAddress, ssrc: Int) { websocket?.launch { val event = WebSocketOpenEvent( @@ -142,8 +139,4 @@ class MagmaClient(val userId: Long) { } } } - - companion object { - val log: Logger = LoggerFactory.getLogger(MagmaClient::class.java) - } } diff --git a/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt b/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt index 9173b5f..44c9a8c 100644 --- a/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/io/RoutePlannerUtil.kt @@ -16,10 +16,7 @@ package obsidian.server.io -import com.sedmelluq.lava.extensions.youtuberotator.planner.AbstractRoutePlanner -import com.sedmelluq.lava.extensions.youtuberotator.planner.NanoIpRoutePlanner -import com.sedmelluq.lava.extensions.youtuberotator.planner.RotatingIpRoutePlanner -import com.sedmelluq.lava.extensions.youtuberotator.planner.RotatingNanoIpRoutePlanner +import com.sedmelluq.lava.extensions.iprotator.planner.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.util.* diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 1edb33d..5b441f5 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -18,25 +18,30 @@ package obsidian.server.io.rest import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import com.sedmelluq.discord.lavaplayer.track.AudioTrackCollectionType import io.ktor.application.* import io.ktor.auth.* import io.ktor.locations.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import kotlinx.serialization.* import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonTransformingSerializer import obsidian.server.Application import obsidian.server.util.TrackUtil import obsidian.server.util.kxs.AudioTrackSerializer import obsidian.server.util.search.AudioLoader import obsidian.server.util.search.LoadType -import org.slf4j.Logger -import org.slf4j.LoggerFactory +import kotlin.reflect.jvm.jvmName fun Routing.tracks() = authenticate { get { data -> @@ -44,11 +49,18 @@ fun Routing.tracks() = authenticate { .load(data.identifier, Application.players) .await() - val playlist = result.playlistName?.let { - LoadTracks.Response.PlaylistInfo(name = it, selectedTrack = result.selectedTrack, url = data.identifier) + val collection = if (result.loadResultType != LoadType.FAILED) { + LoadTracks.Response.CollectionInfo( + name = result.collectionName!!, + selectedTrack = result.selectedTrack, + url = if (result.collectionType!! is AudioTrackCollectionType.SearchResult) null else data.identifier, + type = result.collectionType + ) + } else { + null } - val exception = if (result.loadResultType == LoadType.LOAD_FAILED && result.exception != null) { + val exception = if (result.loadResultType == LoadType.FAILED && result.exception != null) { LoadTracks.Response.Exception( message = result.exception!!.localizedMessage, severity = result.exception!!.severity @@ -60,7 +72,7 @@ fun Routing.tracks() = authenticate { val response = LoadTracks.Response( tracks = result.tracks.map(::getTrack), type = result.loadResultType, - playlistInfo = playlist, + collectionInfo = collection, exception = exception ) @@ -121,23 +133,22 @@ data class LoadTracks(val identifier: String) { data class Response( @SerialName("load_type") val type: LoadType, - @SerialName("playlist_info") - val playlistInfo: PlaylistInfo?, + @SerialName("collection_info") + val collectionInfo: CollectionInfo?, val tracks: List, val exception: Exception? ) { @Serializable - data class Exception( - val message: String, - val severity: FriendlyException.Severity - ) + data class Exception(val message: String, val severity: FriendlyException.Severity) @Serializable - data class PlaylistInfo( + data class CollectionInfo( val name: String, + val url: String?, @SerialName("selected_track") val selectedTrack: Int?, - val url: String + @Serializable(with = AudioTrackCollectionTypeSerializer::class) + val type: AudioTrackCollectionType ) } } @@ -152,7 +163,7 @@ data class Track( data class Info( val title: String, val author: String, - val uri: String, + val uri: String?, val identifier: String, val length: Long, val position: Long, @@ -165,6 +176,25 @@ data class Track( ) } +object AudioTrackCollectionTypeSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("lavaplayer.AudioTrackCollectionType") { + element("name", String.serializer().descriptor) + element("d", JsonObject.serializer().descriptor) + } + + @OptIn(InternalSerializationApi::class) + override fun serialize(encoder: Encoder, value: AudioTrackCollectionType) { + with (encoder.beginStructure(descriptor)) { + encodeStringElement(descriptor, 0, value::class.simpleName ?: value::class.jvmName) + encodeSerializableElement(descriptor, 1, value::class.serializer() as SerializationStrategy, value) + endStructure(descriptor) + } + } + + override fun deserialize(decoder: Decoder): AudioTrackCollectionType = + TODO("Not Supported") +} + object AudioTrackListSerializer : JsonTransformingSerializer>(ListSerializer(AudioTrackSerializer)) { override fun transformDeserialize(element: JsonElement): JsonElement = if (element !is JsonArray) { diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt index 816ca93..45c1a9d 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt @@ -78,6 +78,16 @@ sealed class Dispatch { encodeSerializableElement(descriptor, 0, Op, Op.PLAYER_EVENT) encodeSerializableElement(descriptor, 1, WebSocketClosedEvent.serializer(), value) } + + is Hello -> { + encodeSerializableElement(descriptor, 0, Op, Op.HELLO) + encodeSerializableElement(descriptor, 1, Hello.serializer(), value) + } + + is Resumed -> { + encodeSerializableElement(descriptor, 0, Op, Op.RESUMED) + encodeSerializableElement(descriptor, 1, Resumed.serializer(), value) + } } endStructure(descriptor) @@ -222,6 +232,12 @@ data class TrackExceptionEvent( } } +@Serializable +data class Hello(val id: String) : Dispatch() + +@Serializable +data class Resumed(val id: String, val players: List) : Dispatch() + @Serializable data class Stats( val memory: Memory, diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt index f2d3126..a78a99e 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Op.kt @@ -27,6 +27,8 @@ import kotlinx.serialization.encoding.Encoder @Serializable(with = Op.Serializer::class) enum class Op(val code: Short) { SUBMIT_VOICE_UPDATE(0), + HELLO(13), + RESUMED(14), STATS(1), SETUP_RESUMING(2), diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt index dacc289..3e51903 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt @@ -24,13 +24,12 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.* import moe.kyokobot.koe.VoiceServerInfo +import mu.KotlinLogging import obsidian.server.Application.json import obsidian.server.io.Handlers import obsidian.server.io.Magma.cleanupExecutor import obsidian.server.io.MagmaClient import obsidian.server.util.threadFactory -import org.slf4j.Logger -import org.slf4j.LoggerFactory import java.lang.Runnable import java.util.concurrent.* import java.util.concurrent.CancellationException @@ -38,6 +37,17 @@ import kotlin.coroutines.CoroutineContext import kotlin.time.ExperimentalTime class WebSocketHandler(val client: MagmaClient, private var session: WebSocketServerSession) : CoroutineScope { + companion object { + fun ReceiveChannel.asFlow() = flow { + try { + for (event in this@asFlow) emit(event) + } catch (ex: CancellationException) { + // no-op + } + } + + private val log = KotlinLogging.logger { } + } /** * Resume key @@ -48,7 +58,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe * Stats interval. */ private var stats = - Executors.newSingleThreadScheduledExecutor(threadFactory("Magma Stats-Dispatcher %d", daemon = true)) + Executors.newSingleThreadScheduledExecutor(threadFactory("obsidian-stats-dispatcher-%d", daemon = true)) /** * Whether this magma client is active @@ -81,7 +91,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe private val events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + Job() + get() = Dispatchers.IO + SupervisorJob() init { /* websocket and rest operations */ @@ -123,12 +133,12 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe resumeKey = key resumeTimeout = timeout - log.debug("${client.displayName} - Resuming has been configured; key= $key, timeout= $timeout") + log.debug { "${client.displayName} - Resuming has been configured; key= $key, timeout= $timeout" } } on { bufferTimeout = timeout - log.debug("${client.displayName} - Dispatch buffer timeout: $timeout") + log.debug { "${client.displayName} - Dispatch buffer timeout: $timeout" } } } @@ -156,7 +166,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe } } - log.info("${client.displayName} - web-socket session has closed.") + log.info { "${client.displayName} - web-socket session has closed." } /* connection has been closed. */ active = false @@ -178,7 +188,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe } resumeTimeoutFuture = cleanupExecutor.schedule(runnable, resumeTimeout!!, TimeUnit.MILLISECONDS) - log.info("${client.displayName} - Session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"") + log.info { "${client.displayName} - Session can be resumed within the next $resumeTimeout ms with the key \"$resumeKey\"" } return } @@ -189,7 +199,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe * Resumes this session */ suspend fun resume(session: WebSocketServerSession) { - log.info("${client.displayName} - session has been resumed") + log.info { "${client.displayName} - session has been resumed" } this.session = session this.active = true @@ -230,7 +240,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe currentCoroutineContext().cancelChildren() currentCoroutineContext().cancel() } catch (ex: Exception) { - log.warn("${client.displayName} - Error occurred while cancelling this coroutine scope") + log.warn { "${client.displayName} - Error occurred while cancelling this coroutine scope" } } /* close the websocket session, if not already */ @@ -246,10 +256,10 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe */ private fun send(json: String, payloadName: String? = null) { try { - log.trace("${client.displayName} ${payloadName?.let { "$it " } ?: ""}<<< $json") + log.trace { "${client.displayName} ${payloadName?.let { "$it " } ?: ""}<<< $json" } session.outgoing.trySend(Frame.Text(json)) } catch (ex: Exception) { - log.error("${client.displayName} - An exception occurred while sending a json payload", ex) + log.error(ex) { "${client.displayName} - An exception occurred while sending a json payload" } } } @@ -263,7 +273,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe try { block.invoke(it) } catch (ex: Exception) { - log.error("${client.displayName} - An exception occurred while handling a command", ex) + log.error(ex) { "${client.displayName} - An exception occurred while handling a command" } } } } @@ -279,22 +289,10 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe val data = frame.data.toString(Charset.defaultCharset()) try { - log.info("${client.displayName} >>> $data") + log.info { "${client.displayName} >>> $data" } json.decodeFromString(Operation, data)?.let { events.tryEmit(it) } } catch (ex: Exception) { - log.error("${client.displayName} - An exception occurred while handling an incoming frame", ex) + log.error(ex) { "${client.displayName} - An exception occurred while handling an incoming frame" } } } - - companion object { - fun ReceiveChannel.asFlow() = flow { - try { - for (event in this@asFlow) emit(event) - } catch (ex: CancellationException) { - // no-op - } - } - - private val log: Logger = LoggerFactory.getLogger(MagmaClient::class.java) - } } diff --git a/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt b/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt index b58dc04..4bcd9c4 100644 --- a/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt +++ b/Server/src/main/kotlin/obsidian/server/player/FrameLossTracker.kt @@ -16,8 +16,8 @@ package obsidian.server.player -import com.sedmelluq.discord.lavaplayer.player.AudioPlayer -import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter +import com.sedmelluq.discord.lavaplayer.manager.AudioPlayer +import com.sedmelluq.discord.lavaplayer.manager.event.AudioEventAdapter import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason import obsidian.server.io.ws.Frames @@ -113,10 +113,10 @@ class FrameLossTracker : AudioEventAdapter() { } /* listeners */ - override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack?, reason: AudioTrackEndReason?) = end() - override fun onTrackStart(player: AudioPlayer?, track: AudioTrack?) = start() - override fun onPlayerPause(player: AudioPlayer?) = end() - override fun onPlayerResume(player: AudioPlayer?) = start() + override fun onTrackEnd(player: AudioPlayer, track: AudioTrack, endReason: AudioTrackEndReason) = end() + override fun onTrackStart(player: AudioPlayer, track: AudioTrack) = start() + override fun onPlayerPause(player: AudioPlayer) = end() + override fun onPlayerResume(player: AudioPlayer) = start() companion object { const val ONE_SECOND = 1e9 diff --git a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt index 871f62a..dba7bea 100644 --- a/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt +++ b/Server/src/main/kotlin/obsidian/server/player/ObsidianAPM.kt @@ -16,22 +16,25 @@ package obsidian.server.player -import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager -import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager -import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager -import com.sedmelluq.discord.lavaplayer.source.getyarn.GetyarnAudioSourceManager -import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager -import com.sedmelluq.discord.lavaplayer.source.local.LocalAudioSourceManager -import com.sedmelluq.discord.lavaplayer.source.nico.NicoAudioSourceManager -import com.sedmelluq.discord.lavaplayer.source.soundcloud.* -import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager -import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager -import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager +import com.sedmelluq.discord.lavaplayer.manager.DefaultAudioPlayerManager +import com.sedmelluq.discord.lavaplayer.source.ItemSourceManager +import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampItemSourceManager +import com.sedmelluq.discord.lavaplayer.source.getyarn.GetyarnItemSourceManager +import com.sedmelluq.discord.lavaplayer.source.http.HttpItemSourceManager +import com.sedmelluq.discord.lavaplayer.source.local.LocalItemSourceManager +import com.sedmelluq.discord.lavaplayer.source.nico.NicoItemSourceManager +import com.sedmelluq.discord.lavaplayer.source.soundcloud.DefaultSoundCloudFormatHandler +import com.sedmelluq.discord.lavaplayer.source.soundcloud.DefaultSoundCloudHtmlDataLoader +import com.sedmelluq.discord.lavaplayer.source.soundcloud.DefaultSoundCloudPlaylistLoader +import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudItemSourceManager +import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamItemSourceManager +import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoItemSourceManager +import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeItemSourceManager import com.sedmelluq.discord.lavaplayer.track.playback.NonAllocatingAudioFrameBuffer -import com.sedmelluq.lava.extensions.youtuberotator.YoutubeIpRotatorSetup -import com.sedmelluq.lava.extensions.youtuberotator.planner.* -import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block -import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block +import com.sedmelluq.lava.extensions.iprotator.YoutubeIpRotatorSetup +import com.sedmelluq.lava.extensions.iprotator.planner.* +import com.sedmelluq.lava.extensions.iprotator.tools.ip.Ipv4Block +import com.sedmelluq.lava.extensions.iprotator.tools.ip.Ipv6Block import obsidian.server.Application.config import obsidian.server.config.spec.Obsidian import org.slf4j.Logger @@ -76,11 +79,11 @@ class ObsidianAPM : DefaultAudioPlayerManager() { } init { - configuration.apply { - isFilterHotSwapEnabled = true + configuration { + filterHotSwapEnabled = true if (config[Obsidian.Lavaplayer.nonAllocating]) { logger.info("Using the non-allocating audio frame buffer.") - setFrameBufferFactory(::NonAllocatingAudioFrameBuffer) + useFrameBufferFactory(::NonAllocatingAudioFrameBuffer) } } @@ -96,12 +99,12 @@ class ObsidianAPM : DefaultAudioPlayerManager() { .forEach { source -> when (source.lowercase()) { "youtube" -> { - val youtube = YoutubeAudioSourceManager(config[Obsidian.Lavaplayer.YouTube.allowSearch]).apply { - setPlaylistPageCount(config[Obsidian.Lavaplayer.YouTube.playlistPageLimit]) + val youtube = YoutubeItemSourceManager(config[Obsidian.Lavaplayer.YouTube.allowSearch]).apply { + playlistPageCount = config[Obsidian.Lavaplayer.YouTube.playlistPageLimit] if (routePlanner != null) { - val rotator = YoutubeIpRotatorSetup(routePlanner) - .forSource(this) + val rotator = YoutubeIpRotatorSetup(routePlanner!!) + .applyTo(this) val retryLimit = config[Obsidian.Lavaplayer.RateLimit.retryLimit] if (retryLimit <= 0) { @@ -116,17 +119,15 @@ class ObsidianAPM : DefaultAudioPlayerManager() { } "soundcloud" -> { - val dataReader = DefaultSoundCloudDataReader() val htmlDataLoader = DefaultSoundCloudHtmlDataLoader() val formatHandler = DefaultSoundCloudFormatHandler() registerSourceManager( - SoundCloudAudioSourceManager( + SoundCloudItemSourceManager( config[Obsidian.Lavaplayer.allowScSearch], - dataReader, htmlDataLoader, formatHandler, - DefaultSoundCloudPlaylistLoader(htmlDataLoader, dataReader, formatHandler) + DefaultSoundCloudPlaylistLoader(htmlDataLoader, formatHandler) ) ) } @@ -136,16 +137,16 @@ class ObsidianAPM : DefaultAudioPlayerManager() { val password = config[Obsidian.Lavaplayer.Nico.password] if (email.isNotBlank() && password.isNotBlank()) { - registerSourceManager(NicoAudioSourceManager(email, password)) + registerSourceManager(NicoItemSourceManager(email, password)) } } - "bandcamp" -> registerSourceManager(BandcampAudioSourceManager()) - "twitch" -> registerSourceManager(TwitchStreamAudioSourceManager()) - "vimeo" -> registerSourceManager(VimeoAudioSourceManager()) - "http" -> registerSourceManager(HttpAudioSourceManager()) - "local" -> registerSourceManager(LocalAudioSourceManager()) - "yarn" -> registerSourceManager(GetyarnAudioSourceManager()) + "bandcamp" -> registerSourceManager(BandcampItemSourceManager()) + "twitch" -> registerSourceManager(TwitchStreamItemSourceManager()) + "vimeo" -> registerSourceManager(VimeoItemSourceManager()) + "http" -> registerSourceManager(HttpItemSourceManager()) + "local" -> registerSourceManager(LocalItemSourceManager()) + "yarn" -> registerSourceManager(GetyarnItemSourceManager()) else -> logger.warn("Unknown source \"$source\"") } @@ -154,7 +155,7 @@ class ObsidianAPM : DefaultAudioPlayerManager() { logger.info("Enabled sources: ${enabledSources.joinToString(", ")}") } - override fun registerSourceManager(sourceManager: AudioSourceManager) { + override fun registerSourceManager(sourceManager: ItemSourceManager) { super.registerSourceManager(sourceManager) enabledSources.add(sourceManager.sourceName) } diff --git a/Server/src/main/kotlin/obsidian/server/player/Player.kt b/Server/src/main/kotlin/obsidian/server/player/Player.kt index a928bf2..d552cc0 100644 --- a/Server/src/main/kotlin/obsidian/server/player/Player.kt +++ b/Server/src/main/kotlin/obsidian/server/player/Player.kt @@ -17,10 +17,11 @@ package obsidian.server.player import com.sedmelluq.discord.lavaplayer.format.StandardAudioDataFormats -import com.sedmelluq.discord.lavaplayer.player.AudioPlayer -import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter -import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener +import com.sedmelluq.discord.lavaplayer.manager.AudioPlayer +import com.sedmelluq.discord.lavaplayer.manager.event.AudioEventAdapter +import com.sedmelluq.discord.lavaplayer.manager.event.AudioEventListener import com.sedmelluq.discord.lavaplayer.tools.FriendlyException +import com.sedmelluq.discord.lavaplayer.tools.extensions.addListener import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame @@ -45,6 +46,11 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { PlayerUpdates(this) } + /** + * Frame loss tracker. + */ + val frameLossTracker = FrameLossTracker() + /** * Audio player for receiving frames. */ @@ -55,11 +61,6 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { .addEventListener(this) } - /** - * Frame loss tracker. - */ - val frameLossTracker = FrameLossTracker() - /** * Whether the player is currently playing a track. */ @@ -91,15 +92,15 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { "A track must be playing in order to seek." } - require(audioPlayer.playingTrack.isSeekable) { + require(audioPlayer.playingTrack!!.isSeekable) { "The playing track is not seekable." } - require(position in 0..audioPlayer.playingTrack.duration) { + require(position in 0..audioPlayer.playingTrack!!.duration) { "The given position must be within 0 and the current playing track's duration." } - audioPlayer.playingTrack.position = position + audioPlayer.playingTrack?.position = position } /** @@ -112,7 +113,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { /** * */ - override fun onTrackStuck(player: AudioPlayer?, track: AudioTrack?, thresholdMs: Long) { + override fun onTrackStuck(player: AudioPlayer, track: AudioTrack, thresholdMs: Long) { client.websocket?.let { val event = TrackStuckEvent( guildId = guildId, @@ -127,7 +128,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { /** * */ - override fun onTrackException(player: AudioPlayer?, track: AudioTrack, exception: FriendlyException) { + override fun onTrackException(player: AudioPlayer, track: AudioTrack, exception: FriendlyException) { client.websocket?.let { val event = TrackExceptionEvent( guildId = guildId, @@ -142,7 +143,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { /** * */ - override fun onTrackStart(player: AudioPlayer?, track: AudioTrack) { + override fun onTrackStart(player: AudioPlayer, track: AudioTrack) { client.websocket?.let { val event = TrackStartEvent( guildId = guildId, @@ -156,7 +157,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { /** * Sends a track end player event to the websocket connection, if any. */ - override fun onTrackEnd(player: AudioPlayer?, track: AudioTrack, reason: AudioTrackEndReason) { + override fun onTrackEnd(player: AudioPlayer, track: AudioTrack, reason: AudioTrackEndReason) { client.websocket?.let { val event = TrackEndEvent( track = track, @@ -178,7 +179,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { inner class OpusFrameProvider(connection: MediaConnection) : OpusAudioFrameProvider(connection) { - private val frameBuffer = ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize()) + private val frameBuffer = ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize) private val lastFrame = MutableAudioFrame() init { diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index 7b98c0d..00bfbe2 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -16,7 +16,7 @@ package obsidian.server.player -import com.sedmelluq.discord.lavaplayer.player.AudioPlayer +import com.sedmelluq.discord.lavaplayer.manager.AudioPlayer import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason import kotlinx.coroutines.launch @@ -92,9 +92,9 @@ class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { */ fun currentTrackFor(player: Player): CurrentTrack = CurrentTrack( - track = TrackUtil.encode(player.audioPlayer.playingTrack), + track = TrackUtil.encode(player.audioPlayer.playingTrack!!), paused = player.audioPlayer.isPaused, - position = player.audioPlayer.playingTrack.position + position = player.audioPlayer.playingTrack!!.position ) } } diff --git a/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt index 133106c..80582d3 100644 --- a/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt +++ b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt @@ -16,8 +16,8 @@ package obsidian.server.util -import com.sedmelluq.discord.lavaplayer.player.AudioPlayer -import com.sedmelluq.discord.lavaplayer.player.event.* +import com.sedmelluq.discord.lavaplayer.manager.AudioPlayer +import com.sedmelluq.discord.lavaplayer.manager.event.* import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason @@ -43,16 +43,14 @@ open class CoroutineAudioEventAdapter(private val dispatcher: CoroutineDispatche open suspend fun onPlayerResume(player: AudioPlayer) = Unit open suspend fun onPlayerPause(player: AudioPlayer) = Unit - override fun onEvent(event: AudioEvent) { - launch { - when (event) { - is TrackStartEvent -> onTrackStart(event.player, event.track) - is TrackEndEvent -> onTrackEnd(event.player, event.track, event.endReason) - is TrackStuckEvent -> onTrackStuck(event.player, event.track, event.thresholdMs) - is TrackExceptionEvent -> onTrackException(event.player, event.track, event.exception) - is PlayerResumeEvent -> onPlayerResume(event.player) - is PlayerPauseEvent -> onPlayerPause(event.player) - } + override suspend fun onEvent(event: AudioEvent) { + when (event) { + is TrackStartEvent -> onTrackStart(event.player, event.track) + is TrackEndEvent -> onTrackEnd(event.player, event.track, event.endReason) + is TrackStuckEvent -> onTrackStuck(event.player, event.track, event.thresholdMs) + is TrackExceptionEvent -> onTrackException(event.player, event.track, event.exception) + is PlayerResumeEvent -> onPlayerResume(event.player) + is PlayerPauseEvent -> onPlayerPause(event.player) } } } diff --git a/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt index 1122be0..90f43e2 100644 --- a/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/KoeUtil.kt @@ -25,14 +25,12 @@ import moe.kyokobot.koe.codec.FramePollerFactory import moe.kyokobot.koe.codec.netty.NettyFramePollerFactory import moe.kyokobot.koe.codec.udpqueue.UdpQueueFramePollerFactory import moe.kyokobot.koe.gateway.GatewayVersion +import mu.KotlinLogging import obsidian.server.Application.config import obsidian.server.config.spec.Obsidian -import org.slf4j.Logger -import org.slf4j.LoggerFactory object KoeUtil { - - private val log: Logger = LoggerFactory.getLogger(KoeUtil::class.java) + private val log = KotlinLogging.logger { } /** * The koe instance @@ -55,7 +53,7 @@ object KoeUtil { 5 -> GatewayVersion.V5 4 -> GatewayVersion.V4 else -> { - log.info("Invalid gateway version, defaulting to v5.") + log.info { "Invalid gateway version, defaulting to v5." } GatewayVersion.V5 } } @@ -67,7 +65,7 @@ object KoeUtil { private val framePollerFactory: FramePollerFactory by lazy { when { NativeUtil.udpQueueAvailable && config[Obsidian.Koe.UdpQueue.enabled] -> { - log.info("Enabling udp-queue") + log.info { "Enabling udp-queue" } UdpQueueFramePollerFactory( config[Obsidian.Koe.UdpQueue.bufferDuration], config[Obsidian.Koe.UdpQueue.poolSize] @@ -76,10 +74,7 @@ object KoeUtil { else -> { if (config[Obsidian.Koe.UdpQueue.enabled]) { - log.warn( - "This system and/or architecture appears to not support native audio sending, " - + "GC pauses may cause your bot to stutter during playback." - ) + log.warn { "This system and/or architecture appears to not support native audio sending, GC pauses may cause your bot to stutter during playback." } } NettyFramePollerFactory() @@ -96,7 +91,7 @@ object KoeUtil { "netty-default" -> ByteBufAllocator.DEFAULT "unpooled" -> UnpooledByteBufAllocator.DEFAULT else -> { - log.warn("Unknown byte-buf allocator '${configured}', defaulting to 'pooled'.") + log.warn { "Unknown byte-buf allocator '${configured}', defaulting to 'pooled'." } PooledByteBufAllocator.DEFAULT } } diff --git a/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt b/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt index b0a10cd..85ae603 100644 --- a/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt +++ b/Server/src/main/kotlin/obsidian/server/util/LogbackColorConverter.kt @@ -21,7 +21,6 @@ import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.pattern.CompositeConverter class LogbackColorConverter : CompositeConverter() { - companion object { val Number.ansi: String get() = "\u001b[${this}m" @@ -58,5 +57,4 @@ class LogbackColorConverter : CompositeConverter() { fun interface Convert { fun take(str: String): String } - } diff --git a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt index 9abafbb..8fe9991 100644 --- a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt @@ -20,13 +20,12 @@ import com.github.natanbc.lavadsp.natives.TimescaleNativeLibLoader import com.github.natanbc.nativeloader.NativeLibLoader import com.sedmelluq.discord.lavaplayer.natives.ConnectorNativeLibLoader import com.sedmelluq.discord.lavaplayer.udpqueue.natives.UdpQueueManagerLibrary -import org.slf4j.Logger -import org.slf4j.LoggerFactory +import mu.KotlinLogging /** * Based on https://github.com/natanbc/andesite/blob/master/src/main/java/andesite/util/NativeUtils.java * - * i want native stuff too :'( + * I want native stuff too :'( */ object NativeUtil { @@ -34,7 +33,7 @@ object NativeUtil { var udpQueueAvailable: Boolean = false /* private shit */ - private val logger: Logger = LoggerFactory.getLogger(NativeUtil::class.java) + private val logger = KotlinLogging.logger { } // loaders private val CONNECTOR_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "connector") @@ -87,7 +86,7 @@ object NativeUtil { for (i in 0 until 2) { // wtf natan - markLoaded(java.lang.reflect.Array.get(loadersField.get(null), i)) + markLoaded((loadersField.get(null) as List)[0]) } logger.info("Connector loaded") diff --git a/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt b/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt index 3e23516..aa86f2c 100644 --- a/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt @@ -49,7 +49,7 @@ object TrackUtil { fun decode(encodedTrack: String): AudioTrack { val inputStream = ByteArrayInputStream(decoder.decode(encodedTrack)) return inputStream.use { - players.decodeTrack(MessageInput(it))!!.decodedTrack + players.decodeTrack(MessageInput(it))!!.decodedTrack!! } } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt index 1c9bcda..860087e 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt @@ -16,52 +16,51 @@ package obsidian.server.util.search - -import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler -import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager +import com.sedmelluq.discord.lavaplayer.manager.AudioPlayerManager import com.sedmelluq.discord.lavaplayer.tools.FriendlyException -import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import com.sedmelluq.discord.lavaplayer.track.AudioTrackCollection +import com.sedmelluq.discord.lavaplayer.track.loader.ItemLoadResultAdapter import kotlinx.coroutines.CompletableDeferred -import org.slf4j.LoggerFactory - -class AudioLoader(private val deferred: CompletableDeferred) : AudioLoadResultHandler { +import mu.KotlinLogging +class AudioLoader(private val deferred: CompletableDeferred) : ItemLoadResultAdapter() { companion object { - private val logger = LoggerFactory.getLogger(AudioLoader::class.java) + private val logger = KotlinLogging.logger { } - fun load(identifier: String, playerManager: AudioPlayerManager) = CompletableDeferred().also { - val handler = AudioLoader(it) - playerManager.loadItem(identifier, handler) - } + suspend fun load(identifier: String, playerManager: AudioPlayerManager) = + CompletableDeferred().also { + val itemLoader = playerManager.items.createItemLoader(identifier) + itemLoader.resultHandler = AudioLoader(it) + itemLoader.load() + } } - override fun trackLoaded(audioTrack: AudioTrack) { - logger.info("Loaded track ${audioTrack.info.title}") - deferred.complete(LoadResult(LoadType.TRACK_LOADED, listOf(audioTrack), null, null)) + override fun onTrackLoad(track: AudioTrack) { + logger.info { "Loaded track ${track.info.title}" } + deferred.complete(LoadResult(LoadType.TRACK, listOf(track), null, null)) } - override fun playlistLoaded(audioPlaylist: AudioPlaylist) { - logger.info("Loaded playlist: ${audioPlaylist.name}") - - val result = if (audioPlaylist.isSearchResult) { - LoadResult(LoadType.SEARCH_RESULT, audioPlaylist.tracks, null, null) - } else { - val selectedTrack = audioPlaylist.tracks.indexOf(audioPlaylist.selectedTrack) - LoadResult(LoadType.PLAYLIST_LOADED, audioPlaylist.tracks, audioPlaylist.name, selectedTrack) - } + override fun onCollectionLoad(collection: AudioTrackCollection) { + logger.info { "Loaded playlist: ${collection.name}" } + val result = LoadResult( + LoadType.TRACK_COLLECTION, + collection.tracks, + collection.name, + collection.type, + collection.selectedTrack?.let { collection.tracks.indexOf(it) } + ) deferred.complete(result) } override fun noMatches() { - logger.info("No matches found") + logger.info { "No matches found." } deferred.complete(LoadResult()) } - override fun loadFailed(e: FriendlyException) { - logger.error("Failed to load", e) - deferred.complete(LoadResult(e)) + override fun onLoadFailed(exception: FriendlyException) { + logger.error(exception) { "Failed to load." } + deferred.complete(LoadResult(exception)) } - } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt index 47b39f5..4924dc7 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt @@ -18,19 +18,19 @@ package obsidian.server.util.search import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import com.sedmelluq.discord.lavaplayer.track.AudioTrackCollectionType class LoadResult( - val loadResultType: LoadType = LoadType.NO_MATCHES, + val loadResultType: LoadType = LoadType.NONE, val tracks: List = emptyList(), - val playlistName: String? = null, + val collectionName: String? = null, + val collectionType: AudioTrackCollectionType? = null, val selectedTrack: Int? = null, ) { - var exception: FriendlyException? = null private set - constructor(exception: FriendlyException) : this(LoadType.LOAD_FAILED) { + constructor(exception: FriendlyException) : this(LoadType.FAILED) { this.exception = exception } - } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt b/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt index ce7d276..7f0d805 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/LoadType.kt @@ -17,9 +17,8 @@ package obsidian.server.util.search enum class LoadType { - TRACK_LOADED, - PLAYLIST_LOADED, - SEARCH_RESULT, - NO_MATCHES, - LOAD_FAILED + TRACK, + TRACK_COLLECTION, + NONE, + FAILED } From 82d98806701f835cbdf03df39f7110c7a24c721a Mon Sep 17 00:00:00 2001 From: melike2d Date: Tue, 26 Oct 2021 21:49:43 -0700 Subject: [PATCH 33/46] :sparkles: add a search timeout --- .../obsidian/server/config/spec/Obsidian.kt | 5 ++++ .../kotlin/obsidian/server/io/rest/tracks.kt | 27 ++++++++++++------- obsidian.yml | 1 + 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt index 03a02c7..98093f5 100644 --- a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt +++ b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt @@ -119,6 +119,11 @@ object Obsidian : ConfigSpec() { */ val nonAllocating by optional(false, "non-allocating") + /** + * Timeout (in milliseconds) for track searching. + */ + val searchTimeout by optional(10_000L, "search-timeout") + /** * Names of sources that will be enabled. */ diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 5b441f5..757c6a8 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -25,6 +25,7 @@ import io.ktor.locations.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* +import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.* import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.serializer @@ -37,6 +38,8 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonTransformingSerializer import obsidian.server.Application +import obsidian.server.Application.config +import obsidian.server.config.spec.Obsidian import obsidian.server.util.TrackUtil import obsidian.server.util.kxs.AudioTrackSerializer import obsidian.server.util.search.AudioLoader @@ -45,11 +48,12 @@ import kotlin.reflect.jvm.jvmName fun Routing.tracks() = authenticate { get { data -> - val result = AudioLoader - .load(data.identifier, Application.players) - .await() + val result = withTimeoutOrNull(config[Obsidian.Lavaplayer.searchTimeout]) { + AudioLoader.load(data.identifier, Application.players) + } + ?: return@get context.respond(LoadTracks.Response(LoadType.NONE)) - val collection = if (result.loadResultType != LoadType.FAILED) { + val collection = if (result.loadResultType == LoadType.TRACK_COLLECTION) { LoadTracks.Response.CollectionInfo( name = result.collectionName!!, selectedTrack = result.selectedTrack, @@ -134,9 +138,9 @@ data class LoadTracks(val identifier: String) { @SerialName("load_type") val type: LoadType, @SerialName("collection_info") - val collectionInfo: CollectionInfo?, - val tracks: List, - val exception: Exception? + val collectionInfo: CollectionInfo? = null, + val tracks: List = emptyList(), + val exception: Exception? = null ) { @Serializable data class Exception(val message: String, val severity: FriendlyException.Severity) @@ -184,9 +188,14 @@ object AudioTrackCollectionTypeSerializer : KSerializer, value) + encodeSerializableElement( + descriptor, + 1, + value::class.serializer() as SerializationStrategy, + value + ) endStructure(descriptor) } } diff --git a/obsidian.yml b/obsidian.yml index 102ef38..cb6b675 100644 --- a/obsidian.yml +++ b/obsidian.yml @@ -13,6 +13,7 @@ obsidian: non-allocating: false enabled-sources: [ "youtube", "yarn", "bandcamp", "twitch", "vimeo", "nico", "soundcloud", "local", "http" ] allow-scsearch: true + search-timeout: 15000 rate-limit: ip-blocks: [ ] excluded-ips: [ ] From 006abff79b1ca07bde650be3d86797276d4b4dc6 Mon Sep 17 00:00:00 2001 From: melike2d Date: Tue, 26 Oct 2021 21:50:10 -0700 Subject: [PATCH 34/46] :rotating_light: weird compiler warning tings --- Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt | 2 +- Server/src/main/kotlin/obsidian/server/player/Player.kt | 4 ++-- Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt index a94f1e3..62f747e 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Operation.kt @@ -29,13 +29,13 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject sealed class Operation { + @OptIn(ExperimentalSerializationApi::class) companion object : DeserializationStrategy { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Operation") { element("op", Op.descriptor) element("d", JsonObject.serializer().descriptor, isOptional = true) } - @ExperimentalSerializationApi override fun deserialize(decoder: Decoder): Operation? { var op: Op? = null var data: Operation? = null diff --git a/Server/src/main/kotlin/obsidian/server/player/Player.kt b/Server/src/main/kotlin/obsidian/server/player/Player.kt index d552cc0..909cc6c 100644 --- a/Server/src/main/kotlin/obsidian/server/player/Player.kt +++ b/Server/src/main/kotlin/obsidian/server/player/Player.kt @@ -157,11 +157,11 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { /** * Sends a track end player event to the websocket connection, if any. */ - override fun onTrackEnd(player: AudioPlayer, track: AudioTrack, reason: AudioTrackEndReason) { + override fun onTrackEnd(player: AudioPlayer, track: AudioTrack, endReason: AudioTrackEndReason) { client.websocket?.let { val event = TrackEndEvent( track = track, - endReason = reason, + endReason = endReason, guildId = guildId ) diff --git a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt index 8fe9991..74b4f40 100644 --- a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt @@ -75,7 +75,7 @@ object NativeUtil { } /** - * Loads the lp-cross version of lavaplayer's loader + * Loads the connector natives from lp-cross */ private fun loadConnector() { try { @@ -86,7 +86,7 @@ object NativeUtil { for (i in 0 until 2) { // wtf natan - markLoaded((loadersField.get(null) as List)[0]) + markLoaded((loadersField.get(null) as List<*>)[0]) } logger.info("Connector loaded") @@ -116,7 +116,9 @@ object NativeUtil { false } - private fun markLoaded(loader: Any) { + private fun markLoaded(loader: Any?) { + require (loader != null) + val previousResultField = loader.javaClass.getDeclaredField("previousResult") previousResultField.isAccessible = true previousResultField[loader] = LOAD_RESULT From 05305734ee4bba7fd52ab05d2a446f88fea9d816 Mon Sep 17 00:00:00 2001 From: melike2d Date: Tue, 26 Oct 2021 21:50:33 -0700 Subject: [PATCH 35/46] :sparkles: make AudioLoader.load return the load result instead of a Deferred --- .../src/main/kotlin/obsidian/server/util/search/AudioLoader.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt index 860087e..89606f5 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt @@ -34,6 +34,7 @@ class AudioLoader(private val deferred: CompletableDeferred) : ItemL itemLoader.resultHandler = AudioLoader(it) itemLoader.load() } + .await() } override fun onTrackLoad(track: AudioTrack) { From 2be394128f70c75db00dcd7ca3518da62e1e5b4c Mon Sep 17 00:00:00 2001 From: melike2d Date: Tue, 26 Oct 2021 21:50:45 -0700 Subject: [PATCH 36/46] Rename .java to .kt --- .../udpqueue/natives/{UdpQueueManager.java => UdpQueueManager.kt} | 0 .../{UdpQueueManagerLibrary.java => UdpQueueManagerLibrary.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/{UdpQueueManager.java => UdpQueueManager.kt} (100%) rename Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/{UdpQueueManagerLibrary.java => UdpQueueManagerLibrary.kt} (100%) diff --git a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.java b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.kt similarity index 100% rename from Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.java rename to Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.kt diff --git a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.java b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt similarity index 100% rename from Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.java rename to Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt From 90bc75f1c326761e6547910eb1f5d5d0fc93111a Mon Sep 17 00:00:00 2001 From: melike2d Date: Tue, 26 Oct 2021 21:50:45 -0700 Subject: [PATCH 37/46] :sparkles: convert udpqueue things into kotlin --- .../udpqueue/natives/UdpQueueManager.kt | 104 +++++++----------- .../natives/UdpQueueManagerLibrary.kt | 54 ++++----- 2 files changed, 63 insertions(+), 95 deletions(-) diff --git a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.kt b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.kt index f1fd70c..c61242a 100644 --- a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.kt +++ b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManager.kt @@ -13,35 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.sedmelluq.discord.lavaplayer.udpqueue.natives -package com.sedmelluq.discord.lavaplayer.udpqueue.natives; - -import com.sedmelluq.lava.common.natives.NativeResourceHolder; - -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; +import com.sedmelluq.lava.common.natives.NativeResourceHolder +import java.net.InetSocketAddress +import java.nio.ByteBuffer /** * Manages sending out queues of UDP packets at a fixed interval. + * + * @param capacity Maximum number of packets in one queue + * @param packetInterval Time interval between packets in a queue + * @param maximumPacketSize Maximum packet size */ -public class UdpQueueManager extends NativeResourceHolder { - private final int bufferCapacity; - private final ByteBuffer packetBuffer; - private final UdpQueueManagerLibrary library; - private final long instance; - private boolean released; - - /** - * @param bufferCapacity Maximum number of packets in one queue - * @param packetInterval Time interval between packets in a queue - * @param maximumPacketSize Maximum packet size - */ - public UdpQueueManager(int bufferCapacity, long packetInterval, int maximumPacketSize) { - this.bufferCapacity = bufferCapacity; - packetBuffer = ByteBuffer.allocateDirect(maximumPacketSize); - library = UdpQueueManagerLibrary.getInstance(); - instance = library.create(bufferCapacity, packetInterval); - } +class UdpQueueManager(capacity: Int, packetInterval: Long, maximumPacketSize: Int) : NativeResourceHolder() { + private val packetBuffer: ByteBuffer = ByteBuffer.allocateDirect(maximumPacketSize) + private val library: UdpQueueManagerLibrary = UdpQueueManagerLibrary.instance + private val instance: Long = library.create(capacity, packetInterval) + private var released = false /** * If the queue does not exist yet, returns the maximum number of packets in a queue. @@ -49,23 +38,12 @@ public class UdpQueueManager extends NativeResourceHolder { * @param key Unique queue identifier * @return Number of empty packet slots in the specified queue */ - public int getRemainingCapacity(long key) { - synchronized (library) { - if (released) { - return 0; - } - - return library.getRemainingCapacity(instance, key); + fun getRemainingCapacity(key: Long): Int { + synchronized(library) { + return if (released) 0 else library.getRemainingCapacity(instance, key) } } - /** - * @return Total capacity used for queues in this manager. - */ - public int getCapacity() { - return bufferCapacity; - } - /** * Adds one packet to the specified queue. Will fail if the maximum size of the queue is reached. There is no need to * manually create a queue, it is automatically created when the first packet is added to it and deleted when it @@ -75,19 +53,17 @@ public class UdpQueueManager extends NativeResourceHolder { * @param packet Packet to add to the queue * @return True if adding the packet to the queue succeeded */ - public boolean queuePacket(long key, ByteBuffer packet, InetSocketAddress address) { - synchronized (library) { + fun queuePacket(key: Long, packet: ByteBuffer, address: InetSocketAddress): Boolean { + synchronized(library) { if (released) { - return false; + return false } - int length = packet.remaining(); - packetBuffer.clear(); - packetBuffer.put(packet); + val length = packet.remaining() + packetBuffer.clear() + packetBuffer.put(packet) - int port = address.getPort(); - String hostAddress = address.getAddress().getHostAddress(); - return library.queuePacket(instance, key, hostAddress, port, packetBuffer, length); + return library.queuePacket(instance, key, address.address.hostAddress, address.port, packetBuffer, length) } } @@ -95,27 +71,27 @@ public class UdpQueueManager extends NativeResourceHolder { * This is the method that should be called to start processing the queues. It will use the current thread and return * only when close() method is called on the queue manager. */ - public void process() { - library.process(instance); + fun process() { + library.process(instance) } - @Override - protected void freeResources() { - synchronized (library) { - released = true; - library.destroy(instance); + override fun freeResources() { + synchronized(library) { + released = true + library.destroy(instance) } } - /** - * Simulate a GC pause stop-the-world by starting a heap iteration via JVMTI. The behaviour of this stop-the-world is - * identical to that of an actual GC pause, so nothing in Java can execute during the pause. - * - * @param length Length of the pause in milliseconds - */ - public static void pauseDemo(int length) { - UdpQueueManagerLibrary.getInstance(); - UdpQueueManagerLibrary.pauseDemo(length); + companion object { + /** + * Simulate a GC pause stop-the-world by starting a heap iteration via JVMTI. The behaviour of this stop-the-world is + * identical to that of an actual GC pause, so nothing in Java can execute during the pause. + * + * @param length Length of the pause in milliseconds + */ + fun pauseDemo(length: Int) { + UdpQueueManagerLibrary.instance + UdpQueueManagerLibrary.pauseDemo(length) + } } } - diff --git a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt index 6c5f12d..991151f 100644 --- a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt +++ b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt @@ -14,43 +14,35 @@ * limitations under the License. */ -package com.sedmelluq.discord.lavaplayer.udpqueue.natives; +package com.sedmelluq.discord.lavaplayer.udpqueue.natives +import com.sedmelluq.lava.common.natives.NativeLibraryLoader +import java.nio.ByteBuffer -import com.sedmelluq.lava.common.natives.NativeLibraryLoader; +class UdpQueueManagerLibrary private constructor() { + external fun create(bufferCapacity: Int, packetInterval: Long): Long + external fun deleteQueue(instance: Long, key: Long): Boolean + external fun destroy(instance: Long) -import java.nio.ByteBuffer; + external fun getRemainingCapacity(instance: Long, key: Long): Int -public class UdpQueueManagerLibrary { - private static final NativeLibraryLoader nativeLoader = - NativeLibraryLoader.create(UdpQueueManagerLibrary.class, "udpqueue"); + external fun queuePacket(instance: Long, key: Long, address: String?, port: Int, dataDirectBuffer: ByteBuffer?, dataLength: Int): Boolean + external fun queuePacketWithSocket(instance: Long, key: Long, address: String?, port: Int, dataDirectBuffer: ByteBuffer?, dataLength: Int, explicitSocket: Long): Boolean - private UdpQueueManagerLibrary() { + external fun process(instance: Long) + external fun processWithSocket(instance: Long, ipv4Handle: Long, ipv6Handle: Long) - } - - public static UdpQueueManagerLibrary getInstance() { - nativeLoader.load(); - return new UdpQueueManagerLibrary(); - } - - public native long create(int bufferCapacity, long packetInterval); - - public native void destroy(long instance); - - public native int getRemainingCapacity(long instance, long key); + companion object { + private val nativeLoader = NativeLibraryLoader.create(UdpQueueManagerLibrary::class.java, "udpqueue") - public native boolean queuePacket(long instance, long key, String address, int port, ByteBuffer dataDirectBuffer, - int dataLength); + @JvmStatic + val instance: UdpQueueManagerLibrary + get() { + nativeLoader.load() + return UdpQueueManagerLibrary() + } - public native boolean queuePacketWithSocket(long instance, long key, String address, int port, - ByteBuffer dataDirectBuffer, int dataLength, long explicitSocket); - - public native boolean deleteQueue(long instance, long key); - - public native void process(long instance); - - public native void processWithSocket(long instance, long ipv4Handle, long ipv6Handle); - - public static native void pauseDemo(int length); + @JvmStatic + external fun pauseDemo(length: Int) + } } From 352a94556f1d1d85805d792f82c0bc87c6766f53 Mon Sep 17 00:00:00 2001 From: melike2d Date: Tue, 26 Oct 2021 21:50:53 -0700 Subject: [PATCH 38/46] :sparkles: fix natives --- Server/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 1c0a150..576a724 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -46,7 +46,7 @@ dependencies { implementation("com.github.natanbc:lavadsp:0.7.7") // audio filters implementation("com.github.natanbc:native-loader:0.7.2") // native loader - implementation("com.github.natanbc:lp-cross:0.1.3-1") // lp-cross natives + implementation("com.github.natanbc:lp-cross:0.2") // lp-cross natives /* logging */ implementation("ch.qos.logback:logback-classic:1.2.6") // slf4j logging backend From 077ab386fd161552a1955d5edfd8603276a13661 Mon Sep 17 00:00:00 2001 From: melike2d Date: Wed, 27 Oct 2021 13:42:55 -0700 Subject: [PATCH 39/46] :sparkles: push changes to actually fix natives --- Server/build.gradle.kts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 576a724..c28adbb 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -36,21 +36,19 @@ dependencies { /* audio */ implementation("moe.kyokobot.koe:core:koe-v2-SNAPSHOT") // discord send system - implementation("com.sedmelluq:lavaplayer:1.5.2") { // yes - exclude(group = "com.sedmelluq", module = "lavaplayer-natives") - } + implementation("com.sedmelluq:lavaplayer:1.5.2") // lavaplayer + implementation("com.sedmelluq:udp-queue-natives:2.0.0") // udp-queue natives implementation("com.sedmelluq:lavaplayer-ext-ip-rotator:0.3.0") { // ip rotation exclude(group = "com.sedmelluq", module = "lavaplayer") } - implementation("com.github.natanbc:lavadsp:0.7.7") // audio filters - implementation("com.github.natanbc:native-loader:0.7.2") // native loader - implementation("com.github.natanbc:lp-cross:0.2") // lp-cross natives + implementation("com.github.natanbc:lavadsp:0.7.7") // audio filters + implementation("com.github.natanbc:lp-cross:0.2") // lp-cross natives /* logging */ implementation("ch.qos.logback:logback-classic:1.2.6") // slf4j logging backend - implementation("io.github.microutils:kotlin-logging-jvm:2.0.10") // logging + implementation("io.github.microutils:kotlin-logging-jvm:2.0.11") // logging /* misc */ implementation("fun.dimensional:cuid:1.0.2") // CUIDs @@ -67,7 +65,7 @@ tasks.withType { tasks.withType { kotlinOptions { - jvmTarget = "11" + jvmTarget = "16" incremental = true freeCompilerArgs = listOf( "-Xopt-in=kotlin.ExperimentalStdlibApi", From 4bcc379d9fe132c3ef978937079133a00a9cb920 Mon Sep 17 00:00:00 2001 From: melike2d Date: Wed, 27 Oct 2021 16:29:49 -0700 Subject: [PATCH 40/46] :sparkles: ok this SHOULD fix natives --- Server/build.gradle.kts | 2 +- .../kotlin/obsidian/server/util/NativeUtil.kt | 23 ------------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index c28adbb..5b5d19a 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -44,7 +44,7 @@ dependencies { } implementation("com.github.natanbc:lavadsp:0.7.7") // audio filters - implementation("com.github.natanbc:lp-cross:0.2") // lp-cross natives + implementation("com.github.natanbc:native-loader:0.7.2") // lp-cross natives /* logging */ implementation("ch.qos.logback:logback-classic:1.2.6") // slf4j logging backend diff --git a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt index 74b4f40..26b8203 100644 --- a/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/NativeUtil.kt @@ -36,7 +36,6 @@ object NativeUtil { private val logger = KotlinLogging.logger { } // loaders - private val CONNECTOR_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "connector") private val UDP_QUEUE_LOADER: NativeLibLoader = NativeLibLoader.create(NativeUtil::class.java, "udpqueue") // class names @@ -57,7 +56,6 @@ object NativeUtil { * Loads native library shit */ fun load() { - loadConnector() udpQueueAvailable = loadUdpQueue() timescaleAvailable = loadTimescale() } @@ -74,27 +72,6 @@ object NativeUtil { false } - /** - * Loads the connector natives from lp-cross - */ - private fun loadConnector() { - try { - CONNECTOR_LOADER.load() - - val loadersField = ConnectorNativeLibLoader::class.java.getDeclaredField("loaders") - loadersField.isAccessible = true - - for (i in 0 until 2) { - // wtf natan - markLoaded((loadersField.get(null) as List<*>)[0]) - } - - logger.info("Connector loaded") - } catch (ex: Exception) { - logger.error("Connected failed to load", ex) - } - } - /** * Loads udp-queue natives */ From 32445e3f3719443afd6abec4a19a76610fcabd64 Mon Sep 17 00:00:00 2001 From: Aaron Hennessey Date: Wed, 8 Dec 2021 17:12:40 +0000 Subject: [PATCH 41/46] =?UTF-8?q?=F0=9F=94=80=20upgrade=20lavaplayer=20(#1?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/build.gradle.kts | 2 +- .../lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt | 2 +- Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 5b5d19a..1f2306d 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { /* audio */ implementation("moe.kyokobot.koe:core:koe-v2-SNAPSHOT") // discord send system - implementation("com.sedmelluq:lavaplayer:1.5.2") // lavaplayer + implementation("com.sedmelluq:lavaplayer:1.5.10") // lavaplayer implementation("com.sedmelluq:udp-queue-natives:2.0.0") // udp-queue natives implementation("com.sedmelluq:lavaplayer-ext-ip-rotator:0.3.0") { // ip rotation diff --git a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt index 991151f..e83d42e 100644 --- a/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt +++ b/Server/src/main/java/com/sedmelluq/discord/lavaplayer/udpqueue/natives/UdpQueueManagerLibrary.kt @@ -33,7 +33,7 @@ class UdpQueueManagerLibrary private constructor() { external fun processWithSocket(instance: Long, ipv4Handle: Long, ipv6Handle: Long) companion object { - private val nativeLoader = NativeLibraryLoader.create(UdpQueueManagerLibrary::class.java, "udpqueue") + private val nativeLoader = NativeLibraryLoader.create("udpqueue", UdpQueueManagerLibrary::class.java) @JvmStatic val instance: UdpQueueManagerLibrary diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 757c6a8..682fdac 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -113,7 +113,8 @@ private fun getTrackInfo(audioTrack: AudioTrack): Track.Info = isSeekable = audioTrack.isSeekable, isStream = audioTrack.info.isStream, position = audioTrack.position, - sourceName = audioTrack.sourceManager?.sourceName ?: "unknown" + sourceName = audioTrack.sourceManager?.sourceName ?: "unknown", + artworkUrl = audioTrack.info.artworkUrl, ) /** @@ -176,7 +177,9 @@ data class Track( @SerialName("is_seekable") val isSeekable: Boolean, @SerialName("source_name") - val sourceName: String + val sourceName: String, + @SerialName("artwork_url") + val artworkUrl: String? ) } From 1d33ea269265399e0d9587869f1694ec8bbbf5a7 Mon Sep 17 00:00:00 2001 From: melike2d Date: Sun, 13 Feb 2022 03:03:28 -0800 Subject: [PATCH 42/46] :sparkles: annual update * fix NPE when a player update is attempting to be sent * update lavaplayer --- README.md | 3 +- Server/build.gradle.kts | 25 +++--- .../kotlin/obsidian/server/Application.kt | 6 +- .../obsidian/server/config/spec/Obsidian.kt | 2 +- .../kotlin/obsidian/server/io/Handlers.kt | 4 +- .../kotlin/obsidian/server/io/rest/tracks.kt | 42 +--------- .../kotlin/obsidian/server/io/ws/Dispatch.kt | 2 +- .../kotlin/obsidian/server/player/Player.kt | 4 +- .../obsidian/server/player/PlayerUpdates.kt | 7 +- .../server/player/TrackEndMarkerHandler.kt | 2 +- .../server/util/CoroutineAudioEventAdapter.kt | 2 +- .../kotlin/obsidian/server/util/TrackUtil.kt | 4 +- .../server/util/search/AudioLoader.kt | 84 +++++++++++-------- .../server/util/search/CollectionType.kt | 40 +++++++++ .../obsidian/server/util/search/LoadResult.kt | 5 +- build.gradle.kts | 7 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 17 files changed, 131 insertions(+), 110 deletions(-) create mode 100644 Server/src/main/kotlin/obsidian/server/util/search/CollectionType.kt diff --git a/README.md b/README.md index b0425b8..aba81b3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ For obsidian to work correctly you must use **Java 11** or above. - Goto the [releases page](/releases). -- Download the **Latest Jar File** +- Download the **The Latest Jar File** - Make an [`obsidian.yml`](/obsidian.yml) file in the same directory as the jar file - Open a prompt in the same directory as the jar file. @@ -28,7 +28,6 @@ Now go make a bot with the language and client of your choice! ###### v2 - [slate](https://github.com/Axelancerr/Slate), discord.py (Python 3.7+) -- [obsidian.py](https://github.com/cloudwithax/obsidian.py), discord.py (Python 3.8+) ###### v1 diff --git a/Server/build.gradle.kts b/Server/build.gradle.kts index 1f2306d..3fed722 100644 --- a/Server/build.gradle.kts +++ b/Server/build.gradle.kts @@ -5,8 +5,8 @@ import java.io.ByteArrayOutputStream plugins { application id("com.github.johnrengelman.shadow") version "7.0.0" - kotlin("jvm") version "1.5.30" - kotlin("plugin.serialization") version "1.5.30" + kotlin("jvm") version "1.6.10" + kotlin("plugin.serialization") version "1.6.10" } apply(plugin = "kotlin") @@ -20,13 +20,13 @@ application { dependencies { /* kotlin */ - implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.30") // standard library - implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.30") // reflection - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") // core coroutine library - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0") // json serialization + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.10") // standard library + implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10") // reflection + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0") // core coroutine library + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") // json serialization /* server */ - val ktorVersion = "1.6.4" + val ktorVersion = "1.6.7" implementation("io.ktor:ktor-server-core:$ktorVersion") // ktor server core implementation("io.ktor:ktor-server-cio:$ktorVersion") // ktor cio engine implementation("io.ktor:ktor-locations:$ktorVersion") // ktor locations @@ -36,10 +36,10 @@ dependencies { /* audio */ implementation("moe.kyokobot.koe:core:koe-v2-SNAPSHOT") // discord send system - implementation("com.sedmelluq:lavaplayer:1.5.10") // lavaplayer - implementation("com.sedmelluq:udp-queue-natives:2.0.0") // udp-queue natives - - implementation("com.sedmelluq:lavaplayer-ext-ip-rotator:0.3.0") { // ip rotation + implementation("com.sedmelluq:lavaplayer:1.6.1") // lavaplayer +// implementation("com.sedmelluq:udp-queue-natives:2.0.0") // udp-queue natives + implementation(group = "com.sedmelluq", name = "udp-queue-natives", version = "2.0.0") + implementation("com.sedmelluq:lavaplayer-ext-ip-rotator:0.3.1") { // ip rotation exclude(group = "com.sedmelluq", module = "lavaplayer") } @@ -50,9 +50,6 @@ dependencies { implementation("ch.qos.logback:logback-classic:1.2.6") // slf4j logging backend implementation("io.github.microutils:kotlin-logging-jvm:2.0.11") // logging - /* misc */ - implementation("fun.dimensional:cuid:1.0.2") // CUIDs - val konfVersion = "1.1.2" implementation("com.github.uchuhimo.konf:konf-core:$konfVersion") // konf core shit implementation("com.github.uchuhimo.konf:konf-yaml:$konfVersion") // yaml source diff --git a/Server/src/main/kotlin/obsidian/server/Application.kt b/Server/src/main/kotlin/obsidian/server/Application.kt index 4ae9582..deacdf1 100644 --- a/Server/src/main/kotlin/obsidian/server/Application.kt +++ b/Server/src/main/kotlin/obsidian/server/Application.kt @@ -96,7 +96,7 @@ object Application { logger.info { ("Detected System: type = ${type.osType()}, arch = ${type.architectureType()}") } logger.info { ("Processor Information: ${NativeLibLoader.loadSystemInfo()}") } - } catch (e: Exception) { + } catch (e: Throwable) { val message = "Unable to load system info" + if (e is UnsatisfiedLinkError || e is RuntimeException && e.cause is UnsatisfiedLinkError) ", this isn't an error" else "." @@ -189,12 +189,12 @@ object Application { data class ExceptionResponse( val error: Error, @SerialName("stack_trace") val stackTrace: String, - val success: Boolean = false + val success: Boolean = false, ) { @Serializable data class Error( val message: String?, val cause: Error? = null, - @SerialName("class_name") val className: String + @SerialName("class_name") val className: String, ) } diff --git a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt index 98093f5..a9bdde6 100644 --- a/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt +++ b/Server/src/main/kotlin/obsidian/server/config/spec/Obsidian.kt @@ -85,7 +85,7 @@ object Obsidian : ConfigSpec() { /** * The voice server version to use, defaults to v5 */ - val gatewayVersion by optional(5, "gateway-version") + val gatewayVersion by optional(4, "gateway-version") object UdpQueue : ConfigSpec("udp-queue") { /** diff --git a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt index 6aa8bf3..42b18cf 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt @@ -16,7 +16,7 @@ package obsidian.server.io -import com.sedmelluq.discord.lavaplayer.track.TrackMarker +import com.sedmelluq.discord.lavaplayer.track.marker.TrackMarker import moe.kyokobot.koe.VoiceServerInfo import mu.KotlinLogging import obsidian.server.player.TrackEndMarkerHandler @@ -43,7 +43,7 @@ object Handlers { client.koe.destroyConnection(guildId) } - fun playTrack( + suspend fun playTrack( client: MagmaClient, guildId: Long, track: String, diff --git a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt index 682fdac..0ee08fc 100644 --- a/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt +++ b/Server/src/main/kotlin/obsidian/server/io/rest/tracks.kt @@ -16,9 +16,9 @@ package obsidian.server.io.rest -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack -import com.sedmelluq.discord.lavaplayer.track.AudioTrackCollectionType +import com.sedmelluq.discord.lavaplayer.track.collection.SearchResult +import com.sedmelluq.lava.common.tools.exception.FriendlyException import io.ktor.application.* import io.ktor.auth.* import io.ktor.locations.* @@ -28,14 +28,8 @@ import io.ktor.routing.* import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.* import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonTransformingSerializer import obsidian.server.Application import obsidian.server.Application.config @@ -44,7 +38,6 @@ import obsidian.server.util.TrackUtil import obsidian.server.util.kxs.AudioTrackSerializer import obsidian.server.util.search.AudioLoader import obsidian.server.util.search.LoadType -import kotlin.reflect.jvm.jvmName fun Routing.tracks() = authenticate { get { data -> @@ -57,8 +50,7 @@ fun Routing.tracks() = authenticate { LoadTracks.Response.CollectionInfo( name = result.collectionName!!, selectedTrack = result.selectedTrack, - url = if (result.collectionType!! is AudioTrackCollectionType.SearchResult) null else data.identifier, - type = result.collectionType + url = data.identifier, ) } else { null @@ -151,9 +143,7 @@ data class LoadTracks(val identifier: String) { val name: String, val url: String?, @SerialName("selected_track") - val selectedTrack: Int?, - @Serializable(with = AudioTrackCollectionTypeSerializer::class) - val type: AudioTrackCollectionType + val selectedTrack: Int? ) } } @@ -183,30 +173,6 @@ data class Track( ) } -object AudioTrackCollectionTypeSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("lavaplayer.AudioTrackCollectionType") { - element("name", String.serializer().descriptor) - element("d", JsonObject.serializer().descriptor) - } - - @OptIn(InternalSerializationApi::class) - override fun serialize(encoder: Encoder, value: AudioTrackCollectionType) { - with(encoder.beginStructure(descriptor)) { - encodeStringElement(descriptor, 0, value::class.simpleName ?: value::class.jvmName) - encodeSerializableElement( - descriptor, - 1, - value::class.serializer() as SerializationStrategy, - value - ) - endStructure(descriptor) - } - } - - override fun deserialize(decoder: Decoder): AudioTrackCollectionType = - TODO("Not Supported") -} - object AudioTrackListSerializer : JsonTransformingSerializer>(ListSerializer(AudioTrackSerializer)) { override fun transformDeserialize(element: JsonElement): JsonElement = if (element !is JsonArray) { diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt index 45c1a9d..dbc4a55 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/Dispatch.kt @@ -16,9 +16,9 @@ package obsidian.server.io.ws -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason +import com.sedmelluq.lava.common.tools.exception.FriendlyException import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy diff --git a/Server/src/main/kotlin/obsidian/server/player/Player.kt b/Server/src/main/kotlin/obsidian/server/player/Player.kt index 909cc6c..d3679c3 100644 --- a/Server/src/main/kotlin/obsidian/server/player/Player.kt +++ b/Server/src/main/kotlin/obsidian/server/player/Player.kt @@ -20,11 +20,11 @@ import com.sedmelluq.discord.lavaplayer.format.StandardAudioDataFormats import com.sedmelluq.discord.lavaplayer.manager.AudioPlayer import com.sedmelluq.discord.lavaplayer.manager.event.AudioEventAdapter import com.sedmelluq.discord.lavaplayer.manager.event.AudioEventListener -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.tools.extensions.addListener import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame +import com.sedmelluq.lava.common.tools.exception.FriendlyException import io.netty.buffer.ByteBuf import moe.kyokobot.koe.MediaConnection import moe.kyokobot.koe.media.OpusAudioFrameProvider @@ -79,7 +79,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { /** * Plays the provided [track] and dispatches a Player Update */ - fun play(track: AudioTrack) { + suspend fun play(track: AudioTrack) { audioPlayer.playTrack(track) updates.sendUpdate() } diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index 00bfbe2..5154e7e 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -61,7 +61,12 @@ class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { } } - fun sendUpdate() { + suspend fun sendUpdate() { + if (player.audioPlayer.playingTrack == null) { + stop() + return + } + player.client.websocket?.let { val update = PlayerUpdate( guildId = player.guildId, diff --git a/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt b/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt index f45c161..e86f436 100644 --- a/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt +++ b/Server/src/main/kotlin/obsidian/server/player/TrackEndMarkerHandler.kt @@ -16,7 +16,7 @@ package obsidian.server.player -import com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler +import com.sedmelluq.discord.lavaplayer.track.marker.TrackMarkerHandler class TrackEndMarkerHandler(private val player: Player) : TrackMarkerHandler { override fun handle(state: TrackMarkerHandler.MarkerState) { diff --git a/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt index 80582d3..d0c7a1f 100644 --- a/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt +++ b/Server/src/main/kotlin/obsidian/server/util/CoroutineAudioEventAdapter.kt @@ -18,9 +18,9 @@ package obsidian.server.util import com.sedmelluq.discord.lavaplayer.manager.AudioPlayer import com.sedmelluq.discord.lavaplayer.manager.event.* -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason +import com.sedmelluq.lava.common.tools.exception.FriendlyException import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext diff --git a/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt b/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt index aa86f2c..f133495 100644 --- a/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt +++ b/Server/src/main/kotlin/obsidian/server/util/TrackUtil.kt @@ -16,9 +16,9 @@ package obsidian.server.util -import com.sedmelluq.discord.lavaplayer.tools.io.MessageInput -import com.sedmelluq.discord.lavaplayer.tools.io.MessageOutput import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import com.sedmelluq.lava.common.tools.io.MessageInput +import com.sedmelluq.lava.common.tools.io.MessageOutput import obsidian.server.Application.players import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream diff --git a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt index 89606f5..17ffb22 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/AudioLoader.kt @@ -17,51 +17,61 @@ package obsidian.server.util.search import com.sedmelluq.discord.lavaplayer.manager.AudioPlayerManager -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException -import com.sedmelluq.discord.lavaplayer.track.AudioTrack -import com.sedmelluq.discord.lavaplayer.track.AudioTrackCollection -import com.sedmelluq.discord.lavaplayer.track.loader.ItemLoadResultAdapter -import kotlinx.coroutines.CompletableDeferred +import com.sedmelluq.discord.lavaplayer.track.collection.Playlist +import com.sedmelluq.discord.lavaplayer.track.collection.SearchResult +import com.sedmelluq.discord.lavaplayer.track.loader.ItemLoadResult +import com.sedmelluq.lava.common.tools.exception.FriendlyException import mu.KotlinLogging +import kotlin.reflect.jvm.jvmName -class AudioLoader(private val deferred: CompletableDeferred) : ItemLoadResultAdapter() { - companion object { - private val logger = KotlinLogging.logger { } +object AudioLoader { + private val logger = KotlinLogging.logger { } - suspend fun load(identifier: String, playerManager: AudioPlayerManager) = - CompletableDeferred().also { - val itemLoader = playerManager.items.createItemLoader(identifier) - itemLoader.resultHandler = AudioLoader(it) - itemLoader.load() + suspend fun load(identifier: String, playerManager: AudioPlayerManager): LoadResult { + val item = playerManager.items + .createItemLoader(identifier) + .load() + + return when (item) { + is ItemLoadResult.TrackLoaded -> { + logger.info { "Loaded track ${item.track.info.title}" } + LoadResult(LoadType.TRACK, listOf(item.track), null, null) } - .await() - } - override fun onTrackLoad(track: AudioTrack) { - logger.info { "Loaded track ${track.info.title}" } - deferred.complete(LoadResult(LoadType.TRACK, listOf(track), null, null)) - } + is ItemLoadResult.CollectionLoaded -> { + logger.info { "Loaded playlist: ${item.collection.name}" } + LoadResult( + LoadType.TRACK_COLLECTION, + item.collection.tracks, + item.collection.name, + when (item.collection) { + is Playlist -> if ((item.collection as Playlist).isAlbum) CollectionType.Album else CollectionType.Playlist + is SearchResult -> CollectionType.SearchResult + else -> CollectionType.Unknown(item.collection::class.let { it.simpleName ?: it.jvmName }) + }, + item.collection.selectedTrack?.let { item.collection.tracks.indexOf(it) } + ) + } - override fun onCollectionLoad(collection: AudioTrackCollection) { - logger.info { "Loaded playlist: ${collection.name}" } - val result = LoadResult( - LoadType.TRACK_COLLECTION, - collection.tracks, - collection.name, - collection.type, - collection.selectedTrack?.let { collection.tracks.indexOf(it) } - ) + is ItemLoadResult.LoadFailed -> { + logger.error(item.exception) { "Failed to load." } + LoadResult(item.exception) + } - deferred.complete(result) - } + is ItemLoadResult.NoMatches -> { + logger.info { "No matches found." } + LoadResult() + } - override fun noMatches() { - logger.info { "No matches found." } - deferred.complete(LoadResult()) - } + else -> { + val excp = FriendlyException( + friendlyMessage = "Unknown load result type: ${item::class.qualifiedName}", + severity = FriendlyException.Severity.SUSPICIOUS, + cause = null + ) - override fun onLoadFailed(exception: FriendlyException) { - logger.error(exception) { "Failed to load." } - deferred.complete(LoadResult(exception)) + LoadResult(excp) + } + } } } diff --git a/Server/src/main/kotlin/obsidian/server/util/search/CollectionType.kt b/Server/src/main/kotlin/obsidian/server/util/search/CollectionType.kt new file mode 100644 index 0000000..45028e4 --- /dev/null +++ b/Server/src/main/kotlin/obsidian/server/util/search/CollectionType.kt @@ -0,0 +1,40 @@ +package obsidian.server.util.search + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import javax.naming.OperationNotSupportedException + +@Serializable(with = CollectionType.Serializer::class) +sealed class CollectionType { + object Album : CollectionType() + + object Playlist : CollectionType() + + object SearchResult : CollectionType() + + data class Unknown(val name: String) : CollectionType() + + companion object Serializer: KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("obsidian.server.util.search.CollectionType", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): CollectionType { + throw OperationNotSupportedException() // we're only implementing KSerializer to use @Serializer + } + + override fun serialize(encoder: Encoder, value: CollectionType) { + val name = when (value) { + is Unknown -> value.name + else -> requireNotNull(value::class.simpleName) + } + + encoder.encodeString(name) + } + + } +} diff --git a/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt index 4924dc7..5bb7b9d 100644 --- a/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt +++ b/Server/src/main/kotlin/obsidian/server/util/search/LoadResult.kt @@ -16,15 +16,14 @@ package obsidian.server.util.search -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioTrack -import com.sedmelluq.discord.lavaplayer.track.AudioTrackCollectionType +import com.sedmelluq.lava.common.tools.exception.FriendlyException class LoadResult( val loadResultType: LoadType = LoadType.NONE, val tracks: List = emptyList(), val collectionName: String? = null, - val collectionType: AudioTrackCollectionType? = null, + val collectionType: CollectionType? = null, val selectedTrack: Int? = null, ) { var exception: FriendlyException? = null diff --git a/build.gradle.kts b/build.gradle.kts index 0f58512..33693ba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,7 +16,7 @@ subprojects { } dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") } } @@ -26,6 +26,11 @@ subprojects { name = "Jfrog Dimensional" } + maven { + url = uri("https://maven.dimensional.fun/releases") + name = "Dimensional Fun" + } + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap/") name = "Ktor EAP" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 29e4134..ffed3a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From fac699db5930034067adf9d36c9726277e77b627 Mon Sep 17 00:00:00 2001 From: melike2d Date: Sat, 26 Feb 2022 14:40:13 -0800 Subject: [PATCH 43/46] :sparkles: allow multiple websocket sessions for a client --- .../kotlin/obsidian/server/io/Handlers.kt | 23 +++++----- .../main/kotlin/obsidian/server/io/Magma.kt | 30 ++++++------- .../kotlin/obsidian/server/io/MagmaClient.kt | 37 ++++++++++------ ...SocketHandler.kt => MagmaClientSession.kt} | 44 +++++++++---------- .../kotlin/obsidian/server/io/ws/StatsTask.kt | 8 ++-- .../kotlin/obsidian/server/player/Player.kt | 19 ++++---- .../obsidian/server/player/PlayerUpdates.kt | 4 +- 7 files changed, 86 insertions(+), 79 deletions(-) rename Server/src/main/kotlin/obsidian/server/io/ws/{WebSocketHandler.kt => MagmaClientSession.kt} (88%) diff --git a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt index 42b18cf..85ae6e2 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Handlers.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Handlers.kt @@ -19,6 +19,7 @@ package obsidian.server.io import com.sedmelluq.discord.lavaplayer.track.marker.TrackMarker import moe.kyokobot.koe.VoiceServerInfo import mu.KotlinLogging +import obsidian.server.io.ws.MagmaClientSession import obsidian.server.player.TrackEndMarkerHandler import obsidian.server.player.filter.Filters import obsidian.server.util.TrackUtil @@ -26,14 +27,14 @@ import obsidian.server.util.TrackUtil object Handlers { private val log = KotlinLogging.logger { } - fun submitVoiceServer(client: MagmaClient, guildId: Long, vsi: VoiceServerInfo) { + fun submitVoiceServer(client: MagmaClient, guildId: Long, vsi: VoiceServerInfo, session: MagmaClientSession? = null) { val connection = client.mediaConnectionFor(guildId) connection.connect(vsi) - client.playerFor(guildId).provideTo(connection) + client.playerFor(guildId, session).provideTo(connection) } - fun seek(client: MagmaClient, guildId: Long, position: Long) { - val player = client.playerFor(guildId) + fun seek(client: MagmaClient, guildId: Long, position: Long, session: MagmaClientSession? = null) { + val player = client.playerFor(guildId, session) player.seekTo(position) } @@ -49,9 +50,10 @@ object Handlers { track: String, startTime: Long?, endTime: Long?, - noReplace: Boolean = false + noReplace: Boolean = false, + session: MagmaClientSession? = null ) { - val player = client.playerFor(guildId) + val player = client.playerFor(guildId, session) if (player.audioPlayer.playingTrack != null && noReplace) { log.info { "${client.displayName} - skipping PLAY_TRACK operation" } return @@ -73,8 +75,8 @@ object Handlers { player.play(audioTrack) } - fun stopTrack(client: MagmaClient, guildId: Long) { - val player = client.playerFor(guildId) + fun stopTrack(client: MagmaClient, guildId: Long, session: MagmaClientSession? = null) { + val player = client.playerFor(guildId, session) player.audioPlayer.stopTrack() } @@ -83,13 +85,14 @@ object Handlers { guildId: Long, filters: Filters? = null, pause: Boolean? = null, - sendPlayerUpdates: Boolean? = null + sendPlayerUpdates: Boolean? = null, + session: MagmaClientSession? = null ) { if (filters == null && pause == null && sendPlayerUpdates == null) { return } - val player = client.playerFor(guildId) + val player = client.playerFor(guildId, session) pause?.let { player.audioPlayer.isPaused = it } filters?.let { player.filters = it } sendPlayerUpdates?.let { player.updates.enabled = it } diff --git a/Server/src/main/kotlin/obsidian/server/io/Magma.kt b/Server/src/main/kotlin/obsidian/server/io/Magma.kt index 81365a6..9641a65 100644 --- a/Server/src/main/kotlin/obsidian/server/io/Magma.kt +++ b/Server/src/main/kotlin/obsidian/server/io/Magma.kt @@ -37,7 +37,7 @@ import obsidian.server.io.rest.respondAndFinish import obsidian.server.io.rest.tracks import obsidian.server.io.ws.CloseReasons import obsidian.server.io.ws.StatsTask -import obsidian.server.io.ws.WebSocketHandler +import obsidian.server.io.ws.MagmaClientSession import obsidian.server.util.threadFactory import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors @@ -87,7 +87,7 @@ object Magma { } } - /* check if a client name is required, if so check if there was a provided client name */ + /* check if a client name is required, if so check for a provided client name */ if (clientName == null && config[Obsidian.requireClientName]) { return@intercept respondAndFinish( HttpStatusCode.BadRequest, @@ -133,20 +133,17 @@ object Magma { return@webSocket close(CloseReasons.MISSING_USER_ID) } - val client = clients[userId] - ?: createClient(userId, clientName) + val client = clients[userId] ?: createClient(userId, clientName) - val wsh = client.websocket - if (wsh != null) { - /* check for a resume key, if one was given check if the client has the same resume key/ */ - val resumeKey: String? = request.headers["Resume-Key"] - if (resumeKey != null && wsh.resumeKey == resumeKey) { + /* check for a resume key, if one was given check if the client has the same resume key/ */ + val resumeKey: String? = request.headers["Resume-Key"] + if (resumeKey != null) { + val session = client.sessions.find { it.resumeKey == resumeKey } + if (session != null) { /* resume the client session */ - wsh.resume(this) + session.resume(this) return@webSocket } - - return@webSocket close(CloseReasons.DUPLICATE_SESSION) } handleWebsocket(client, this) @@ -202,13 +199,12 @@ object Magma { * Handles a [WebSocketServerSession] for the supplied [client] */ private suspend fun handleWebsocket(client: MagmaClient, wss: WebSocketServerSession) { - val wsh = WebSocketHandler(client, wss).also { - client.websocket = it - } + val session = MagmaClientSession(client, wss) + client.sessions.add(session) /* listen for incoming messages. */ try { - wsh.listen() + session.listen() } catch (ex: Exception) { log.error(ex) { "${client.displayName} - An error occurred while listening for frames." } if (wss.isActive) { @@ -216,6 +212,6 @@ object Magma { } } - wsh.handleClose() + session.handleClose() } } diff --git a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt index 400a5c6..aa6e50a 100644 --- a/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt +++ b/Server/src/main/kotlin/obsidian/server/io/MagmaClient.kt @@ -16,12 +16,12 @@ package obsidian.server.io -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import moe.kyokobot.koe.KoeClient import moe.kyokobot.koe.KoeEventAdapter import moe.kyokobot.koe.MediaConnection import obsidian.server.io.ws.WebSocketClosedEvent -import obsidian.server.io.ws.WebSocketHandler +import obsidian.server.io.ws.MagmaClientSession import obsidian.server.io.ws.WebSocketOpenEvent import obsidian.server.player.Player import obsidian.server.util.KoeUtil @@ -29,15 +29,17 @@ import java.net.InetSocketAddress import java.util.concurrent.ConcurrentHashMap class MagmaClient(private val userId: Long) { + val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + /** * The name of this client. */ var name: String? = null /** - * The websocket handler for this client, or null if one hasn't been initialized. + * The websocket sessions for this client, may be empty. */ - var websocket: WebSocketHandler? = null + var sessions: MutableList = mutableListOf() /** * The display name for this client. @@ -64,9 +66,9 @@ class MagmaClient(private val userId: Long) { * * @param guildId ID of the guild. */ - fun playerFor(guildId: Long): Player { + fun playerFor(guildId: Long, session: MagmaClientSession? = null): Player { return players.computeIfAbsent(guildId) { - Player(guildId, this) + Player(guildId, this, session) } } @@ -92,9 +94,11 @@ class MagmaClient(private val userId: Long) { * Whether we should be cautious about shutting down. */ suspend fun shutdown(safe: Boolean = true) { - websocket?.shutdown() - websocket = null + if (sessions.isNotEmpty()) { + return + } + sessions.onEach { it.shutdown() }.clear() val activePlayers = players.count { (_, player) -> player.audioPlayer.playingTrack != null } @@ -103,7 +107,7 @@ class MagmaClient(private val userId: Long) { return } - /* no players are active so it's safe to remove the client. */ + /* no players are active, so it's safe to remove the client. */ for ((id, player) in players) { player.destroy() @@ -111,23 +115,30 @@ class MagmaClient(private val userId: Long) { } koe.close() + scope.cancel() } inner class EventAdapterImpl(private val connection: MediaConnection) : KoeEventAdapter() { override fun gatewayReady(target: InetSocketAddress, ssrc: Int) { - websocket?.launch { + val session = players[connection.guildId]?.session + ?: return + + session.scope.launch { val event = WebSocketOpenEvent( guildId = connection.guildId, ssrc = ssrc, target = target.toString(), ) - websocket?.send(event) + session.send(event) } } override fun gatewayClosed(code: Int, reason: String?, byRemote: Boolean) { - websocket?.launch { + val session = players[connection.guildId]?.session + ?: return + + session.scope.launch { val event = WebSocketClosedEvent( guildId = connection.guildId, code = code, @@ -135,7 +146,7 @@ class MagmaClient(private val userId: Long) { byRemote = byRemote ) - websocket?.send(event) + session.send(event) } } } diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt b/Server/src/main/kotlin/obsidian/server/io/ws/MagmaClientSession.kt similarity index 88% rename from Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt rename to Server/src/main/kotlin/obsidian/server/io/ws/MagmaClientSession.kt index 3e51903..b885bec 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/WebSocketHandler.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/MagmaClientSession.kt @@ -33,10 +33,9 @@ import obsidian.server.util.threadFactory import java.lang.Runnable import java.util.concurrent.* import java.util.concurrent.CancellationException -import kotlin.coroutines.CoroutineContext import kotlin.time.ExperimentalTime -class WebSocketHandler(val client: MagmaClient, private var session: WebSocketServerSession) : CoroutineScope { +class MagmaClientSession(val client: MagmaClient, private var wss: WebSocketServerSession) { companion object { fun ReceiveChannel.asFlow() = flow { try { @@ -49,6 +48,8 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe private val log = KotlinLogging.logger { } } + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + /** * Resume key */ @@ -90,38 +91,35 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe */ private val events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) - override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + SupervisorJob() - init { /* websocket and rest operations */ on { val vsi = VoiceServerInfo(sessionId, endpoint, token) - Handlers.submitVoiceServer(client, guildId, vsi) + Handlers.submitVoiceServer(client, guildId, vsi, this@MagmaClientSession) } on { - Handlers.configure(client, guildId, filters = filters) + Handlers.configure(client, guildId, filters = filters, session = this@MagmaClientSession) } on { - Handlers.configure(client, guildId, pause = state) + Handlers.configure(client, guildId, pause = state, session = this@MagmaClientSession) } on { - Handlers.configure(client, guildId, filters, pause, sendPlayerUpdates) + Handlers.configure(client, guildId, filters, pause, sendPlayerUpdates, this@MagmaClientSession) } on { - Handlers.seek(client, guildId, position) + Handlers.seek(client, guildId, position, this@MagmaClientSession) } on { - Handlers.playTrack(client, guildId, track, startTime, endTime, noReplace) + Handlers.playTrack(client, guildId, track, startTime, endTime, noReplace, this@MagmaClientSession) } on { - Handlers.stopTrack(client, guildId) + Handlers.stopTrack(client, guildId, this@MagmaClientSession) } on { @@ -140,7 +138,6 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe bufferTimeout = timeout log.debug { "${client.displayName} - Dispatch buffer timeout: $timeout" } } - } /** @@ -155,14 +152,13 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe stats.scheduleAtFixedRate(statsRunnable, 0, 1, TimeUnit.MINUTES) /* listen for incoming frames. */ - session.incoming + wss.incoming .asFlow() .buffer(Channel.UNLIMITED) .collect { when (it) { is Frame.Binary, is Frame.Text -> handleIncomingFrame(it) - else -> { // no-op - } + else -> { /* no-op */ } } } @@ -192,6 +188,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe return } + client.sessions.remove(this) client.shutdown() } @@ -201,7 +198,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe suspend fun resume(session: WebSocketServerSession) { log.info { "${client.displayName} - session has been resumed" } - this.session = session + this.wss = session this.active = true this.resumeTimeoutFuture?.cancel(false) @@ -221,7 +218,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe */ fun send(dispatch: Dispatch) { val json = json.encodeToString(Dispatch.Companion, dispatch) - if (!session.isActive) { + if (!wss.isActive) { dispatchBuffer?.offer(json) return } @@ -237,15 +234,14 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe /* cancel this coroutine context */ try { - currentCoroutineContext().cancelChildren() - currentCoroutineContext().cancel() + scope.cancel() } catch (ex: Exception) { log.warn { "${client.displayName} - Error occurred while cancelling this coroutine scope" } } /* close the websocket session, if not already */ if (active) { - session.close(CloseReason(1000, "shutting down")) + wss.close(CloseReason(1000, "shutting down")) } } @@ -257,7 +253,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe private fun send(json: String, payloadName: String? = null) { try { log.trace { "${client.displayName} ${payloadName?.let { "$it " } ?: ""}<<< $json" } - session.outgoing.trySend(Frame.Text(json)) + wss.outgoing.trySend(Frame.Text(json)) } catch (ex: Exception) { log.error(ex) { "${client.displayName} - An exception occurred while sending a json payload" } } @@ -269,7 +265,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe private inline fun on(crossinline block: suspend T.() -> Unit) { events.filterIsInstance() .onEach { - launch { + scope.launch { try { block.invoke(it) } catch (ex: Exception) { @@ -277,7 +273,7 @@ class WebSocketHandler(val client: MagmaClient, private var session: WebSocketSe } } } - .launchIn(this) + .launchIn(scope) } /** diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt index 6c04e4d..81bca63 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/StatsTask.kt @@ -34,11 +34,11 @@ object StatsTask { } } - fun getRunnable(wsh: WebSocketHandler): Runnable { + fun getRunnable(session: MagmaClientSession): Runnable { return Runnable { - wsh.launch { - val stats = build(wsh.client) - wsh.send(stats) + session.scope.launch { + val stats = build(session.client) + session.send(stats) } } } diff --git a/Server/src/main/kotlin/obsidian/server/player/Player.kt b/Server/src/main/kotlin/obsidian/server/player/Player.kt index d3679c3..54ad03d 100644 --- a/Server/src/main/kotlin/obsidian/server/player/Player.kt +++ b/Server/src/main/kotlin/obsidian/server/player/Player.kt @@ -30,14 +30,15 @@ import moe.kyokobot.koe.MediaConnection import moe.kyokobot.koe.media.OpusAudioFrameProvider import obsidian.server.Application.players import obsidian.server.io.MagmaClient -import obsidian.server.io.ws.TrackEndEvent -import obsidian.server.io.ws.TrackExceptionEvent -import obsidian.server.io.ws.TrackStartEvent -import obsidian.server.io.ws.TrackStuckEvent +import obsidian.server.io.ws.* import obsidian.server.player.filter.Filters import java.nio.ByteBuffer -class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { +class Player( + val guildId: Long, + val client: MagmaClient, + val session: MagmaClientSession? = null +) : AudioEventAdapter() { /** * Handles all updates for this player. @@ -114,7 +115,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { * */ override fun onTrackStuck(player: AudioPlayer, track: AudioTrack, thresholdMs: Long) { - client.websocket?.let { + session?.let { val event = TrackStuckEvent( guildId = guildId, thresholdMs = thresholdMs, @@ -129,7 +130,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { * */ override fun onTrackException(player: AudioPlayer, track: AudioTrack, exception: FriendlyException) { - client.websocket?.let { + session?.let { val event = TrackExceptionEvent( guildId = guildId, track = track, @@ -144,7 +145,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { * */ override fun onTrackStart(player: AudioPlayer, track: AudioTrack) { - client.websocket?.let { + session?.let { val event = TrackStartEvent( guildId = guildId, track = track @@ -158,7 +159,7 @@ class Player(val guildId: Long, val client: MagmaClient) : AudioEventAdapter() { * Sends a track end player event to the websocket connection, if any. */ override fun onTrackEnd(player: AudioPlayer, track: AudioTrack, endReason: AudioTrackEndReason) { - client.websocket?.let { + session?.let { val event = TrackEndEvent( track = track, endReason = endReason, diff --git a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt index 5154e7e..9e9bda7 100644 --- a/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt +++ b/Server/src/main/kotlin/obsidian/server/player/PlayerUpdates.kt @@ -36,7 +36,7 @@ class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { set(value) { field = value - player.client.websocket?.launch { + player.session?.scope?.launch { if (value) start() else stop() } } @@ -67,7 +67,7 @@ class PlayerUpdates(val player: Player) : CoroutineAudioEventAdapter() { return } - player.client.websocket?.let { + player.session?.let { val update = PlayerUpdate( guildId = player.guildId, currentTrack = currentTrackFor(player), From a116fbc102e996c70e01d3beef479e59acfcc00a Mon Sep 17 00:00:00 2001 From: melike2d Date: Sat, 26 Feb 2022 14:40:28 -0800 Subject: [PATCH 44/46] :sparkles: log op name along with json --- .../obsidian/server/io/ws/MagmaClientSession.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Server/src/main/kotlin/obsidian/server/io/ws/MagmaClientSession.kt b/Server/src/main/kotlin/obsidian/server/io/ws/MagmaClientSession.kt index b885bec..bc09dc4 100644 --- a/Server/src/main/kotlin/obsidian/server/io/ws/MagmaClientSession.kt +++ b/Server/src/main/kotlin/obsidian/server/io/ws/MagmaClientSession.kt @@ -285,8 +285,15 @@ class MagmaClientSession(val client: MagmaClient, private var wss: WebSocketServ val data = frame.data.toString(Charset.defaultCharset()) try { - log.info { "${client.displayName} >>> $data" } - json.decodeFromString(Operation, data)?.let { events.tryEmit(it) } + val event = json.decodeFromString(Operation, data) + log.info { "${client.displayName} >>> ${event + ?.let { it::class.simpleName } + ?.let { "$it " } + ?: ""}$data" } + + if (event != null) { + scope.launch { events.emit(event) } + } } catch (ex: Exception) { log.error(ex) { "${client.displayName} - An exception occurred while handling an incoming frame" } } From b67174e5ab20a954fdf614f2c701a6108813f206 Mon Sep 17 00:00:00 2001 From: melike2d Date: Sat, 26 Feb 2022 14:41:40 -0800 Subject: [PATCH 45/46] :memo: update docs --- docs/ws/protocol.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ws/protocol.md b/docs/ws/protocol.md index 2737c8a..c3bf1ec 100644 --- a/docs/ws/protocol.md +++ b/docs/ws/protocol.md @@ -16,13 +16,15 @@ Resume-Key: The resume key (like lavalink), however this is only needed if the s ## Close Codes | Close Code | Reason | -| ---------- | ---------------------------------------------------- | +|------------|------------------------------------------------------| | 4001 | Invalid or missing authorization | | 4002 | Missing `Client-Name` header or query-parameter | | 4003 | Missing `User-Id` header or query-parameter | -| 4005 | A session for the supplied user already exists. | +| ~~4005~~ | ~~A session for the supplied user already exists.~~ | | 4006 | An error occurred while handling a received payload. | +* 4006 has been deprecated with commit `a116fbc1` + ## Payload Structure - **op**: numeric op code From cbdd30452bd214da20189a93f8ae2e50fd2c048c Mon Sep 17 00:00:00 2001 From: 2D <44017640+melike2d@users.noreply.github.com> Date: Sat, 26 Feb 2022 14:47:31 -0800 Subject: [PATCH 46/46] :memo: update docs --- docs/ws/protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ws/protocol.md b/docs/ws/protocol.md index c3bf1ec..5731e0f 100644 --- a/docs/ws/protocol.md +++ b/docs/ws/protocol.md @@ -23,7 +23,7 @@ Resume-Key: The resume key (like lavalink), however this is only needed if the s | ~~4005~~ | ~~A session for the supplied user already exists.~~ | | 4006 | An error occurred while handling a received payload. | -* 4006 has been deprecated with commit `a116fbc1` +* 4005 has been deprecated with commit [`fac699d`](https://github.com/mixtape-bot/obsidian/commit/fac699db5930034067adf9d36c9726277e77b627) ## Payload Structure