diff --git a/pom.xml b/pom.xml index fd0d864..4d4523a 100644 --- a/pom.xml +++ b/pom.xml @@ -19,10 +19,15 @@ spigotmc-repo https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + maxhenkel https://maven.maxhenkel.de/repository/public + + minebench-repo + https://repo.minebench.de/ + @@ -51,5 +56,38 @@ ffmpeg 0.8.0 + + + de.themoep + inventorygui + 1.6.4-SNAPSHOT + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + true + false + + + de.themoep:inventorygui + + + + + + + + diff --git a/src/main/java/eu/m724/musicPlugin/MusicCommands.java b/src/main/java/eu/m724/musicPlugin/MusicCommands.java deleted file mode 100644 index b14ba85..0000000 --- a/src/main/java/eu/m724/musicPlugin/MusicCommands.java +++ /dev/null @@ -1,137 +0,0 @@ -package eu.m724.musicPlugin; - -import eu.m724.musicPlugin.file.AudioFileStorage; -import eu.m724.musicPlugin.item.PortableMediaPlayer; -import eu.m724.musicPlugin.item.PortableMediaPlayers; -import eu.m724.musicPlugin.player.MusicPlayer; -import eu.m724.musicPlugin.file.AudioFile; -import eu.m724.musicPlugin.player.StaticMusicPlayer; -import net.md_5.bungee.api.chat.ClickEvent; -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.chat.HoverEvent; -import net.md_5.bungee.api.chat.hover.content.Text; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.net.URI; -import java.util.Map; - -public class MusicCommands implements CommandExecutor { - private final AudioFileStorage storage; - private MusicPlayer player; - - public MusicCommands(AudioFileStorage storage) { - this.storage = storage; - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (command.getName().equals("download")) { - try { - var cf = storage.download(URI.create(args[0]).toURL()); - sender.sendMessage("Started download"); - cf.handle((hash, ex) -> { - if (ex != null) - sender.sendMessage("ERROR downloading: " + ex.getMessage()); - else - sender.spigot().sendMessage( - new ComponentBuilder("Hash: " + hash) - .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to copy"))) - .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, hash)) - .append("\nClick here to convert to 48 kbps") - .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to do that"))) - .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/convert " + hash + " 48000")) - .create() - ); - return null; - }); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else if (command.getName().equals("convert")) { - try { - var cf = storage.convert(args[0], Integer.parseInt(args[1])); - sender.sendMessage("Converting " + args[0] + " to " + args[1] + "bps..."); - cf.thenAccept(f -> { - sender.spigot().sendMessage( - new ComponentBuilder("Converted " + args[0] + " to " + args[1] + "bps! Click to play") - .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to play"))) - .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/play " + args[0] + " " + args[1])) - .create() - ); - }).exceptionally(ex -> { - sender.sendMessage("ERROR converting: " + ex.getMessage()); - return null; - }); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else if (command.getName().equals("play")) { - if (args.length == 1) { - StringBuilder msg = new StringBuilder("Available bitrates:"); - for (Map.Entry entry : storage.getVersions(args[0]).entrySet()) { - msg.append(" ").append(entry.getKey()); - } - sender.sendMessage(msg.toString()); - return true; - } - var file = storage.get(args[0], Integer.parseInt(args[1])); - sender.sendMessage("Initializeng"); - player = new StaticMusicPlayer(((Player)sender).getLocation()); - player.init(); - - try (FileInputStream fis = new FileInputStream(file)) { - var song = new AudioFile(fis); - song.load(); - player.setAudio(song); - player.unpause(); - } catch (IOException e) { - throw new RuntimeException(e); - } - - sender.sendMessage("done si r now playing it"); - } else if (command.getName().equals("stop")) { - player.stop(); - sender.sendMessage("stopedd"); - } else if (command.getName().equals("pause")) { - player.pause(); - sender.sendMessage("puased"); - } else if (command.getName().equals("resume")) { - player.unpause(); - sender.sendMessage("unapuised"); - } else if (command.getName().equals("pmp")) { - var p = (Player) sender; - var pmp = PortableMediaPlayers.get(p.getItemInHand()); - - switch (args[0]) { - case "info" -> { - sender.sendMessage(String.valueOf(pmp.id)); - sender.sendMessage(String.valueOf(pmp.premium)); - sender.sendMessage(pmp.engraving); - } - case "create" -> { - pmp = PortableMediaPlayer.create(args[0].equals("yes"), args[1]); - p.getInventory().addItem(pmp.getItemStack()); - } - case "play" -> { - var file = storage.get(args[1], Integer.parseInt(args[2])); - - try (FileInputStream fis = new FileInputStream(file)) { - var song = new AudioFile(fis); - song.load(); - pmp.play(song); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - } - - return true; - } -} diff --git a/src/main/java/eu/m724/musicPlugin/MusicPlugin.java b/src/main/java/eu/m724/musicPlugin/MusicPlugin.java deleted file mode 100644 index 2309119..0000000 --- a/src/main/java/eu/m724/musicPlugin/MusicPlugin.java +++ /dev/null @@ -1,61 +0,0 @@ -package eu.m724.musicPlugin; - -import de.maxhenkel.voicechat.api.BukkitVoicechatService; -import de.maxhenkel.voicechat.api.VoicechatConnection; -import de.maxhenkel.voicechat.api.VoicechatServerApi; -import eu.m724.musicPlugin.file.AudioFileStorage; -import eu.m724.musicPlugin.item.ItemEvents; -import eu.m724.musicPlugin.item.speaker.BlockChecker; -import org.bukkit.plugin.java.JavaPlugin; - -public final class MusicPlugin extends JavaPlugin { - @Override - public void onEnable() { - Statics.plugin = this; - - BukkitVoicechatService service = getServer().getServicesManager().load(BukkitVoicechatService.class); - - if (service != null) { - service.registerPlugin( - new MyVoicechatPlugin( - this::onApiStarted, - this::onPlayerConnected - ) - ); - } - - getDataFolder().mkdir(); - var mcmd = new MusicCommands(new AudioFileStorage(getDataFolder())); - getCommand("download").setExecutor(mcmd); - getCommand("convert").setExecutor(mcmd); - getCommand("play").setExecutor(mcmd); - getCommand("stop").setExecutor(mcmd); - getCommand("pause").setExecutor(mcmd); - getCommand("resume").setExecutor(mcmd); - getCommand("pmp").setExecutor(mcmd); - - getServer().getPluginManager().registerEvents(new ItemEvents(), this); - - // TODO do this better, maybe along events - new BlockChecker().runTaskTimerAsynchronously(this, 0, 20); - } - - private void onApiStarted(VoicechatServerApi api) { - getLogger().info("registerating..."); - - var category = api.volumeCategoryBuilder() - .setId("musicc") - .setName("Music players") - .build(); - - api.registerVolumeCategory(category); - - Statics.api = api; - - getLogger().info("Sucess"); - } - - private void onPlayerConnected(VoicechatConnection connection) { - getLogger().info("Player connected: " + connection); - } -} diff --git a/src/main/java/eu/m724/musicPlugin/MyVoicechatPlugin.java b/src/main/java/eu/m724/musicPlugin/MyVoicechatPlugin.java deleted file mode 100644 index 0a254ad..0000000 --- a/src/main/java/eu/m724/musicPlugin/MyVoicechatPlugin.java +++ /dev/null @@ -1,45 +0,0 @@ -package eu.m724.musicPlugin; - -import de.maxhenkel.voicechat.api.VoicechatApi; -import de.maxhenkel.voicechat.api.VoicechatConnection; -import de.maxhenkel.voicechat.api.VoicechatPlugin; -import de.maxhenkel.voicechat.api.VoicechatServerApi; -import de.maxhenkel.voicechat.api.events.EventRegistration; -import de.maxhenkel.voicechat.api.events.PlayerConnectedEvent; -import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent; - -import java.util.function.Consumer; - -public class MyVoicechatPlugin implements VoicechatPlugin { - private final Consumer apiConsumer; - private final Consumer playerConnected; - - public MyVoicechatPlugin(Consumer apiConsumer, Consumer playerConnected) { - this.apiConsumer = apiConsumer; - this.playerConnected = playerConnected; - } - - @Override - public String getPluginId() { - return "myplgugonbo"; - } - - @Override - public void initialize(VoicechatApi api) { - VoicechatPlugin.super.initialize(api); - } - - @Override - public void registerEvents(EventRegistration registration) { - registration.registerEvent(VoicechatServerStartedEvent.class, this::onServerStarted); - registration.registerEvent(PlayerConnectedEvent.class, this::onPlayerConnected); - } - - public void onServerStarted(VoicechatServerStartedEvent event) { - apiConsumer.accept(event.getVoicechat()); - } - - public void onPlayerConnected(PlayerConnectedEvent event) { - playerConnected.accept(event.getConnection()); - } -} diff --git a/src/main/java/eu/m724/musicPlugin/Statics.java b/src/main/java/eu/m724/musicPlugin/Statics.java deleted file mode 100644 index 55bda0c..0000000 --- a/src/main/java/eu/m724/musicPlugin/Statics.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.m724.musicPlugin; - -import de.maxhenkel.voicechat.api.VoicechatServerApi; - -// TODO find a better way -public class Statics { - public static MusicPlugin plugin; - public static VoicechatServerApi api; -} diff --git a/src/main/java/eu/m724/musicPlugin/file/AudioFile.java b/src/main/java/eu/m724/musicPlugin/file/AudioFile.java deleted file mode 100644 index 63c1d48..0000000 --- a/src/main/java/eu/m724/musicPlugin/file/AudioFile.java +++ /dev/null @@ -1,72 +0,0 @@ -package eu.m724.musicPlugin.file; - -import org.gagravarr.ogg.OggFile; -import org.gagravarr.opus.OpusAudioData; -import org.gagravarr.opus.OpusFile; - -import java.io.*; -import java.util.ArrayList; - -public class AudioFile { - private final InputStream inputStream; - - private NotEncoder encoder; - private String title, artist; - private int duration; - - public AudioFile(File file) { - try { - this.inputStream = new FileInputStream(file); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } - - public AudioFile(InputStream inputStream) { - this.inputStream = inputStream; - } - - public void load() throws IOException { - if (encoder != null) return; - - var list = new ArrayList(); - - var opus = new OpusFile(new OggFile(inputStream)); - var frames = 0; - - OpusAudioData packet; - while ((packet = opus.getNextAudioPacket()) != null) { - list.add(packet.getData()); - frames += packet.getNumberOfFrames(); - } - - this.duration = frames / 50; // a frame is 20 ms long so 20 * 50 is 1000 - System.out.printf("audiop file has %s framrs\n", frames); - - this.title = opus.getTags().getTitle(); - this.artist = opus.getTags().getArtist(); - - this.encoder = new NotEncoder(list.toArray(byte[][]::new)); - } - - - public NotEncoder getEncoder() { - return encoder; - } - - /** - * @return if data was loaded i.e. if load was called - */ - public boolean isLoaded() { - return encoder != null; - } - - /** @return Track duration in seconds */ - public int getTrackDuration() { return duration; } - - /** @return Track title if available */ - public String getTrackTitle() { return title; } - - /** @return Track artist if available */ - public String getTrackArtist() { return artist; } -} diff --git a/src/main/java/eu/m724/musicPlugin/file/AudioFileConverter.java b/src/main/java/eu/m724/musicPlugin/file/AudioFileConverter.java deleted file mode 100644 index af13a2e..0000000 --- a/src/main/java/eu/m724/musicPlugin/file/AudioFileConverter.java +++ /dev/null @@ -1,39 +0,0 @@ -package eu.m724.musicPlugin.file; - -import net.bramp.ffmpeg.FFmpeg; -import net.bramp.ffmpeg.FFmpegExecutor; -import net.bramp.ffmpeg.builder.FFmpegBuilder; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.concurrent.CompletableFuture; - -public class AudioFileConverter { - private final FFmpeg ffmpeg; - - public AudioFileConverter(FFmpeg ffmpeg) { - this.ffmpeg = ffmpeg; - } - - public CompletableFuture convert(File input, File output, int bitrate) throws IOException { - return convert(input.getAbsolutePath(), output, bitrate); - } - - public CompletableFuture convert(URL source, File output, int bitrate) throws IOException { - return convert(source.toString(), output, bitrate); - } - - private CompletableFuture convert(String input, File output, int bitrate) throws IOException { - var builder = new FFmpegBuilder() - .setInput(input) - .addOutput(output.getAbsolutePath()) - .setAudioChannels(1) - .setAudioSampleRate(48_000) - .setAudioBitRate(bitrate) - .done(); - - var executor = new FFmpegExecutor(ffmpeg); - return CompletableFuture.runAsync(executor.createJob(builder)); - } -} diff --git a/src/main/java/eu/m724/musicPlugin/file/AudioFileStorage.java b/src/main/java/eu/m724/musicPlugin/file/AudioFileStorage.java deleted file mode 100644 index c35a783..0000000 --- a/src/main/java/eu/m724/musicPlugin/file/AudioFileStorage.java +++ /dev/null @@ -1,118 +0,0 @@ -package eu.m724.musicPlugin.file; - -import net.bramp.ffmpeg.FFmpeg; -import org.apache.commons.lang3.RandomStringUtils; - -import java.io.*; -import java.net.URL; -import java.nio.file.Files; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.stream.Collectors; - -public class AudioFileStorage { - private final File basePath; - private final AudioFileConverter converter; - - public AudioFileStorage(File basePath) { - this.basePath = basePath; - - try { - this.converter = new AudioFileConverter(new FFmpeg()); // TODO better error handling and not here - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Get versions (bitrates) of an audio file - * - * @param hash TODO NOT SANITIZED - * @return Map of Bps to file, or null if no such hash - */ - public Map getVersions(String hash) { - var dir = new File(basePath, hash); - - var files = dir.listFiles(); - if (files == null) return null; - - return Arrays.stream(files).collect(Collectors.toMap( - f -> Integer.parseInt(f.getName().split("\\.")[0]), - f -> f - )); - } - - /** - * Get an audio file of given original hash and bitrate - * - * @param hash TODO NOT SANITIZED - * @param bitrate the bitrate in bps like 32000 - */ - public File get(String hash, int bitrate) { - return new File(basePath, hash + "/" + bitrate + ".opus"); - } - - /** - * Get the original file with a hash - */ - public File getOriginal(String hash) { - return new File(basePath, hash + "/0.original"); - } - - /** - * Converts a (original) file to some bitrate - * - * @param hash the hash - * @param bitrate the target bitrate in bps - * @return the future with the new file - */ - public CompletableFuture convert(String hash, int bitrate) throws IOException { - var file = get(hash, bitrate); - if (file.exists()) return CompletableFuture.completedFuture(file); - - var og = getOriginal(hash); - if (!og.exists()) return null; - - return converter.convert(og, file, bitrate).thenApply(v -> file); - } - - /** - * Downloads an audio file and saves it as original - * - * @param url the url to download from - * @return the hash - */ - public CompletableFuture download(URL url) { - return CompletableFuture.supplyAsync(() -> { - try { - var temp = new File(basePath, "temp_" + RandomStringUtils.randomAlphabetic(8)); - var digest = MessageDigest.getInstance("SHA-256"); - - try ( - var input = new BufferedInputStream(url.openStream()); - var output = new FileOutputStream(temp) - ) { - var dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = input.read(dataBuffer, 0, 1024)) != -1) { - output.write(dataBuffer, 0, bytesRead); - digest.update(dataBuffer, 0, bytesRead); - } - } - - String hash = HexFormat.of().formatHex(digest.digest()); - var og = getOriginal(hash); - og.getParentFile().mkdir(); - if (!og.exists()) - Files.move(temp.toPath(), og.toPath()); - - return hash; - } catch (IOException | NoSuchAlgorithmException e) { - throw new CompletionException(e); - } - }); - } -} diff --git a/src/main/java/eu/m724/musicPlugin/file/NotEncoder.java b/src/main/java/eu/m724/musicPlugin/file/NotEncoder.java deleted file mode 100644 index 9aaf626..0000000 --- a/src/main/java/eu/m724/musicPlugin/file/NotEncoder.java +++ /dev/null @@ -1,47 +0,0 @@ -package eu.m724.musicPlugin.file; - -import de.maxhenkel.voicechat.api.opus.OpusEncoder; - -public class NotEncoder implements OpusEncoder { - private final byte[][] opusFrames; - private int i = 0; - private final int size; - - private boolean closed = false; - - public NotEncoder(byte[][] opusFrames) { - this.opusFrames = opusFrames; - this.size = opusFrames.length; - } - - public void seek(int frame) { - i = Math.clamp(frame, 0, size); - } - - public int getFrame() { return i; } - - public boolean hasRemaining() { - return i < size; - } - - @Override - public byte[] encode(short[] rawAudio) { - if (i < size) - return opusFrames[i++]; - return null; - } - - @Override - public void resetState() { - closed = false; - i = 0; - } - - @Override - public boolean isClosed() { - return closed; - } - - @Override - public void close() { closed = true; } -} diff --git a/src/main/java/eu/m724/musicPlugin/item/speaker/Speaker.java b/src/main/java/eu/m724/musicPlugin/item/speaker/Speaker.java deleted file mode 100644 index 8a5c6fb..0000000 --- a/src/main/java/eu/m724/musicPlugin/item/speaker/Speaker.java +++ /dev/null @@ -1,35 +0,0 @@ -package eu.m724.musicPlugin.item.speaker; - -import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; -import eu.m724.musicPlugin.file.AudioFile; -import eu.m724.musicPlugin.player.MovingMusicPlayer; -import eu.m724.musicPlugin.player.MusicPlayer; - -import java.util.function.Consumer; - -public abstract class Speaker { - private final MusicPlayer musicPlayer; - private Consumer destroyCallback; - - protected Speaker(MusicPlayer musicPlayer) { - this.musicPlayer = musicPlayer; - musicPlayer.init(); - } - - public void destroy() { - musicPlayer.stop(); - - onDestroy(); - destroyCallback.accept(null); - } - - public void setDestroyCallback(Consumer consumer) { - this.destroyCallback = consumer; - } - - public MusicPlayer getMusicPlayer() { - return musicPlayer; - } - - abstract void onDestroy(); -} diff --git a/src/main/java/eu/m724/musicPlugin/player/EntityMusicPlayer.java b/src/main/java/eu/m724/musicPlugin/player/EntityMusicPlayer.java deleted file mode 100644 index becaabd..0000000 --- a/src/main/java/eu/m724/musicPlugin/player/EntityMusicPlayer.java +++ /dev/null @@ -1,33 +0,0 @@ -package eu.m724.musicPlugin.player; - -import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; -import de.maxhenkel.voicechat.api.audiochannel.EntityAudioChannel; -import org.bukkit.entity.Entity; - -import java.util.UUID; - -public class EntityMusicPlayer extends MusicPlayer { - private final Entity entity; - - public EntityMusicPlayer(Entity entity) { - this.entity = entity; - } - - @Override - AudioChannel createChannel() { - var channel = api.createEntityAudioChannel( - UUID.randomUUID(), - api.fromEntity(entity) - ); - - channel.setCategory("musicc"); - channel.setDistance(32); - - return channel; - } - - @Override - public void setDistance(int distance) { - ((EntityAudioChannel)this.channel).setDistance(distance); - } -} diff --git a/src/main/java/eu/m724/musicPlugin/player/LocalMusicPlayer.java b/src/main/java/eu/m724/musicPlugin/player/LocalMusicPlayer.java deleted file mode 100644 index 7c54947..0000000 --- a/src/main/java/eu/m724/musicPlugin/player/LocalMusicPlayer.java +++ /dev/null @@ -1,41 +0,0 @@ -package eu.m724.musicPlugin.player; - -import de.maxhenkel.voicechat.api.ServerLevel; -import de.maxhenkel.voicechat.api.VoicechatConnection; -import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; -import eu.m724.musicPlugin.Statics; -import org.bukkit.entity.Player; - -import java.util.UUID; - -public class LocalMusicPlayer extends MusicPlayer { - private final ServerLevel level; - private final VoicechatConnection connection; - - public LocalMusicPlayer(Player player) throws NotConnectedException { - this.level = Statics.api.fromServerLevel(player.getWorld()); - this.connection = Statics.api.getConnectionOf(player.getUniqueId()); - - if (connection == null) { - throw new NotConnectedException(); - } - } - - @Override - AudioChannel createChannel() { - var channel = api.createStaticAudioChannel( - UUID.randomUUID(), - level, - connection - ); - - channel.setCategory("musicc"); - - return channel; - } - - @Override - public void setDistance(int distance) { } // distance doesn't apply to this one - - public static class NotConnectedException extends Exception {} -} diff --git a/src/main/java/eu/m724/musicPlugin/player/MovingMusicPlayer.java b/src/main/java/eu/m724/musicPlugin/player/MovingMusicPlayer.java deleted file mode 100644 index d157bb0..0000000 --- a/src/main/java/eu/m724/musicPlugin/player/MovingMusicPlayer.java +++ /dev/null @@ -1,56 +0,0 @@ -package eu.m724.musicPlugin.player; - -import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; -import de.maxhenkel.voicechat.api.audiochannel.LocationalAudioChannel; -import org.bukkit.Location; -import org.bukkit.entity.Entity; - -import java.util.UUID; - -public class MovingMusicPlayer extends MusicPlayer { - private Entity entity; - private Location location; - - public void moveTo(Location location) { - System.out.println("Mvoed to a"); - - this.location = location; - this.channel = createChannel(); - - if (isReady() && !isPlaying()) { - this.pause(); - this.unpause(); - } - } - - public void attachTo(Entity entity) { - var channel = api.createEntityAudioChannel( - UUID.randomUUID(), - api.fromEntity(entity) - ); - - channel.setCategory("musicc"); - channel.setDistance(4); - - this.channel = channel; - } - - @Override - AudioChannel createChannel() { - var channel = api.createLocationalAudioChannel( - UUID.randomUUID(), - api.fromServerLevel(location.getWorld()), - api.createPosition(location.getX(), location.getY(), location.getZ()) - ); - - channel.setCategory("musicc"); - channel.setDistance(4); - - return channel; - } - - @Override - public void setDistance(int distance) { - ((LocationalAudioChannel)this.channel).setDistance(distance); - } -} diff --git a/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java b/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java deleted file mode 100644 index 3c8b6c1..0000000 --- a/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java +++ /dev/null @@ -1,181 +0,0 @@ -package eu.m724.musicPlugin.player; - -import de.maxhenkel.voicechat.api.VoicechatServerApi; -import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; -import de.maxhenkel.voicechat.api.audiochannel.AudioPlayer; -import eu.m724.musicPlugin.Statics; -import eu.m724.musicPlugin.file.AudioFile; - -import java.io.IOException; -import java.util.function.Consumer; - -public abstract class MusicPlayer { - // TODO find a better way - final VoicechatServerApi api = Statics.api; - - AudioChannel channel; - - private boolean ready = false, playing = false; - private AudioPlayer player; - private AudioFile audioFile; - - private Consumer onAction = (r) -> {}; - - abstract AudioChannel createChannel(); - abstract public void setDistance(int distance); - - /** - * Initializes this music player - */ - public void init() { - this.channel = createChannel(); - } - - /** - * Set the consumer that will be called after playback is paused, stopped, resumed etc
- * There can be only one for one music player - * - * @see TrackAction - */ - public void setOnAction(Consumer onAction) { - this.onAction = onAction; - } - - - /* Playback control */ - - /** - * Sets audio file to play
- * If it's not loaded, it will be loaded synchronously.
- * The file is not rewinded. If you want to start from beginning, {@link MusicPlayer#seek(int)} - * After this, do {@link MusicPlayer#unpause()} - * - * @see MusicPlayer#pause() - * @see MusicPlayer#stop() - */ - public void setAudio(AudioFile audioFile) { - if (audioFile != null) - this.audioFile = audioFile; - - if (ready) - stop(); - - if (!audioFile.isLoaded()) { - try { - System.out.println("Audio not already loaded, so I load it"); - audioFile.load(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - System.out.printf("Audio loaded it's %d seconds long, so I'm guessing %d frames\n", audioFile.getTrackDuration(), audioFile.getTrackDuration() * 50); - - ready = true; - - unpause(); - } - - public void unpause() { - if (!ready || playing) return; - System.out.println("playback unpaused"); - - var sa = new short[960]; - var enc = audioFile.getEncoder(); - - player = api.createAudioPlayer(channel, enc, () -> enc.hasRemaining() ? sa : null); - player.setOnStopped(this::onStop); - player.startPlaying(); - - // TODO this may take a long time and it might glitch if it does - onAction.accept(TrackAction.UNPAUSE); - - playing = true; - - System.out.println("playback unpaused now"); - } - - /** - * Stops playback and rewinds - */ - public void stop() { - if (!ready) return; - - stopPlayback(false); - audioFile.getEncoder().resetState(); - } - - /** - * Pauses playback - */ - public void pause() { - if (!ready || playing) return; - stopPlayback(true); - } - - /** - * Seek to some point in current track - * - * @param target the target time in milliseconds - */ - public void seek(int target) { - audioFile.getEncoder().seek(target / 20); // a frame is 20 ms usually - } - - - /* Internal methods */ - - - private void stopPlayback(boolean pause) { - System.out.println("playback stopped"); - playing = false; - ready = pause; - - player.stopPlaying(); - player = null; - - onStop(); - } - - private void onStop() { - if (ready && !playing) { // paused - System.out.println("I detected pause"); - onAction.accept(TrackAction.PAUSE); - playing = false; - } else if (ready) { // not paused and still playing - System.out.println("I detected end"); - onAction.accept(TrackAction.DURATION); - ready = false; - } else { // not playing - System.out.println("I detected stop"); - onAction.accept(TrackAction.STOP); - } - } - - public enum TrackAction { - /** - * Song stopped playing because it played - */ - DURATION, - /** - * {@link MusicPlayer#stop()} was called, or started playing a new track - */ - STOP, - /** - * {@link MusicPlayer#pause()} was called - */ - PAUSE, - /** - * {@link MusicPlayer#unpause()} was called - */ - UNPAUSE - } - - public boolean isReady() { - return ready; - } - - public boolean isPlaying() { - return ready && playing; - } -} diff --git a/src/main/java/eu/m724/music_plugin/DebugLogger.java b/src/main/java/eu/m724/music_plugin/DebugLogger.java new file mode 100644 index 0000000..11eecb0 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/DebugLogger.java @@ -0,0 +1,51 @@ +package eu.m724.music_plugin; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DebugLogger { + static Logger logger; + + public static void info(String message, Object... format) { + log(Level.INFO, message, format); + } + + public static void warning(String message, Object... format) { + log(Level.WARNING, message, format); + } + + public static void severe(String message, Object... format) { + log(Level.SEVERE, message, format); + } + + public static void fine(String message, Object... format) { + log(Level.FINE, message, format); + } + + public static void finer(String message, Object... format) { + log(Level.FINER, message, format); + } + + private static void log(Level level, String message, Object... format) { + if (logger.getLevel().intValue() > level.intValue()) return; + + var caller = Thread.currentThread().getStackTrace()[3].getClassName(); + + if (caller.startsWith("eu.m724.music_plugin.")) + caller = caller.substring(21); + + message = "[" + caller + "] " + message.formatted(format); + + if (level.intValue() < Level.INFO.intValue()) { // levels below info are never logged even if set for some reason + // colors text gray (cyan is close to gray) + if (level == Level.FINE) { + message = "\033[38;5;250m" + message + "\033[39m"; + } else { + message = "\033[38;5;245m" + message + "\033[39m"; + } + level = Level.INFO; + } + + logger.log(level, message); + } +} \ No newline at end of file diff --git a/src/main/java/eu/m724/music_plugin/MusicCommands.java b/src/main/java/eu/m724/music_plugin/MusicCommands.java new file mode 100644 index 0000000..866c1ce --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/MusicCommands.java @@ -0,0 +1,44 @@ +package eu.m724.music_plugin; + +import eu.m724.music_plugin.item.PortableMediaPlayer; +import eu.m724.music_plugin.item.PortableMediaPlayers; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.io.IOException; + +public class MusicCommands implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (command.getName().equals("pmp")) { + var p = (Player) sender; + var pmp = PortableMediaPlayers.get(p.getItemInHand()); + + switch (args[0]) { + case "info" -> { + sender.sendMessage(String.valueOf(pmp.id)); + sender.sendMessage(String.valueOf(pmp.premium)); + sender.sendMessage(pmp.engraving); + } + case "create" -> { + pmp = PortableMediaPlayer.create(args[0].equals("yes"), args[1]); + p.getInventory().addItem(pmp.getItemStack()); + } + case "play" -> { + var storage = MusicPlugin.getStorage(); + var path = storage.get(args[1], Integer.parseInt(args[2])); + + try { + pmp.play(path.toFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } + + return true; + } +} diff --git a/src/main/java/eu/m724/music_plugin/MusicPlugin.java b/src/main/java/eu/m724/music_plugin/MusicPlugin.java new file mode 100644 index 0000000..000bc48 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/MusicPlugin.java @@ -0,0 +1,90 @@ +package eu.m724.music_plugin; + +import de.maxhenkel.voicechat.api.BukkitVoicechatService; +import de.maxhenkel.voicechat.api.VoicechatServerApi; +import eu.m724.music_plugin.audio.storage.AudioFileStorage; +import eu.m724.music_plugin.audio.Converter; +import eu.m724.music_plugin.audio.storage.Downloader; +import eu.m724.music_plugin.item.ItemEvents; +import eu.m724.music_plugin.item.speaker.BlockChecker; +import net.bramp.ffmpeg.FFmpeg; +import org.bukkit.NamespacedKey; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; +import java.util.logging.Level; + +public final class MusicPlugin extends JavaPlugin { + private static MusicPlugin INSTANCE; + private static VoicechatServerApi VOICECHAT_API; + private static AudioFileStorage AUDIO_FILE_STORAGE; + private static Converter CONVERTER; + private static Downloader DOWNLOADER; + + @Override + public void onEnable() { + var start = System.nanoTime(); + INSTANCE = this; + + getLogger().setLevel(Level.FINEST); + DebugLogger.logger = getLogger(); + + var service = getServer().getServicesManager().load(BukkitVoicechatService.class); + service.registerPlugin(new MyVoicechatPlugin(api -> VOICECHAT_API = api)); + + getDataFolder().mkdir(); + var storagePath = getDataFolder().toPath().resolve("storage"); + + AUDIO_FILE_STORAGE = new AudioFileStorage(storagePath); + DOWNLOADER = new Downloader(storagePath); + + try { + CONVERTER = new Converter(new FFmpeg()); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize FFmpeg", e); + } + + getCommand("test").setExecutor(new TestCommand()); + + /*var mcmd = new MusicCommands(new AudioFileStorage(getDataFolder())); + getCommand("download").setExecutor(mcmd); + getCommand("convert").setExecutor(mcmd); + getCommand("play").setExecutor(mcmd); + getCommand("stop").setExecutor(mcmd); + getCommand("pause").setExecutor(mcmd); + getCommand("resume").setExecutor(mcmd); + getCommand("pmp").setExecutor(mcmd);*/ + + getServer().getPluginManager().registerEvents(new ItemEvents(), this); + + // TODO do this better, maybe along events + new BlockChecker().runTaskTimerAsynchronously(this, 0, 20); + + var end = System.nanoTime(); + DebugLogger.fine("Enabled in %.3f milliseconds", (end - start) / 1000000.0); + } + + public static MusicPlugin getInstance() { + return INSTANCE; + } + + public static VoicechatServerApi getVoicechatApi() { + return VOICECHAT_API; + } + + public static AudioFileStorage getStorage() { + return AUDIO_FILE_STORAGE; + } + + public static Converter getConverter() { + return CONVERTER; + } + + public static Downloader getDownloader() { + return DOWNLOADER; + } + + public static NamespacedKey getNamespacedKey(String key) { + return new NamespacedKey(INSTANCE, key); + } +} diff --git a/src/main/java/eu/m724/music_plugin/MyVoicechatPlugin.java b/src/main/java/eu/m724/music_plugin/MyVoicechatPlugin.java new file mode 100644 index 0000000..9f56d14 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/MyVoicechatPlugin.java @@ -0,0 +1,42 @@ +package eu.m724.music_plugin; + +import de.maxhenkel.voicechat.api.VoicechatPlugin; +import de.maxhenkel.voicechat.api.VoicechatServerApi; +import de.maxhenkel.voicechat.api.events.EventRegistration; +import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent; + +import java.util.function.Consumer; + +public class MyVoicechatPlugin implements VoicechatPlugin { + private final Consumer apiConsumer; + + public MyVoicechatPlugin(Consumer apiConsumer) { + this.apiConsumer = apiConsumer; + } + + @Override + public String getPluginId() { + return "music724"; + } + + @Override + public void registerEvents(EventRegistration registration) { + registration.registerEvent(VoicechatServerStartedEvent.class, this::onServerStarted); + } + + private void onServerStarted(VoicechatServerStartedEvent event) { + var api = event.getVoicechat(); + + var category = api.volumeCategoryBuilder() + .setId("music") + .setName("Music players") + .build(); + + api.registerVolumeCategory(category); + + apiConsumer.accept(api); + + DebugLogger.fine("Registered"); + } +} + diff --git a/src/main/java/eu/m724/music_plugin/TestCommand.java b/src/main/java/eu/m724/music_plugin/TestCommand.java new file mode 100644 index 0000000..d4a5180 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/TestCommand.java @@ -0,0 +1,99 @@ +package eu.m724.music_plugin; + +import eu.m724.music_plugin.audio.channel.LocationalChannel; +import eu.m724.music_plugin.audio.player.OpusFilePlayer; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.gagravarr.opus.OpusFile; + +import java.io.IOException; +import java.net.URI; + +// TODO remove +public class TestCommand implements CommandExecutor { + private OpusFilePlayer opusFilePlayer; + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + var player = (Player) sender; + + if (args[0].equals("download")) { + testDownload(player, args[1]); + } else if (args[0].equals("convert")) { + testConvert(player, args[1], Integer.parseInt(args[2])); + } else if (args[0].equals("play")) { + testPlay(player, args[1], Integer.parseInt(args[2])); + } else if (args[0].equals("pause")) { + opusFilePlayer.pause(); + } else if (args[0].equals("unpause")) { + opusFilePlayer.unpause(); + } + + return true; + } + + private void testDownload(Player player, String url) { + player.sendMessage("Downloading " + url); + MusicPlugin.getDownloader().download(URI.create(url)).handle((hash, ex) -> { + if (ex != null) + player.sendMessage("ERROR downloading: " + ex.getMessage()); + else + player.spigot().sendMessage( + new ComponentBuilder("Hash: " + hash) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to copy"))) + .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, hash)) + .append("\nClick here to convert to 48 kbps") + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to do that"))) + .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/convert " + hash + " 48000")) + .create() + ); + return null; + }); + } + + private void testConvert(Player player, String hash, int bitrate) { + try { + MusicPlugin.getStorage().convert(MusicPlugin.getConverter(), hash, bitrate).handle((f, ex) -> { + if (ex != null) + player.sendMessage("ERROR converting: " + ex.getMessage()); + else + player.spigot().sendMessage( + new ComponentBuilder("Converted " + hash.substring(0, 7) + "... to " + bitrate + "bps! Click to play") + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to play"))) + .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/test play " + hash + " " + bitrate)) + .create() + ); + return null; + }); + } catch (IOException e) { + e.printStackTrace(); + player.sendMessage("Error"); + } + } + + private void testPlay(Player player, String hash, int bitrate) { + var channel = new LocationalChannel(player.getLocation()).getAudioChannel(); + + if (opusFilePlayer != null) { + opusFilePlayer.stop(); + } + + try { + opusFilePlayer = new OpusFilePlayer(new OpusFile(MusicPlugin.getStorage().get(hash, bitrate).toFile()), channel); + opusFilePlayer.play(); + opusFilePlayer.setOnTrackEvent(e -> { + player.sendMessage("Event: " + e); + }); + player.sendMessage("Now playing"); + } catch (IOException e) { + e.printStackTrace(); + player.sendMessage("Error"); + } + } +} diff --git a/src/main/java/eu/m724/music_plugin/audio/Converter.java b/src/main/java/eu/m724/music_plugin/audio/Converter.java new file mode 100644 index 0000000..7eb3799 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/audio/Converter.java @@ -0,0 +1,56 @@ +package eu.m724.music_plugin.audio; + +import net.bramp.ffmpeg.FFmpeg; +import net.bramp.ffmpeg.FFmpegExecutor; +import net.bramp.ffmpeg.builder.FFmpegBuilder; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +public class Converter { + private final FFmpeg ffmpeg; + + public Converter(FFmpeg ffmpeg) { + this.ffmpeg = ffmpeg; + } + + /** + * Converts a media file to OPUS with given bitrate. + * + * @param bitrate the target bitrate in Bps (bits per second) + * @return the future that results in {@code output} when done + * @throws IOException if something went wrong with FFmpeg + */ + public CompletableFuture convert(Path input, Path output, int bitrate) throws IOException { + return convert(input.toAbsolutePath().toString(), output, bitrate); + } + + /** + * Converts a media file to OPUS with given bitrate. + * + * @deprecated It's better to use this plugin to download + * @param bitrate the target bitrate in Bps (bits per second) + * @return the future that results in {@code output} when done + * @throws IOException if something went wrong with FFmpeg + */ + @Deprecated(forRemoval = true) + public CompletableFuture convert(URL source, Path output, int bitrate) throws IOException { + return convert(source.toString(), output, bitrate); + } + + private CompletableFuture convert(String input, Path output, int bitrate) throws IOException { + var builder = new FFmpegBuilder() + .setInput(input) + .addOutput(output.toAbsolutePath().toUri()) + .setAudioChannels(1) + .setAudioSampleRate(48_000) + .setAudioBitRate(bitrate) + .done(); + + var executor = new FFmpegExecutor(ffmpeg); + return CompletableFuture.runAsync(executor.createJob(builder)).thenApply(v -> output); + } +} diff --git a/src/main/java/eu/m724/music_plugin/audio/OpusFileEncoder.java b/src/main/java/eu/m724/music_plugin/audio/OpusFileEncoder.java new file mode 100644 index 0000000..668ac27 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/audio/OpusFileEncoder.java @@ -0,0 +1,63 @@ +package eu.m724.music_plugin.audio; + +import de.maxhenkel.voicechat.api.opus.OpusEncoder; +import eu.m724.music_plugin.DebugLogger; +import org.apache.commons.lang3.NotImplementedException; +import org.gagravarr.opus.OpusFile; + +import java.io.File; +import java.io.IOException; + +public class OpusFileEncoder implements OpusEncoder { + private final OpusFile opusFile; + private boolean closed = false; + + public OpusFileEncoder(OpusFile file) { + this.opusFile = file; + } + + @Override + public byte[] encode(short[] rawAudio) { + try { + var packet = opusFile.getNextAudioPacket(); + if (packet != null) + return packet.getData(); + else + return new byte[0]; + } catch (IOException e) { + throw new RuntimeException("Reading next packet", e); + } + } + + @Override + public void resetState() { + // TODO + throw new NotImplementedException(); + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void close() { + DebugLogger.fine("opus file just closed"); + if (closed) return; + closed = true; + + try { + opusFile.close(); + } catch (IOException e) { + throw new RuntimeException("Closing opus file", e); + } + } + + /* SUPPLIER */ + + private final short[] array = new short[960]; + + public short[] supplier() { + return !closed ? array : null; + } +} diff --git a/src/main/java/eu/m724/music_plugin/audio/channel/AbstractChannel.java b/src/main/java/eu/m724/music_plugin/audio/channel/AbstractChannel.java new file mode 100644 index 0000000..3afdd0e --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/audio/channel/AbstractChannel.java @@ -0,0 +1,16 @@ +package eu.m724.music_plugin.audio.channel; + +import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; + +public abstract class AbstractChannel { + protected T channel; + + public T getAudioChannel() { + if (this.channel == null) + this.channel = doCreateChannel(); + return this.channel; + } + + protected abstract T doCreateChannel(); + abstract void setDistance(int distance); +} diff --git a/src/main/java/eu/m724/music_plugin/audio/channel/EntityChannel.java b/src/main/java/eu/m724/music_plugin/audio/channel/EntityChannel.java new file mode 100644 index 0000000..3a6b129 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/audio/channel/EntityChannel.java @@ -0,0 +1,36 @@ +package eu.m724.music_plugin.audio.channel; + +import de.maxhenkel.voicechat.api.audiochannel.EntityAudioChannel; +import eu.m724.music_plugin.MusicPlugin; +import org.bukkit.entity.Entity; + +import java.util.UUID; + +public class EntityChannel extends AbstractChannel { + private final Entity entity; + + public EntityChannel(Entity entity) { + this.entity = entity; + } + + @Override + protected EntityAudioChannel doCreateChannel() { + var api = MusicPlugin.getVoicechatApi(); + + var channel = api.createEntityAudioChannel( + UUID.randomUUID(), + api.fromEntity(entity) + ); + + channel.setCategory("music"); + channel.setDistance(32); + + return channel; + } + + @Override + public void setDistance(int distance) { + // we go getChannel() not just channel, because getChannel() creates it if it doesn't exist + getAudioChannel().setDistance(distance); + } +} diff --git a/src/main/java/eu/m724/musicPlugin/player/StaticMusicPlayer.java b/src/main/java/eu/m724/music_plugin/audio/channel/LocationalChannel.java similarity index 53% rename from src/main/java/eu/m724/musicPlugin/player/StaticMusicPlayer.java rename to src/main/java/eu/m724/music_plugin/audio/channel/LocationalChannel.java index f751555..b51fd38 100644 --- a/src/main/java/eu/m724/musicPlugin/player/StaticMusicPlayer.java +++ b/src/main/java/eu/m724/music_plugin/audio/channel/LocationalChannel.java @@ -1,27 +1,29 @@ -package eu.m724.musicPlugin.player; +package eu.m724.music_plugin.audio.channel; -import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; import de.maxhenkel.voicechat.api.audiochannel.LocationalAudioChannel; +import eu.m724.music_plugin.MusicPlugin; import org.bukkit.Location; import java.util.UUID; -public class StaticMusicPlayer extends MusicPlayer { +public class LocationalChannel extends AbstractChannel { private final Location location; - public StaticMusicPlayer(Location location) { + public LocationalChannel(Location location) { this.location = location; } @Override - AudioChannel createChannel() { + protected LocationalAudioChannel doCreateChannel() { + var api = MusicPlugin.getVoicechatApi(); + var channel = api.createLocationalAudioChannel( UUID.randomUUID(), api.fromServerLevel(location.getWorld()), api.createPosition(location.getX(), location.getY(), location.getZ()) ); - channel.setCategory("musicc"); + channel.setCategory("music"); channel.setDistance(32); return channel; @@ -29,6 +31,7 @@ public class StaticMusicPlayer extends MusicPlayer { @Override public void setDistance(int distance) { - ((LocationalAudioChannel)this.channel).setDistance(distance); + // we go getChannel() not just channel, because getChannel() creates it if it doesn't exist + getAudioChannel().setDistance(distance); } } diff --git a/src/main/java/eu/m724/music_plugin/audio/player/OpusFilePlayer.java b/src/main/java/eu/m724/music_plugin/audio/player/OpusFilePlayer.java new file mode 100644 index 0000000..dbcdcff --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/audio/player/OpusFilePlayer.java @@ -0,0 +1,129 @@ +package eu.m724.music_plugin.audio.player; + +import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; +import de.maxhenkel.voicechat.api.audiochannel.AudioPlayer; +import eu.m724.music_plugin.DebugLogger; +import eu.m724.music_plugin.MusicPlugin; +import eu.m724.music_plugin.audio.OpusFileEncoder; +import org.gagravarr.opus.OpusFile; + +import java.io.File; +import java.io.IOException; +import java.util.function.Consumer; + +public class OpusFilePlayer { + private final OpusFileEncoder encoder; + + private AudioChannel channel; + private AudioPlayer audioPlayer; + private boolean playing = false; + + private Consumer onTrackEvent = (r) -> {}; + + public OpusFilePlayer(OpusFile opusFile, AudioChannel channel) { + this.encoder = new OpusFileEncoder(opusFile); + this.channel = channel; + } + + public OpusFilePlayer(File file, AudioChannel channel) throws IOException { + this(new OpusFile(file), channel); + } + + /** + * Set the consumer that will be called after playback is paused, stopped, resumed etc
+ * There can be only one for one music player + * + * @see TrackEvent + */ + public void setOnTrackEvent(Consumer onTrackEvent) { + this.onTrackEvent = onTrackEvent; + } + + public void setChannel(AudioChannel channel) { + DebugLogger.finer("Changing channel..."); + this.channel = channel; + + if (!playing) { + // to not call events pointlessly + var ote = this.onTrackEvent; + this.onTrackEvent = (r) -> {}; + + // playing recreates the audio channel + pause(); + play(); + + this.onTrackEvent = ote; + } + + DebugLogger.fine("Channel changed"); + } + + /** + * Starts or resumes playback + */ + public void play() { + DebugLogger.finer("Playback starting..."); + if (playing) return; + + audioPlayer = MusicPlugin.getVoicechatApi().createAudioPlayer(channel, encoder, encoder::supplier); + audioPlayer.setOnStopped(this::onPlayerStopped); + audioPlayer.startPlaying(); + + playing = true; + + onTrackEvent.accept(TrackEvent.START); + DebugLogger.fine("Playback started"); + } + + /** + * An alias for {@link #play()} + */ + public void unpause() { + play(); + } + + /** + * Pause playback
+ * To resume, {@link #play()} + */ + public void pause() { + stop(false); + } + + /** + * Stops playback (pauses and rewinds) + */ + public void stop() { + stop(true); + } + + private void stop(boolean reset) { + DebugLogger.fine("Playback stopping... (reset: %s)", reset); + if (!playing) return; + + playing = false; + audioPlayer.stopPlaying(); + + if (reset) + encoder.resetState(); + + DebugLogger.fine("Playback stopped (reset: %s)", reset); + } + + private void onPlayerStopped() { + if (playing) { + DebugLogger.fine("onPlayerStopped called and playing, calling END"); + onTrackEvent.accept(TrackEvent.END); + } else { + DebugLogger.fine("onPlayerStopped called and not playing, calling STOP"); + onTrackEvent.accept(TrackEvent.STOP); + } + } + + /** + * @return Is player playing (not paused) + */ + public boolean isPlaying() { + return playing; + } +} diff --git a/src/main/java/eu/m724/music_plugin/audio/player/TrackEvent.java b/src/main/java/eu/m724/music_plugin/audio/player/TrackEvent.java new file mode 100644 index 0000000..b850e3a --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/audio/player/TrackEvent.java @@ -0,0 +1,17 @@ +package eu.m724.music_plugin.audio.player; + +public enum TrackEvent { + /** + * Track ended + */ + END, + /** + * Track was stopped (or paused)
+ * If it ended, {@link TrackEvent#END} is used instead + */ + STOP, + /** + * Track started playing (or unpaused) + */ + START +} diff --git a/src/main/java/eu/m724/music_plugin/audio/storage/AudioFileStorage.java b/src/main/java/eu/m724/music_plugin/audio/storage/AudioFileStorage.java new file mode 100644 index 0000000..f90433a --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/audio/storage/AudioFileStorage.java @@ -0,0 +1,89 @@ +package eu.m724.music_plugin.audio.storage; + +import eu.m724.music_plugin.audio.Converter; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class AudioFileStorage { + private final Path basePath; + + public AudioFileStorage(Path basePath) { + this.basePath = basePath; + } + + /** + * Get versions (bitrates) of an audio file
+ * Returns a map of bitrate: file, or null if no such hash
+ * Original is 0 + * + * @param hash TODO NOT SANITIZED + * @return Map of Bps to file, or null if no such hash + */ + public Map getVersions(String hash) throws IOException { + assert isHashValid(hash); + + var dir = basePath.resolve(hash); + + try (var files = Files.list(dir)) { + return files.collect(Collectors.toMap( + k -> Integer.parseInt(k.getFileName().toString().split("\\.")[0]), + v -> v + )); + } catch (NotDirectoryException e) { + return null; + } + } + + /** + * Get an audio file of given original hash and bitrate + * + * @param hash the hex sha256 hash of the original + * @param bitrate the bitrate in bps like 32000 + */ + public Path get(String hash, int bitrate) { + assert isHashValid(hash); + return basePath.resolve(hash).resolve(bitrate + ".opus"); + } + + /** + * Get the original file with a hash + */ + public Path getOriginal(String hash) { + assert isHashValid(hash); + return basePath.resolve(hash).resolve("0.original"); + } + + /** + * Checks if a hash is a valid SHA256 hash + * @param hash the hash in hex format + */ + private boolean isHashValid(String hash) { + var bytes = HexFormat.of().parseHex(hash); + return bytes.length == 32; + } + + // TODO move those + + /** + * Converts a (original) file to some bitrate + * + * @param hash the hash + * @param bitrate the target bitrate in bps + * @return the future with the new file + */ + public CompletableFuture convert(Converter converter, String hash, int bitrate) throws IOException { + var file = get(hash, bitrate); + if (Files.exists(file)) return CompletableFuture.completedFuture(file); + + var og = getOriginal(hash); + if (!Files.exists(og)) return null; + + return converter.convert(og, file, bitrate).thenApply(v -> file); + } +} diff --git a/src/main/java/eu/m724/music_plugin/audio/storage/Downloader.java b/src/main/java/eu/m724/music_plugin/audio/storage/Downloader.java new file mode 100644 index 0000000..47af394 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/audio/storage/Downloader.java @@ -0,0 +1,86 @@ +package eu.m724.music_plugin.audio.storage; + +import eu.m724.music_plugin.DebugLogger; +import org.apache.commons.lang3.RandomStringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class Downloader { + private final Path basePath; + + public Downloader(Path basePath) { + this.basePath = basePath; + } + + /** + * Downloads an audio file and saves it as original + * + * @param uri the uri to download from + * @return the downloaded file's SHA-256 hash (in a future) + */ + public CompletableFuture download(URI uri) { + DebugLogger.fine("About to download from " + uri); + + try ( + var client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build(); + ) { + var request = HttpRequest.newBuilder(uri).GET().build(); + return client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()).thenApply(this::futureFunction); + } + } + + private String futureFunction(HttpResponse response) { + var contentLength = response.headers().firstValue("Content-Length"); + DebugLogger.finer("Connected. Size: %s", contentLength.orElse(null)); + + var temp = basePath.resolve("temp_" + RandomStringUtils.randomAlphabetic(8)); + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + try ( + var input = response.body(); + var output = Files.newOutputStream(temp) + ) { + var dataBuffer = new byte[1024 * 1024]; + int bytesRead; + while ((bytesRead = input.read(dataBuffer, 0, dataBuffer.length)) != -1) { + output.write(dataBuffer, 0, bytesRead); + digest.update(dataBuffer, 0, bytesRead); + } + } catch (IOException e) { + throw new RuntimeException("Exception downloading from " + response.uri(), e); + } + + String hash = HexFormat.of().formatHex(digest.digest()); + DebugLogger.fine("Downloaded, hash: " + hash); + + try { + Files.createDirectory(basePath.resolve(hash)); + Files.move(temp, basePath.resolve(hash).resolve("0.original")); + } catch (FileAlreadyExistsException e) { + DebugLogger.fine("Already exists!"); + } catch (IOException e) { + throw new CompletionException("Exception saving file", e); + } + + DebugLogger.fine("Done"); + return hash; + } +} diff --git a/src/main/java/eu/m724/musicPlugin/item/ItemEvents.java b/src/main/java/eu/m724/music_plugin/item/ItemEvents.java similarity index 86% rename from src/main/java/eu/m724/musicPlugin/item/ItemEvents.java rename to src/main/java/eu/m724/music_plugin/item/ItemEvents.java index 66fcb6f..7e62569 100644 --- a/src/main/java/eu/m724/musicPlugin/item/ItemEvents.java +++ b/src/main/java/eu/m724/music_plugin/item/ItemEvents.java @@ -1,22 +1,13 @@ -package eu.m724.musicPlugin.item; +package eu.m724.music_plugin.item; -import eu.m724.musicPlugin.item.speaker.BlockSpeaker; -import eu.m724.musicPlugin.item.speaker.Speaker; -import eu.m724.musicPlugin.player.MovingMusicPlayer; +import eu.m724.music_plugin.item.speaker.BlockSpeaker; import org.bukkit.GameMode; import org.bukkit.Material; import org.bukkit.Particle; -import org.bukkit.block.Block; -import org.bukkit.entity.Item; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockDamageEvent; -import org.bukkit.event.entity.EntityDeathEvent; -import org.bukkit.event.entity.EntityDropItemEvent; import org.bukkit.event.entity.EntityPickupItemEvent; -import org.bukkit.event.entity.ItemDespawnEvent; import org.bukkit.event.inventory.InventoryMoveItemEvent; import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerDropItemEvent; diff --git a/src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayer.java b/src/main/java/eu/m724/music_plugin/item/PortableMediaPlayer.java similarity index 55% rename from src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayer.java rename to src/main/java/eu/m724/music_plugin/item/PortableMediaPlayer.java index 2d01d84..d0009d2 100644 --- a/src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayer.java +++ b/src/main/java/eu/m724/music_plugin/item/PortableMediaPlayer.java @@ -1,8 +1,10 @@ -package eu.m724.musicPlugin.item; +package eu.m724.music_plugin.item; -import eu.m724.musicPlugin.file.AudioFile; -import eu.m724.musicPlugin.item.speaker.Speaker; -import eu.m724.musicPlugin.player.MusicPlayer; +import eu.m724.music_plugin.DebugLogger; +import eu.m724.music_plugin.MusicPlugin; +import eu.m724.music_plugin.audio.player.OpusFilePlayer; +import eu.m724.music_plugin.audio.player.TrackEvent; +import eu.m724.music_plugin.item.speaker.Speaker; import net.md_5.bungee.api.ChatColor; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -11,14 +13,16 @@ import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.persistence.PersistentDataType; +import java.io.File; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.ThreadLocalRandom; public class PortableMediaPlayer { - static NamespacedKey idKey = new NamespacedKey("tweaks724", "player_id"); - static NamespacedKey dataKey = new NamespacedKey("tweaks724", "player_data"); + static final NamespacedKey idKey = MusicPlugin.getNamespacedKey("player_id"); + static final NamespacedKey dataKey = MusicPlugin.getNamespacedKey("player_data");; public final int id; @@ -29,8 +33,8 @@ public class PortableMediaPlayer { public final boolean premium; public final String engraving; - private PlayedSong played; - private Speaker speaker; + private OpusFilePlayer player; // TODO rename? + private Speaker speaker; PortableMediaPlayer(int id, int storageSeconds, int audioBitrate, boolean premium, String engraving) { this.id = id; @@ -44,7 +48,7 @@ public class PortableMediaPlayer { return new PortableMediaPlayer(ThreadLocalRandom.current().nextInt(), 600, 32000, premium, engraving); } - public void setSpeaker(Speaker speaker) { + public void setSpeaker(Speaker speaker) { if (speaker.equals(this.speaker)) return; @@ -54,80 +58,73 @@ public class PortableMediaPlayer { this.speaker.destroy(); } - speaker.setDestroyCallback(c -> { + speaker.setDestroyCallback(v -> { System.out.println("spekar rip"); - if (played != null) - played.pause(); // this one stops the tracker not playback, which is stopped in Speaker + if (player != null) + player.pause(); this.speaker = null; }); this.speaker = speaker; - - if (played != null && !played.paused()) { - unpause(); - } + player.setChannel(speaker.getChannel().getAudioChannel()); } - public void play(AudioFile audioFile) { + public void play(File file) throws IOException { + DebugLogger.finer("pmp play"); if (speaker == null) return; - played = new PlayedSong(audioFile); + //played = new Track(audioFile); + if (player != null) + player.stop(); - speaker.getMusicPlayer().setOnAction(action -> { - if (action == MusicPlayer.TrackAction.PAUSE) { - // track paused - System.out.println("Okay its paused"); - played.pause(); - played.hint(played.file.getEncoder().getFrame()); - } else if (action == MusicPlayer.TrackAction.UNPAUSE) { // track unpaused - System.out.println("Okay its unpaused"); - played.unpause(); - } else if (action == MusicPlayer.TrackAction.DURATION) { // track ended - System.out.println("Okay its ened"); - next(); + this.player = new OpusFilePlayer(file, speaker.getChannel().getAudioChannel()); + + player.setOnTrackEvent(event -> { + if (event == TrackEvent.START) { + DebugLogger.finer("I detected track START"); + //played.unpause(); + } else if (event == TrackEvent.STOP) { + DebugLogger.finer("I detected track STOP"); + //played.pause(); + //played.hint(played.file.getEncoder().getFrame()); + } else if (event == TrackEvent.END) { + DebugLogger.finer("I detected track END"); + //next(); } }); - unpause(); + player.play(); } void pause() { - System.out.println("pmp pase"); - if (played == null || speaker == null) return; - System.out.println("pmp pasedd"); + DebugLogger.finer("pmp pause"); - speaker.getMusicPlayer().pause(); + if (player != null) + player.pause(); } void unpause() { - System.out.println("pmp unpase"); - if (played == null || speaker == null) return; - System.out.println("pmp unpasedd"); + DebugLogger.finer("pmp unpause"); - speaker.getMusicPlayer().setAudio(played.file); - speaker.getMusicPlayer().seek(played.getProgress()); - speaker.getMusicPlayer().unpause(); + if (player != null) + player.unpause(); } void stop() { - System.out.println("pmp stop"); + DebugLogger.finer("pmp stop"); - if (speaker != null) - speaker.destroy(); - - this.played = null; + if (player != null) + player.stop(); } + // TODO void prev() { - System.out.println("pmp prev"); - this.played = new PlayedSong(played.file); - unpause(); + DebugLogger.fine("pmp previous (does nothing)"); } + // TODO void next() { - System.out.println("pmp next"); - this.played = null; - // TODO + System.out.println("pmp next (does nothing)"); } /* Item functions */ @@ -136,7 +133,7 @@ public class PortableMediaPlayer { var is = new ItemStack(Material.IRON_INGOT); var meta = is.getItemMeta(); - meta.setItemName("Portable music player"); + meta.setItemName("Portable Music Player"); meta.addEnchant(Enchantment.UNBREAKING, 1, false); meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); meta.getPersistentDataContainer().set(idKey, PersistentDataType.INTEGER, id); @@ -151,7 +148,7 @@ public class PortableMediaPlayer { private byte[] getData() { var buffer = ByteBuffer.allocate(11 + engraving.length()); - buffer.put((byte) 0); // version + buffer.put((byte) 0); // data format version buffer.put((byte) (premium ? 1 : 0)); buffer.putShort((short) storageSeconds); // make int if 18 hours is not enough. or store as minutes diff --git a/src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayers.java b/src/main/java/eu/m724/music_plugin/item/PortableMediaPlayers.java similarity index 98% rename from src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayers.java rename to src/main/java/eu/m724/music_plugin/item/PortableMediaPlayers.java index ebf7571..feab236 100644 --- a/src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayers.java +++ b/src/main/java/eu/m724/music_plugin/item/PortableMediaPlayers.java @@ -1,4 +1,4 @@ -package eu.m724.musicPlugin.item; +package eu.m724.music_plugin.item; import org.bukkit.inventory.ItemStack; import org.bukkit.persistence.PersistentDataType; diff --git a/src/main/java/eu/m724/musicPlugin/item/speaker/BlockChecker.java b/src/main/java/eu/m724/music_plugin/item/speaker/BlockChecker.java similarity index 85% rename from src/main/java/eu/m724/musicPlugin/item/speaker/BlockChecker.java rename to src/main/java/eu/m724/music_plugin/item/speaker/BlockChecker.java index 43e4d07..5586734 100644 --- a/src/main/java/eu/m724/musicPlugin/item/speaker/BlockChecker.java +++ b/src/main/java/eu/m724/music_plugin/item/speaker/BlockChecker.java @@ -1,4 +1,4 @@ -package eu.m724.musicPlugin.item.speaker; +package eu.m724.music_plugin.item.speaker; import org.bukkit.scheduler.BukkitRunnable; diff --git a/src/main/java/eu/m724/musicPlugin/item/speaker/BlockSpeaker.java b/src/main/java/eu/m724/music_plugin/item/speaker/BlockSpeaker.java similarity index 82% rename from src/main/java/eu/m724/musicPlugin/item/speaker/BlockSpeaker.java rename to src/main/java/eu/m724/music_plugin/item/speaker/BlockSpeaker.java index f167c96..2bc6aee 100644 --- a/src/main/java/eu/m724/musicPlugin/item/speaker/BlockSpeaker.java +++ b/src/main/java/eu/m724/music_plugin/item/speaker/BlockSpeaker.java @@ -1,17 +1,16 @@ -package eu.m724.musicPlugin.item.speaker; +package eu.m724.music_plugin.item.speaker; -import eu.m724.musicPlugin.Statics; -import eu.m724.musicPlugin.player.StaticMusicPlayer; +import eu.m724.music_plugin.DebugLogger; +import eu.m724.music_plugin.MusicPlugin; +import eu.m724.music_plugin.audio.channel.LocationalChannel; import org.bukkit.Location; import org.bukkit.Material; -import org.bukkit.Particle; import org.bukkit.metadata.FixedMetadataValue; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ThreadLocalRandom; -public class BlockSpeaker extends Speaker { +public class BlockSpeaker extends Speaker { static final Map speakers = new HashMap<>(); public static BlockSpeaker get(Location location) { @@ -25,7 +24,8 @@ public class BlockSpeaker extends Speaker { return null; var isTopSpeaker = below.getBlock().getType() == Material.NOTE_BLOCK; - System.out.println("SPekaer is top: " + isTopSpeaker); + DebugLogger.fine("SPekaer is top: " + isTopSpeaker); + return speakers.compute(isTopSpeaker ? below : location, (k, v) -> new BlockSpeaker(k)); } @@ -35,9 +35,9 @@ public class BlockSpeaker extends Speaker { private boolean large; public BlockSpeaker(Location location) { - super(new StaticMusicPlayer(location)); + super(new LocationalChannel(location)); this.location = location; - this.location.getBlock().setMetadata("t_speaker", new FixedMetadataValue(Statics.plugin, true)); + this.location.getBlock().setMetadata("t_speaker", new FixedMetadataValue(MusicPlugin.getInstance(), true)); } @Override @@ -50,17 +50,18 @@ public class BlockSpeaker extends Speaker { if (location.clone().add(0, 1, 0).getBlock().getType() == Material.NOTE_BLOCK) { if (!large) { System.out.println("Speaker now large"); - getMusicPlayer().setDistance(64); + getChannel().setDistance(64); this.large = true; } } else { if (large) { System.out.println("Speaker now small"); - getMusicPlayer().setDistance(32); + getChannel().setDistance(32); this.large = false; } } - if (getMusicPlayer().isPlaying()) { + // TODO bring it back + /*if (getMusicPlayer().isPlaying()) { if (large) { location.getWorld().spawnParticle(Particle.NOTE, location.clone().add(0.6, 0.5, 0), 1, 0, ThreadLocalRandom.current().nextDouble(-1, 0.5), ThreadLocalRandom.current().nextDouble(-0.4, 0.4)); location.getWorld().spawnParticle(Particle.NOTE, location.clone().add(-0.6, 0.5, 0), 1, 0, ThreadLocalRandom.current().nextDouble(-1, 0.5), ThreadLocalRandom.current().nextDouble(-0.4, 0.4)); @@ -69,7 +70,7 @@ public class BlockSpeaker extends Speaker { } else { location.getWorld().spawnParticle(Particle.NOTE, location.clone().add(0, 0.7, 0), 1, ThreadLocalRandom.current().nextDouble(-0.4, 0.4), 0, ThreadLocalRandom.current().nextDouble(-0.4, 0.4)); } - } + }*/ } else { System.out.println("Speaker disaper"); destroy(); diff --git a/src/main/java/eu/m724/music_plugin/item/speaker/Speaker.java b/src/main/java/eu/m724/music_plugin/item/speaker/Speaker.java new file mode 100644 index 0000000..51cf0a0 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/item/speaker/Speaker.java @@ -0,0 +1,29 @@ +package eu.m724.music_plugin.item.speaker; + +import eu.m724.music_plugin.audio.channel.AbstractChannel; + +import java.util.function.Consumer; + +public abstract class Speaker> { + private final T channel; + private Consumer destroyCallback; + + protected Speaker(T channel) { + this.channel = channel; + } + + public void destroy() { + onDestroy(); + destroyCallback.accept(null); + } + + public void setDestroyCallback(Consumer consumer) { + this.destroyCallback = consumer; + } + + public T getChannel() { + return this.channel; + } + + abstract void onDestroy(); +} diff --git a/src/main/java/eu/m724/music_plugin/library/Library.java b/src/main/java/eu/m724/music_plugin/library/Library.java new file mode 100644 index 0000000..45bd2b7 --- /dev/null +++ b/src/main/java/eu/m724/music_plugin/library/Library.java @@ -0,0 +1,21 @@ +package eu.m724.music_plugin.library; + +import java.util.ArrayList; +import java.util.List; + +public class Library { + private List tracks = new ArrayList<>(); + private int playingTrack; + + public void addTrack(Track track) { + tracks.add(track); + } + + public void removeTrack(Track track) { + tracks.remove(track); + } + + public Track getPlayingTrack() { + return tracks.get(playingTrack); + } +} diff --git a/src/main/java/eu/m724/musicPlugin/item/PlayedSong.java b/src/main/java/eu/m724/music_plugin/library/Track.java similarity index 77% rename from src/main/java/eu/m724/musicPlugin/item/PlayedSong.java rename to src/main/java/eu/m724/music_plugin/library/Track.java index 07af38e..caa76a7 100644 --- a/src/main/java/eu/m724/musicPlugin/item/PlayedSong.java +++ b/src/main/java/eu/m724/music_plugin/library/Track.java @@ -1,14 +1,14 @@ -package eu.m724.musicPlugin.item; +package eu.m724.music_plugin.library; -import eu.m724.musicPlugin.file.AudioFile; +import java.io.File; -public class PlayedSong { - public final AudioFile file; +public class Track { + public final File file; private int progress = 0; private long started = -1; - public PlayedSong(AudioFile file) { + public Track(File file) { this.file = file; } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 0125e2b..7f0f5f9 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: music-plugin -version: 1 -main: eu.m724.musicPlugin.MusicPlugin -api-version: '1.21' +version: ${project.version} +main: eu.m724.music_plugin.MusicPlugin +api-version: '1.21.1' depend: [voicechat]