diff --git a/README.md b/README.md index 082aee3..154c2cd 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Adding support for the following sources: - TikTok (in beta, works on _most_ videos and **will** break all the time) - PornHub (search by prefixing with `phsearch:`) - soundgasm +- streamDeckAudio files + - These files are only accepted over HTTP currently ## Lavalink version compatibility diff --git a/plugin/src/main/java/com/dunctebot/lavalinkplugin/DuncteBotConfig.java b/plugin/src/main/java/com/dunctebot/lavalinkplugin/DuncteBotConfig.java index bb342be..d79ce6b 100644 --- a/plugin/src/main/java/com/dunctebot/lavalinkplugin/DuncteBotConfig.java +++ b/plugin/src/main/java/com/dunctebot/lavalinkplugin/DuncteBotConfig.java @@ -28,6 +28,7 @@ public static class Sources { private boolean tiktok = true; private boolean mixcloud = true; private boolean soundgasm = true; + private boolean elgato = false; public boolean isGetyarn() { return getyarn; @@ -100,5 +101,13 @@ public boolean isSoundgasm() { public void setSoundgasm(boolean soundgasm) { this.soundgasm = soundgasm; } + + public boolean isElgato() { + return elgato; + } + + public void setElgato(boolean elgato) { + this.elgato = elgato; + } } -} \ No newline at end of file +} diff --git a/plugin/src/main/java/com/dunctebot/lavalinkplugin/DuncteBotInjector.java b/plugin/src/main/java/com/dunctebot/lavalinkplugin/DuncteBotInjector.java index 58bac85..6810640 100644 --- a/plugin/src/main/java/com/dunctebot/lavalinkplugin/DuncteBotInjector.java +++ b/plugin/src/main/java/com/dunctebot/lavalinkplugin/DuncteBotInjector.java @@ -1,6 +1,7 @@ package com.dunctebot.lavalinkplugin; import com.dunctebot.sourcemanagers.clypit.ClypitAudioSourceManager; +import com.dunctebot.sourcemanagers.elgato.streamdeck.StreamDeckAudioSourceManager; import com.dunctebot.sourcemanagers.getyarn.GetyarnAudioSourceManager; import com.dunctebot.sourcemanagers.mixcloud.MixcloudAudioSourceManager; import com.dunctebot.sourcemanagers.ocremix.OCRemixAudioSourceManager; @@ -82,6 +83,12 @@ public AudioPlayerManager configure(@NotNull AudioPlayerManager manager) { manager.registerSourceManager(new SoundGasmAudioSourceManager()); } + if (this.sourcesConfig.isElgato()) { + logger.warn("Elgato (.streamDeckAudio) audio source manager is not supported atm"); +// logger.info("Registering Elgato (.streamDeckAudio) audio source manager"); +// manager.registerSourceManager(new StreamDeckAudioSourceManager()); + } + return manager; } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 421f322..0e5a94b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,7 +24,7 @@ fun VersionCatalogBuilder.common() { fun VersionCatalogBuilder.sourceManager() { version("slf4j-version", "2.0.9") - library("lavaplayer", "dev.arbjerg", "lavaplayer").version("2.0.3") + library("lavaplayer", "dev.arbjerg", "lavaplayer").version("2.1.2") library("logger", "org.slf4j", "slf4j-api").versionRef("slf4j-version") library("logger-impl", "org.slf4j", "slf4j-simple").versionRef("slf4j-version") library("commonsIo", "commons-io", "commons-io").version("2.7") diff --git a/source-managers/src/main/java/com/dunctebot/sourcemanagers/Mp3Track.java b/source-managers/src/main/java/com/dunctebot/sourcemanagers/Mp3Track.java index 65c4636..ad23d5e 100644 --- a/source-managers/src/main/java/com/dunctebot/sourcemanagers/Mp3Track.java +++ b/source-managers/src/main/java/com/dunctebot/sourcemanagers/Mp3Track.java @@ -54,11 +54,20 @@ protected void loadStream(LocalAudioTrackExecutor localExecutor, HttpInterface h final String trackUrl = getPlaybackUrl(); log.debug("Starting {} track from URL: {}", manager.getSourceName(), trackUrl); // Setting contentLength (last param) to null makes it default to Long.MAX_VALUE - try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(trackUrl), this.getTrackDuration())) { + try ( + final var stream = this.wrapStream( + new PersistentHttpStream(httpInterface, new URI(trackUrl), this.getTrackDuration()) + ) + ) { processDelegate(createAudioTrack(this.trackInfo, stream), localExecutor); } } + // Helper function in case we need to wrap the http stream into something else for decoding + protected SeekableInputStream wrapStream(SeekableInputStream stream) { + return stream; + } + protected InternalAudioTrack createAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream stream) { return new Mp3AudioTrack(trackInfo, stream); } diff --git a/source-managers/src/main/java/com/dunctebot/sourcemanagers/elgato/streamdeck/ElgatoInputStream.java b/source-managers/src/main/java/com/dunctebot/sourcemanagers/elgato/streamdeck/ElgatoInputStream.java new file mode 100644 index 0000000..48d4cfd --- /dev/null +++ b/source-managers/src/main/java/com/dunctebot/sourcemanagers/elgato/streamdeck/ElgatoInputStream.java @@ -0,0 +1,58 @@ +package com.dunctebot.sourcemanagers.elgato.streamdeck; + +import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; +import com.sedmelluq.discord.lavaplayer.track.info.AudioTrackInfoProvider; + +import java.io.IOException; +import java.util.List; + +// TODO: can I inject a custom probe into LP? +// See: MediaContainerRegistry +public class ElgatoInputStream extends SeekableInputStream { + private static final byte XOR_VAL = 0x5E; + + private final SeekableInputStream inputStream; + + public ElgatoInputStream(SeekableInputStream inputStream) { + super(inputStream.getContentLength(), inputStream.getMaxSkipDistance()); + this.inputStream = inputStream; + } + + @Override + public long getPosition() { + return this.inputStream.getPosition(); + } + + // TODO: Will this work? + @Override + protected void seekHard(long position) throws IOException { + ((ElgatoInputStream) this.inputStream).seekHard(position); + } + + @Override + public boolean canSeekHard() { + return this.inputStream.canSeekHard(); + } + + @Override + public List getTrackInfoProviders() { + return this.inputStream.getTrackInfoProviders(); + } + + @Override + public int read() throws IOException { + final var read = this.inputStream.read(); + + if (read == -1) { + return -1; + } + + return read ^ XOR_VAL; + } + + @Override + public void close() throws IOException { + super.close(); + this.inputStream.close(); + } +} diff --git a/source-managers/src/main/java/com/dunctebot/sourcemanagers/elgato/streamdeck/StreamDeckAudioSourceManager.java b/source-managers/src/main/java/com/dunctebot/sourcemanagers/elgato/streamdeck/StreamDeckAudioSourceManager.java new file mode 100644 index 0000000..7ab3ae6 --- /dev/null +++ b/source-managers/src/main/java/com/dunctebot/sourcemanagers/elgato/streamdeck/StreamDeckAudioSourceManager.java @@ -0,0 +1,68 @@ +package com.dunctebot.sourcemanagers.elgato.streamdeck; + +import com.dunctebot.sourcemanagers.AbstractDuncteBotHttpSource; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.tools.Units; +import com.sedmelluq.discord.lavaplayer.track.AudioItem; +import com.sedmelluq.discord.lavaplayer.track.AudioReference; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Locale; + +// TODO: http vs local file +public class StreamDeckAudioSourceManager extends AbstractDuncteBotHttpSource { + @Override + public String getSourceName() { + return "StreamDeckAudio"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + final var url = new URI(reference.getIdentifier()); + + if (url.getPath().toLowerCase(Locale.ROOT).endsWith(".streamdeckaudio")) { + final var parts = List.of(url.getPath().split("/")); + final var fileName = parts.get(parts.size() - 1); + + return new StreamDeckAudioTrack( + new AudioTrackInfo( + fileName, + "Elgato", + Units.CONTENT_LENGTH_UNKNOWN, + fileName, + false, + url.toString() + ), + this + ); + } + } catch (URISyntaxException ignored) { + return null; + } + + return null; + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + // Nothing to encode + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new StreamDeckAudioTrack(trackInfo, this); + } +} diff --git a/source-managers/src/main/java/com/dunctebot/sourcemanagers/elgato/streamdeck/StreamDeckAudioTrack.java b/source-managers/src/main/java/com/dunctebot/sourcemanagers/elgato/streamdeck/StreamDeckAudioTrack.java new file mode 100644 index 0000000..117078c --- /dev/null +++ b/source-managers/src/main/java/com/dunctebot/sourcemanagers/elgato/streamdeck/StreamDeckAudioTrack.java @@ -0,0 +1,42 @@ +package com.dunctebot.sourcemanagers.elgato.streamdeck; + +import com.dunctebot.sourcemanagers.Mp3Track; +import com.sedmelluq.discord.lavaplayer.container.wav.WavAudioTrack; +import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; +import com.sedmelluq.discord.lavaplayer.track.InternalAudioTrack; + +public class StreamDeckAudioTrack extends Mp3Track { + private final StreamDeckAudioSourceManager manager; + + public StreamDeckAudioTrack(AudioTrackInfo trackInfo, StreamDeckAudioSourceManager manager) { + super(trackInfo, manager); + this.manager = manager; + } + + @Override + protected SeekableInputStream wrapStream(SeekableInputStream stream) { + return new ElgatoInputStream(stream); + } + + @Override + protected InternalAudioTrack createAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream stream) { + return new WavAudioTrack(trackInfo, stream); + } + + @Override + public String getPlaybackUrl() { + return this.trackInfo.uri; + } + + @Override + protected AudioTrack makeShallowClone() { + return new StreamDeckAudioTrack(this.trackInfo, this.manager); + } + + @Override + public StreamDeckAudioSourceManager getSourceManager() { + return this.manager; + } +} diff --git a/source-managers/src/test/java/LocalPlaybackTest.java b/source-managers/src/test/java/LocalPlaybackTest.java new file mode 100644 index 0000000..4b448bf --- /dev/null +++ b/source-managers/src/test/java/LocalPlaybackTest.java @@ -0,0 +1,55 @@ +import com.dunctebot.sourcemanagers.elgato.streamdeck.StreamDeckAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat; +import com.sedmelluq.discord.lavaplayer.format.AudioPlayerInputStream; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.FunctionalResultHandler; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.SourceDataLine; + +import static com.sedmelluq.discord.lavaplayer.format.StandardAudioDataFormats.COMMON_PCM_S16_BE; + +public class LocalPlaybackTest { + public static void main(String[] args) throws Exception { + final var mngr = new StreamDeckAudioSourceManager(); + + AudioPlayerManager manager = new DefaultAudioPlayerManager(); + + manager.registerSourceManager(mngr); + + manager.getConfiguration().setOutputFormat(COMMON_PCM_S16_BE); + + AudioPlayer player = manager.createPlayer(); + + player.setVolume(35); + + manager.loadItem( + "https://cdn.discordapp.com/attachments/340834322674089986/1242398908815118376/Fanfare_-_Show_Intro.streamDeckAudio?ex=664f0326&is=664db1a6&hm=9a4898f7301601b3bc14cfda4101aab0ed94cdfb5fe89d2a0917dc4e01514da6&", + new FunctionalResultHandler(item -> { + player.playTrack(item); + }, playlist -> { + player.playTrack(playlist.getTracks().get(0)); + }, null, null) + ); + + + AudioDataFormat format = manager.getConfiguration().getOutputFormat(); + AudioInputStream stream = AudioPlayerInputStream.createStream(player, format, 10000L, false); + SourceDataLine.Info info = new DataLine.Info(SourceDataLine.class, stream.getFormat()); + SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info); + + line.open(stream.getFormat()); + line.start(); + + byte[] buffer = new byte[COMMON_PCM_S16_BE.maximumChunkSize()]; + int chunkSize; + + while ((chunkSize = stream.read(buffer)) >= 0) { + line.write(buffer, 0, chunkSize); + } + } +} diff --git a/source-managers/src/test/resources/simplelogger.properties b/source-managers/src/test/resources/simplelogger.properties new file mode 100644 index 0000000..7c97c34 --- /dev/null +++ b/source-managers/src/test/resources/simplelogger.properties @@ -0,0 +1,3 @@ +org.slf4j.simpleLogger.defaultLogLevel=TRACE + +defaultLogLevel=TRACE