From cffaf80c7174d5117f3c284062203dec298cc302 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Sun, 15 Dec 2024 19:06:44 +0100 Subject: [PATCH] Intial commit 2 --- .gitignore | 113 +++++++++++++ pom.xml | 75 +++++++++ .../eu/m724/musicPlugin/MusicCommands.java | 108 +++++++++++++ .../java/eu/m724/musicPlugin/MusicPlugin.java | 52 ++++++ .../m724/musicPlugin/MyVoicechatPlugin.java | 45 ++++++ .../java/eu/m724/musicPlugin/Statics.java | 8 + .../eu/m724/musicPlugin/file/AudioFile.java | 71 +++++++++ .../musicPlugin/file/AudioFileConverter.java | 39 +++++ .../musicPlugin/file/AudioFileStorage.java | 118 ++++++++++++++ .../eu/m724/musicPlugin/file/NotEncoder.java | 41 +++++ .../musicPlugin/player/EntityMusicPlayer.java | 27 ++++ .../m724/musicPlugin/player/MusicPlayer.java | 150 ++++++++++++++++++ .../musicPlugin/player/StaticMusicPlayer.java | 28 ++++ src/main/resources/plugin.yml | 30 ++++ 14 files changed, 905 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/eu/m724/musicPlugin/MusicCommands.java create mode 100644 src/main/java/eu/m724/musicPlugin/MusicPlugin.java create mode 100644 src/main/java/eu/m724/musicPlugin/MyVoicechatPlugin.java create mode 100644 src/main/java/eu/m724/musicPlugin/Statics.java create mode 100644 src/main/java/eu/m724/musicPlugin/file/AudioFile.java create mode 100644 src/main/java/eu/m724/musicPlugin/file/AudioFileConverter.java create mode 100644 src/main/java/eu/m724/musicPlugin/file/AudioFileStorage.java create mode 100644 src/main/java/eu/m724/musicPlugin/file/NotEncoder.java create mode 100644 src/main/java/eu/m724/musicPlugin/player/EntityMusicPlayer.java create mode 100644 src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java create mode 100644 src/main/java/eu/m724/musicPlugin/player/StaticMusicPlayer.java create mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4788b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f83b9ee --- /dev/null +++ b/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + eu.m724 + music-plugin + 0.0.1-SNAPSHOT + jar + + music-plugin + + + 21 + UTF-8 + + + + clean package + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + + + + + spigotmc-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + sonatype + https://oss.sonatype.org/content/groups/public/ + + + maxhenkel + https://maven.maxhenkel.de/repository/public + + + + + + org.spigotmc + spigot-api + 1.21.1-R0.1-SNAPSHOT + provided + + + de.maxhenkel.voicechat + voicechat-api + 2.5.0 + provided + + + + org.gagravarr + vorbis-java-core + 0.8 + + + net.bramp.ffmpeg + ffmpeg + 0.8.0 + + + + diff --git a/src/main/java/eu/m724/musicPlugin/MusicCommands.java b/src/main/java/eu/m724/musicPlugin/MusicCommands.java new file mode 100644 index 0000000..ccf20eb --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/MusicCommands.java @@ -0,0 +1,108 @@ +package eu.m724.musicPlugin; + +import eu.m724.musicPlugin.file.AudioFileStorage; +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.play(song); + } 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"); + } + + return true; + } +} diff --git a/src/main/java/eu/m724/musicPlugin/MusicPlugin.java b/src/main/java/eu/m724/musicPlugin/MusicPlugin.java new file mode 100644 index 0000000..a1115eb --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/MusicPlugin.java @@ -0,0 +1,52 @@ +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 org.bukkit.plugin.java.JavaPlugin; + +public final class MusicPlugin extends JavaPlugin { + @Override + public void onEnable() { + 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); + + } + + private void onApiStarted(VoicechatServerApi api) { + getLogger().info("registerating..."); + + var category = api.volumeCategoryBuilder() + .setId("music724") + .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 new file mode 100644 index 0000000..0a254ad --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/MyVoicechatPlugin.java @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000..f948bb3 --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/Statics.java @@ -0,0 +1,8 @@ +package eu.m724.musicPlugin; + +import de.maxhenkel.voicechat.api.VoicechatServerApi; + +// TODO find a better way +public class Statics { + 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 new file mode 100644 index 0000000..95380a1 --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/file/AudioFile.java @@ -0,0 +1,71 @@ +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; + + 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 new file mode 100644 index 0000000..af13a2e --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/file/AudioFileConverter.java @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..c35a783 --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/file/AudioFileStorage.java @@ -0,0 +1,118 @@ +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 new file mode 100644 index 0000000..346f9f5 --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/file/NotEncoder.java @@ -0,0 +1,41 @@ +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 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/player/EntityMusicPlayer.java b/src/main/java/eu/m724/musicPlugin/player/EntityMusicPlayer.java new file mode 100644 index 0000000..fe1edc5 --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/player/EntityMusicPlayer.java @@ -0,0 +1,27 @@ +package eu.m724.musicPlugin.player; + +import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; +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("music724"); + channel.setDistance(32); + + return channel; + } +} diff --git a/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java b/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java new file mode 100644 index 0000000..cca9457 --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java @@ -0,0 +1,150 @@ +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; + + private AudioChannel channel; + + private boolean playing = false, paused = false; + private AudioPlayer player; + private AudioFile audioFile; + + private Consumer onAction = (r) -> {}; + + abstract AudioChannel createChannel(); + + /** + * 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 */ + + /** + * Starts playback of an audio file
+ * If it's not loaded, it will be loaded synchronously. + * + * @see MusicPlayer#pause() + * @see MusicPlayer#stop() + */ + public void play(AudioFile audioFile) { + if (audioFile != null) + this.audioFile = audioFile; + + if (playing) + stop(); + + if (!audioFile.isLoaded()) { + try { + audioFile.load(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + unpause(); + } + + public void unpause() { + 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(playing ? TrackAction.UNPAUSE : TrackAction.START); + + playing = true; + paused = false; + + } + + /** + * Stops playback and rewinds + */ + public void stop() { + stopPlayback(false); + audioFile.getEncoder().resetState(); + } + + /** + * Pauses playback + */ + public void pause() { + stopPlayback(true); + } + + + /* Internal methods */ + + + private void stopPlayback(boolean pause) { + if (player == null) return; + + paused = pause; + playing = false; + + player.stopPlaying(); + player = null; + + onStop(); + } + + private void onStop() { + if (paused) // paused + onAction.accept(TrackAction.PAUSE); + else if (playing) { // not paused and still playing + onAction.accept(TrackAction.DURATION); + playing = false; + } else // not playing + onAction.accept(TrackAction.STOP); + } + + public enum TrackAction { + /** + * A new track started playing + */ + START, + /** + * 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 + } +} diff --git a/src/main/java/eu/m724/musicPlugin/player/StaticMusicPlayer.java b/src/main/java/eu/m724/musicPlugin/player/StaticMusicPlayer.java new file mode 100644 index 0000000..1ea789d --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/player/StaticMusicPlayer.java @@ -0,0 +1,28 @@ +package eu.m724.musicPlugin.player; + +import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; +import org.bukkit.Location; + +import java.util.UUID; + +public class StaticMusicPlayer extends MusicPlayer { + private final Location location; + + public StaticMusicPlayer(Location location) { + this.location = location; + } + + @Override + AudioChannel createChannel() { + var channel = api.createLocationalAudioChannel( + UUID.randomUUID(), + api.fromServerLevel(location.getWorld()), + api.createPosition(location.getX(), location.getY(), location.getZ()) + ); + + channel.setCategory("music724"); + channel.setDistance(32); + + return channel; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..a46c688 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,30 @@ +name: music-plugin +version: 1 +main: eu.m724.musicPlugin.MusicPlugin +api-version: '1.21' + +depend: [voicechat] + +libraries: + - net.bramp.ffmpeg:ffmpeg:0.8.0 + - org.gagravarr:vorbis-java-core:0.8 + +commands: + download: + description: Downloads a file from URL. Run /convert next + usage: / + convert: + description: Converts original downloaded file to some bitrate (in bps) + usage: / + play: + description: Plays the audio file of specified bitrate. Run /convert first + usage: / + stop: + description: Stops playback + usage: / + pause: + description: Pauses playback + usage: / + resume: + description: Resumes playback + usage: / \ No newline at end of file