Intial commit 2
This commit is contained in:
commit
cffaf80c71
14 changed files with 905 additions and 0 deletions
113
.gitignore
vendored
Normal file
113
.gitignore
vendored
Normal file
|
@ -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/
|
75
pom.xml
Normal file
75
pom.xml
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>eu.m724</groupId>
|
||||||
|
<artifactId>music-plugin</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>music-plugin</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<defaultGoal>clean package</defaultGoal>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.version}</source>
|
||||||
|
<target>${java.version}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spigotmc-repo</id>
|
||||||
|
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>sonatype</id>
|
||||||
|
<url>https://oss.sonatype.org/content/groups/public/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>maxhenkel</id>
|
||||||
|
<url>https://maven.maxhenkel.de/repository/public</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot-api</artifactId>
|
||||||
|
<version>1.21.1-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.maxhenkel.voicechat</groupId>
|
||||||
|
<artifactId>voicechat-api</artifactId>
|
||||||
|
<version>2.5.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.gagravarr/vorbis-java-core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.gagravarr</groupId>
|
||||||
|
<artifactId>vorbis-java-core</artifactId>
|
||||||
|
<version>0.8</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.bramp.ffmpeg</groupId>
|
||||||
|
<artifactId>ffmpeg</artifactId>
|
||||||
|
<version>0.8.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
108
src/main/java/eu/m724/musicPlugin/MusicCommands.java
Normal file
108
src/main/java/eu/m724/musicPlugin/MusicCommands.java
Normal file
|
@ -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<Integer, File> 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;
|
||||||
|
}
|
||||||
|
}
|
52
src/main/java/eu/m724/musicPlugin/MusicPlugin.java
Normal file
52
src/main/java/eu/m724/musicPlugin/MusicPlugin.java
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
45
src/main/java/eu/m724/musicPlugin/MyVoicechatPlugin.java
Normal file
45
src/main/java/eu/m724/musicPlugin/MyVoicechatPlugin.java
Normal file
|
@ -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<VoicechatServerApi> apiConsumer;
|
||||||
|
private final Consumer<VoicechatConnection> playerConnected;
|
||||||
|
|
||||||
|
public MyVoicechatPlugin(Consumer<VoicechatServerApi> apiConsumer, Consumer<VoicechatConnection> 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());
|
||||||
|
}
|
||||||
|
}
|
8
src/main/java/eu/m724/musicPlugin/Statics.java
Normal file
8
src/main/java/eu/m724/musicPlugin/Statics.java
Normal file
|
@ -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;
|
||||||
|
}
|
71
src/main/java/eu/m724/musicPlugin/file/AudioFile.java
Normal file
71
src/main/java/eu/m724/musicPlugin/file/AudioFile.java
Normal file
|
@ -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<byte[]>();
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
|
@ -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<Void> convert(File input, File output, int bitrate) throws IOException {
|
||||||
|
return convert(input.getAbsolutePath(), output, bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Void> convert(URL source, File output, int bitrate) throws IOException {
|
||||||
|
return convert(source.toString(), output, bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> 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));
|
||||||
|
}
|
||||||
|
}
|
118
src/main/java/eu/m724/musicPlugin/file/AudioFileStorage.java
Normal file
118
src/main/java/eu/m724/musicPlugin/file/AudioFileStorage.java
Normal file
|
@ -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<Integer, File> 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<File> 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<String> 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
41
src/main/java/eu/m724/musicPlugin/file/NotEncoder.java
Normal file
41
src/main/java/eu/m724/musicPlugin/file/NotEncoder.java
Normal file
|
@ -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; }
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
150
src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java
Normal file
150
src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java
Normal file
|
@ -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<TrackAction> 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<br>
|
||||||
|
* There can be only one for one music player
|
||||||
|
*
|
||||||
|
* @see TrackAction
|
||||||
|
*/
|
||||||
|
public void setOnAction(Consumer<TrackAction> onAction) {
|
||||||
|
this.onAction = onAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Playback control */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts playback of an audio file<br>
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
30
src/main/resources/plugin.yml
Normal file
30
src/main/resources/plugin.yml
Normal file
|
@ -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: /<command> <url>
|
||||||
|
convert:
|
||||||
|
description: Converts original downloaded file to some bitrate (in bps)
|
||||||
|
usage: /<command> <hash> <bitrate>
|
||||||
|
play:
|
||||||
|
description: Plays the audio file of specified bitrate. Run /convert first
|
||||||
|
usage: /<command> <hash> <bitrate>
|
||||||
|
stop:
|
||||||
|
description: Stops playback
|
||||||
|
usage: /<command>
|
||||||
|
pause:
|
||||||
|
description: Pauses playback
|
||||||
|
usage: /<command>
|
||||||
|
resume:
|
||||||
|
description: Resumes playback
|
||||||
|
usage: /<command>
|
Loading…
Reference in a new issue