From 6759afe76bdffa8c76d0383b8af2a30c272a47fd Mon Sep 17 00:00:00 2001 From: Nekojimi Date: Sun, 14 Apr 2024 18:46:44 +0100 Subject: [PATCH] WIP: initial work on streaming instead of downloading. --- .../java/moe/nekojimi/chords/Downloader.java | 182 +++++++++++------- .../java/moe/nekojimi/chords/TrackPlayer.java | 28 +-- 2 files changed, 125 insertions(+), 85 deletions(-) diff --git a/src/main/java/moe/nekojimi/chords/Downloader.java b/src/main/java/moe/nekojimi/chords/Downloader.java index fdf79af..7b712e7 100644 --- a/src/main/java/moe/nekojimi/chords/Downloader.java +++ b/src/main/java/moe/nekojimi/chords/Downloader.java @@ -5,10 +5,8 @@ */ package moe.nekojimi.chords; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; +import com.beust.jcommander.Strings; +import java.io.*; import java.nio.charset.Charset; import java.nio.file.Files; import java.util.*; @@ -40,6 +38,7 @@ public class Downloader extends QueueThing private static final int BITRATE_TARGET = (int) AudioSendHandler.INPUT_FORMAT.getSampleRate(); private static final Pattern FORMAT_PATTERN = Pattern.compile("^([\\w]+)\\s+([\\w]+)\\s+(\\w+ ?\\w*)\\s+(.*)$"); public static final Pattern DESTINATION_PATTERN = Pattern.compile("Destination: (.*\\.wav)"); + public static final Pattern STREAM_PATTERN = Pattern.compile("Destination: -"); public static final Pattern PROGRESS_PATTERN = Pattern.compile("\\[download\\].*?([\\d\\.]+)%"); private static final Pattern INFO_JSON_PATTERN = Pattern.compile("Writing video metadata as JSON to: (.*\\.info\\.json)"); private static final Pattern ETA_PATTERN = Pattern.compile("\\[download\\].*?ETA\\s+(\\d{1,2}:\\d{2})"); @@ -99,7 +98,8 @@ public class Downloader extends QueueThing { getInfo(request); - download(promise); + boolean streamOutput = request.getTracks().size() == 1; + download(promise, streamOutput); } catch (Exception ex) { ex.printStackTrace(); @@ -157,8 +157,8 @@ public class Downloader extends QueueThing return; try { - String cmd = Chords.getSettings().getYtdlCommand() + " --skip-download -F " + track.getUrl().toString(); - Process exec = runCommand(cmd, FORMAT_TIMEOUT); +// String cmd = + " --skip-download -F " + track.getUrl().toString(); + Process exec = runCommand(List.of(Chords.getSettings().getYtdlCommand(), "--skip-download", "-F", track.getUrl().toString()), FORMAT_TIMEOUT); InputStream input = exec.getInputStream(); String output = new String(input.readAllBytes(), Charset.defaultCharset()); @@ -231,8 +231,8 @@ public class Downloader extends QueueThing List ret = new ArrayList<>(); try { - String cmd = Chords.getSettings().getYtdlCommand() + " --skip-download --print-json " + request.getUrl().toString(); - Process exec = runCommand(cmd, INFO_TIMEOUT); +// String cmd = Chords.getSettings().getYtdlCommand() + " --skip-download --print-json " + request.getUrl().toString(); + Process exec = runCommand(List.of(Chords.getSettings().getYtdlCommand(), "--skip-download", "--print-json", request.getUrl().toString()), INFO_TIMEOUT); InputStream input = exec.getInputStream(); // read each line as JSON, turn each into a track object @@ -330,7 +330,7 @@ public class Downloader extends QueueThing return request.getTracks().get(idx); } - private void download(Promise promise) throws InterruptedException, ExecutionException + private void download(Promise promise, boolean streamOutput) throws InterruptedException, ExecutionException { TrackRequest request = promise.getInput(); Set uniqueFormats = new HashSet<>(); @@ -351,69 +351,109 @@ public class Downloader extends QueueThing try { messageHandler.accept(request, null); - String cmd = Chords.getSettings().getYtdlCommand() - + " -x" - + " -f " + formatCodes + "worstaudio/bestaudio/worst/best" - + " --audio-format=wav" - + " --no-playlist" - // + " --extractor-args youtube:player_client=android" - + " -N 8" - + " -o " + getDownloadDir().getAbsolutePath() + "/%(title)s.%(ext)s " - + request.getUrl().toString(); - - Process exec = runCommand(cmd, DOWNLOAD_TIMEOUT); - InputStream in = exec.getInputStream(); - - Scanner sc = new Scanner(in); - while (sc.hasNextLine()) + List cmd = new ArrayList<>(); + cmd.add(Chords.getSettings().getYtdlCommand()); + cmd.add("-x"); + cmd.add("-f=" + formatCodes + "worstaudio/bestaudio/worst/best"); + cmd.add("--audio-format=wav"); + cmd.add("--no-playlist"); +// cmd.add(" --extractor-args youtube:player_client=android"; + cmd.add("-N8"); + if (streamOutput) { - String line = sc.nextLine(); - System.out.println(line); - - Matcher itemMatcher = DOWNLOAD_ITEM_PATTERN.matcher(line); - if (itemMatcher.find()) - { - int idx = Integer.parseInt(itemMatcher.group(1)) - 1; -// int total = Integer.parseInt(itemMatcher.group(2)); + cmd.add("--downloader=ffmpeg"); // download using FFMpeg + cmd.add("--downloader-args=ffmpeg:-f wav -c:a pcm_s16le"); // tell FFMpeg to convert to wav + cmd.add("-o"); // output to stdout + cmd.add("-"); + } else + { + cmd.add("-o " + getDownloadDir().getAbsolutePath() + "/%(title)s.%(ext)s"); + } + cmd.add(request.getUrl().toString()); - downloadIdx = idx; - } + Process exec = runCommand(cmd, streamOutput ? 0 : DOWNLOAD_TIMEOUT); - Matcher progMatcher = PROGRESS_PATTERN.matcher(line); - if (progMatcher.find()) + if (streamOutput) + { + Scanner sc = new Scanner(exec.getErrorStream()); + while (sc.hasNextLine()) { - getTrackFromRequest(request, downloadIdx).setProgress(Double.parseDouble(progMatcher.group(1))); - messageHandler.accept(request, null); + String line = sc.nextLine(); + System.out.println(line); + Matcher streamMatcher = STREAM_PATTERN.matcher(line); + if (streamMatcher.find()) + { + break; + } } - Matcher destMatcher = DESTINATION_PATTERN.matcher(line); - if (destMatcher.find()) + if (!exec.isAlive() && exec.exitValue() != 0) + throw new RuntimeException("yt-dlp failed with error code " + exec.exitValue()); + + Track track = getTrackFromRequest(request, 0); + BufferedInputStream inBuf = new BufferedInputStream(exec.getInputStream()); + inBuf.mark(128); + final byte[] headerBytes = inBuf.readNBytes(80); + String header = new String(headerBytes); + System.out.println("streaming data header: " + header); + if (!header.startsWith("RIFF")) + throw new RuntimeException("Streaming data has bad header!"); + inBuf.reset(); + track.setInputStream(inBuf); + promise.complete(track); + track.setProgress(100.0); + messageHandler.accept(request, null); + } else + { + Scanner sc = new Scanner(exec.getInputStream()); + while (sc.hasNextLine()) { - Track track = getTrackFromRequest(request, downloadIdx); + String line = sc.nextLine(); + System.out.println(line); - track.setLocation(new File(destMatcher.group(1))); + Matcher itemMatcher = DOWNLOAD_ITEM_PATTERN.matcher(line); + if (itemMatcher.find()) + { + int idx = Integer.parseInt(itemMatcher.group(1)) - 1; + downloadIdx = idx; + } - // this is currently our criteria for completion; submit the track and move on - promise.complete(track); + Matcher progMatcher = PROGRESS_PATTERN.matcher(line); + if (progMatcher.find()) + { + getTrackFromRequest(request, downloadIdx).setProgress(Double.parseDouble(progMatcher.group(1))); + messageHandler.accept(request, null); + } - track.setProgress(100.0); + Matcher destMatcher = DESTINATION_PATTERN.matcher(line); + if (destMatcher.find()) + { + Track track = getTrackFromRequest(request, downloadIdx); - messageHandler.accept(request, null); + track.setLocation(new File(destMatcher.group(1))); - downloadIdx++; + // this is currently our criteria for completion; submit the track and move on + promise.complete(track); + + track.setProgress(100.0); + + messageHandler.accept(request, null); + + downloadIdx++; + } } - } - boolean exited = exec.waitFor(DOWNLOAD_TIMEOUT, TimeUnit.SECONDS); - if (exited) - { - String error = new String(exec.getErrorStream().readAllBytes(), Charset.defaultCharset()); + boolean exited = exec.waitFor(DOWNLOAD_TIMEOUT, TimeUnit.SECONDS); + if (exited) + { + String error = new String(exec.getErrorStream().readAllBytes(), Charset.defaultCharset()); - if (exec.exitValue() != 0) - throw new RuntimeException("youtube-dl failed with error " + exec.exitValue() + ", output:\n" + error); - } else - { - throw new RuntimeException("youtube-dl failed to exit."); + if (exec.exitValue() != 0) + throw new RuntimeException("youtube-dl failed with error " + exec.exitValue() + ", output:\n" + error); + } else + { + throw new RuntimeException("youtube-dl failed to exit."); + } } messageHandler.accept(request, null); @@ -426,25 +466,21 @@ public class Downloader extends QueueThing } } - private Process runCommand(String cmd, int timeoutSecs) throws RuntimeException, IOException, InterruptedException + private Process runCommand(List cmd, int timeoutSecs) throws RuntimeException, IOException, InterruptedException { System.out.println("Running command: " + cmd); - // Process exec = Runtime.getRuntime().exec().split(" ")); - Process exec = new ProcessBuilder(cmd.split(" ")).redirectOutput(ProcessBuilder.Redirect.PIPE).redirectError(ProcessBuilder.Redirect.PIPE).start(); - scheduler.schedule(() -> + Process exec = new ProcessBuilder(cmd).redirectOutput(ProcessBuilder.Redirect.PIPE).redirectError(ProcessBuilder.Redirect.PIPE).start(); + if (timeoutSecs > 0) { - if (exec.isAlive()) + scheduler.schedule(() -> { - exec.destroyForcibly(); - System.err.println("Process " + cmd + " took too long, killing process."); - } - }, timeoutSecs, TimeUnit.SECONDS); -// boolean done = exec.waitFor(timeoutSecs, TimeUnit.SECONDS); -// if (!done) -// { -// exec.destroyForcibly(); -// throw new RuntimeException("Took too long, giving up."); -// } + if (exec.isAlive()) + { + exec.destroyForcibly(); + System.err.println("Process " + cmd + " took too long, killing process."); + } + }, timeoutSecs, TimeUnit.SECONDS); + } return exec; } diff --git a/src/main/java/moe/nekojimi/chords/TrackPlayer.java b/src/main/java/moe/nekojimi/chords/TrackPlayer.java index 2fa64c9..6081bf0 100644 --- a/src/main/java/moe/nekojimi/chords/TrackPlayer.java +++ b/src/main/java/moe/nekojimi/chords/TrackPlayer.java @@ -5,6 +5,7 @@ */ package moe.nekojimi.chords; +import java.io.BufferedInputStream; import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; @@ -39,30 +40,32 @@ public class TrackPlayer implements Closeable { AudioInputStream in = null; AudioFormat decodedFormat = null; - for (int retry = 0; retry < RETRY_COUNT; retry++) + int retry = 0; + while (in == null) { try { - in = AudioSystem.getAudioInputStream(track.getInputStream()); + in = AudioSystem.getAudioInputStream(new BufferedInputStream(track.getInputStream())); decodedFormat = AudioSendHandler.INPUT_FORMAT; break; // it worked! } catch (Exception ex) { - try + retry++; + if (retry < RETRY_COUNT) { - Thread.sleep(((long) Math.pow(2, retry)) * RETRY_DELAY); - } catch (InterruptedException ex1) - { - if (retry < RETRY_COUNT) + System.err.println("Open file " + track.getLocation() + " failed because " + ex.getMessage() + " retry " + retry + "..."); + try { - System.err.println("Open file " + track.getLocation() + " failed, retry " + retry + "..."); - continue; - } else + Thread.sleep(((long) Math.pow(2, retry)) * RETRY_DELAY); + } catch (InterruptedException ex1) { - throw ex; } + } else + { + throw ex; } + } } input = AudioSystem.getAudioInputStream(decodedFormat, in); @@ -88,7 +91,7 @@ public class TrackPlayer implements Closeable // throw new OutOfInputException(); int toRead = Math.min(length, audioBuffer.getCurrentNumberOfBytes()); -// System.out.println("To read: " + toRead + " from " + audioBuffer.getCurrentNumberOfBytes()); + System.out.println("To read: " + toRead + " from " + audioBuffer.getCurrentNumberOfBytes()); if (toRead <= 0) throw new OutOfInputException(); @@ -166,6 +169,7 @@ public class TrackPlayer implements Closeable } + public static class OutOfInputException extends RuntimeException {