package defpackage; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import javax.sound.sampled.LineUnavailableException; import org.lwjgl.PointerBuffer; import org.lwjgl.system.MemoryStack; import org.lwjgl.util.tinyfd.TinyFileDialogs; public final class ajn implements aji, ajk { private static final ajn INSTANCE = new ajn(); private static final String CLIPS_DIRECTORY = "clips"; private static final String IO_THREAD = "EvoClipIO"; private static final String EXPORT_THREAD = "EvoClipExport"; private static final String NOTIFY_TITLE = "Clips"; private static final String STATUS_SAVED = "saved"; private static final String STATUS_EMPTY = "buffer is empty"; private static final String STATUS_RECORDING = "recording"; private static final String STATUS_STOPPED = "recording saved"; private static final String STATUS_EXPORTED = "exported"; private static final String STATUS_FAILED = "failed"; private static final String STATUS_BUSY = "already working"; private static final String STATUS_NO_AUDIO = "audio device unavailable"; private static final String STATUS_NO_LOOPBACK = "game audio needs a loopback device"; private static final String STATUS_CANCELLED = "export cancelled"; private static final String DIALOG_TITLE = "Save clip"; private static final String FILTER_PATTERN = "*.mp4"; private static final String FILTER_LABEL = "MP4 video"; private static final String SAVE_FAILURE = "clip save failed"; private static final String RECORD_FAILURE = "clip recording failed"; private static final String EXPORT_FAILURE = "clip export failed"; private static final String AUDIO_FAILURE = "clip audio capture unavailable for channel {}"; private static final String LABEL_RECORDING = "REC "; private static final String LABEL_SAVING = "Saving clip"; private static final String LABEL_EXPORTING = "Exporting "; private static final String TIME_SEPARATOR = ":"; private static final String TIME_PAD = "0"; private static final String PERCENT = "%"; private static final int PIXEL_POOL_CAPACITY = 8; private static final int MIN_WORKERS = 1; private static final int MAX_WORKERS = 4; private static final int CORES_PER_WORKER = 2; private static final int FRAMES_PER_WORKER = 30; private static final int MAX_CATCHUP_INTERVALS = 4; private static final long FLUSH_TIMEOUT_MILLIS = 3000L; private static final long NANOS_PER_SECOND = 1000000000L; private static final long MILLIS_PER_SECOND = 1000L; private static final long SECONDS_PER_MINUTE = 60L; private static final int PAD_LIMIT = 10; private static final int PERCENT_SCALE = 100; private static final int MIN_EXPORT_CHUNK = 15; private static final float PROGRESS_COMPLETE = 1.0F; private final akd library = new akd(new File(bz.DIRECTORY, "clips").toPath()); private final ajf pixels = new ajf((-530988065 | 530988073) + (-530988065 & 530988073)); private final akg ring = new akg( (4068614446794629938L | -4068614446794629938L) + (4068614446794629938L & -4068614446794629938L), (-7238148903045534534L ^ 7238148903045534534L) + ((-7238148903045534534L & 7238148903045534534L) << 1) ); private final aju audioRing = new aju((-3540198577801529315L ^ 3540198577801529315L) + ((-3540198577801529315L & 3540198577801529315L) << 1)); private final ExecutorService io = Executors.newSingleThreadExecutor( factory("EvoClipIO") ); private final ExecutorService exports = Executors.newSingleThreadExecutor( factory("EvoClipExport") ); private final Object writerLock = new Object(); private final Object poolLock = new Object(); private volatile ajj encoders; private ajz writer; private volatile ajq systemAudio; private volatile ajq microphoneAudio; private volatile boolean systemAudioFailed; private volatile boolean microphoneFailed; private long lastCaptureNanos; private int lastLevelId; private volatile int frameWidth; private volatile int frameHeight; private volatile boolean recording; private volatile boolean forcedExport; private volatile long recordingStartedMillis; private volatile boolean saving; private volatile boolean exporting; volatile float exportProgress; private ajn() { } public static ajn getInstance() { return INSTANCE; } public akd library() { return this.library; } public int bufferedFrames() { return this.ring.frameCount(); } public long bufferedMillis() { return this.ring.spanMillis(); } public long bufferedBytes() { return this.ring.usedBytes(); } public boolean isRecording() { return this.recording; } public Path activeClipPath() { synchronized (this.writerLock) { return this.writer == null ? null : this.writer.path(); } } public boolean isExporting() { return this.exporting; } public float exportProgress() { return this.exportProgress; } public ajp status() { ajp var1 = null; int var2 = 1695866167; while (true) { switch (var2) { case -2116776308: if (this.recording) { var2 = -1047961509; } else { var2 = -1204917921; } break; case -1394340563: return var1; case -1204917921: if (this.saving) { var2 = 1668494564; } else { var2 = 1605639620; } break; case -1047961509: return new ajp(ajo.RECORDING, "REC " + elapsed(System.currentTimeMillis() - this.recordingStartedMillis), 1.0F); case 541660842: return ajp.NONE; case 1605639620: if (this.exporting) { var2 = 1913038014; } else { var2 = 541660842; } break; case 1668494564: return new ajp(ajo.SAVING, "Saving clip", 1.0F); case 1695866167: default: var1 = aje.getInstance().status(); if (var1.active()) { var2 = -1394340563; } else { var2 = -2116776308; } break; case 1913038014: return new ajp(ajo.EXPORTING, "Exporting " + Math.round(this.exportProgress * 100.0F) + "%", this.exportProgress); } } } public void captureFrame() { ti var1 = ti.INSTANCE; boolean var2 = aje.getInstance().advance(); if ((var1.isEnabled() || this.recording) && aqg.mc.level != null && aqg.mc.player != null) { if (var2 || var1.instantReplay.isEnabled() || this.recording) { int var3 = System.identityHashCode(aqg.mc.level); if (var3 != this.lastLevelId) { this.lastLevelId = var3; this.ring.clear(); } long var4 = System.nanoTime(); long var6 = ((204804686408849003L ^ -204804685408849003L) + ((204804686408849003L & -204804685408849003L) << 1)) / var1.captureFps(); if (var4 - this.lastCaptureNanos >= var6) { this.lastCaptureNanos = var4 - this.lastCaptureNanos > var6 * ((6528836991116781498L ^ -6528836991116781494L) + ((6528836991116781498L & -6528836991116781494L) << 1)) ? var4 : this.lastCaptureNanos + var6; this.ring.resize(var1.bufferMillis(), var1.bufferBytes()); this.audioRing.resize(var1.bufferMillis()); this.startEncoders(); this.syncAudioCapture(var1); ajl.capture(var1.captureHeight(), this); } } } } @Override public byte[] borrow(int var1) { return this.pixels.take(var1); } @Override public boolean accept(byte[] var1, int var2, int var3, long var4) { ajj var6 = null; int var8 = 0; int var7 = -1692979841; while (true) { switch (var7) { case -2101731117: var8 = 0; var7 = 113816286; break; case -1692979841: default: var6 = this.encoders; if (var6 != null) { var7 = -1472899494; } else { var7 = -2101731117; } break; case -1472899494: if (var6.submit(var1, var2, var3, var4, ti.INSTANCE.jpegQuality())) { var7 = 1714758591; } else { var7 = -2101731117; } break; case 113816286: return (boolean)((var8) != 0); case 1714758591: var8 = 1; var7 = 113816286; } } } @Override public void discard(byte[] var1) { this.pixels.recycle(var1); } @Override public void onFrame(int var1, int var2, akb var3) { if (var1 != this.frameWidth || var2 != this.frameHeight) { this.frameWidth = var1; this.frameHeight = var2; this.ring.clear(); if (this.recording) { this.stopRecording(); } } aje var4 = aje.getInstance(); var4.onFrame(var1, var2, var3); if (ti.INSTANCE.instantReplay.isEnabled() && !var4.isActive()) { this.ring.add(var3); } synchronized (this.writerLock) { if (this.writer != null) { try { this.writer.append(var3); } catch (IOException var8) { id.error(Cif.MEDIA, "clip recording failed", var8); this.closeWriterLocked(); this.recording = (boolean)(((0)) != 0); } } } } private void onAudio(ajr var1) { if (ti.INSTANCE.instantReplay.isEnabled()) { this.audioRing.add(var1); } synchronized (this.writerLock) { if (this.writer != null) { try { this.writer.appendAudio(var1); } catch (IOException var5) { id.error(Cif.MEDIA, "clip recording failed", var5); } } } } private void syncAudioCapture(ti var1) { int var2 = var1.gameAudio.isEnabled() && !this.systemAudioFailed ? 1: 0; int var3 = var1.microphone.isEnabled() && !this.microphoneFailed ? 1: 0; if (var2 != ( this.systemAudio != null ? 1: (1992414113 ^ -1992414113) + ((1992414113 & -1992414113) << 1) ) || var3 != ( this.microphoneAudio != null ? 1: (-1007646925 ^ 1007646925) + ((-1007646925 & 1007646925) << 1) )) { String var4 = var1.gameAudioDevice(); String var5 = var1.microphoneDevice(); int var6 = var4 != null && var4.equals(var5) ? 1: 0; this.io .execute( () -> { if (var2 == 0 && this.systemAudio != null) { this.systemAudio.close(); this.systemAudio = null; } if (var3 == 0 && this.microphoneAudio != null) { this.microphoneAudio.close(); this.microphoneAudio = null; } if (var3 != 0 && this.microphoneAudio == null) { this.microphoneAudio = this.openAudio(1, var5); this.microphoneFailed = (boolean)(((this.microphoneAudio == null ? 1: (1683764292 ^ -1683764292) + ((1683764292 & -1683764292) << 1))) != 0); } if (var2 != 0 && this.systemAudio == null) { if (var4 == null) { this.systemAudioFailed = (boolean)(((1)) != 0); notifyStatus( "game audio needs a loopback device" ); } else if (var6 != 0 && this.microphoneAudio != null) { this.systemAudioFailed = (boolean)(((1)) != 0); notifyStatus("game audio needs a loopback device"); } else { this.systemAudio = this.openAudio(0, var4); this.systemAudioFailed = (boolean)(((this.systemAudio == null ? 1: (31052517 ^ -31052517) + ((31052517 & -31052517) << 1))) != 0); } } } ); } } private ajq openAudio(int var1, String var2) { if (var2 == null) { notifyStatus("audio device unavailable"); return null; } try { ajq var3 = new ajq(var1, var2, this::onAudio); var3.start(System.currentTimeMillis()); return var3; } catch (LineUnavailableException | RuntimeException var4) { id.error( Cif.MEDIA, "clip audio capture unavailable for channel {}", var1, var4 ); notifyStatus("audio device unavailable"); return null; } } private void closeAudio() { int var1 = 46666817; while (true) { switch (var1) { case -1012708051: this.microphoneAudio.close(); this.microphoneAudio = null; var1 = 54512945; break; case -72612598: this.systemAudio.close(); this.systemAudio = null; var1 = 535564911; break; case 46666817: default: if (this.systemAudio != null) { var1 = -72612598; } else { var1 = 535564911; } break; case 54512945: this.systemAudioFailed = (boolean)(((0)) != 0); this.microphoneFailed = (boolean)(((0)) != 0); this.audioRing.clear(); return; case 535564911: if (this.microphoneAudio != null) { var1 = -1012708051; } else { var1 = 54512945; } } } } public void saveInstantReplay() { List var1 = null; List var2 = null; int var3 = 0; int var4 = 0; int var5 = 0; int var6 = 985883918; while (true) { switch (var6) { case -857498035: var2 = this.audioRing.snapshot(((akb)var1.getFirst()).timestampMillis(), ((akb)var1.getLast()).timestampMillis()); var3 = this.frameWidth; var4 = this.frameHeight; var5 = ti.INSTANCE.captureFps(); this.saving = (boolean)(((1)) != 0); this.io.execute(() -> this.writeClip(var1, var2, var3, var4, var5)); return; case -411039100: var1 = this.ring.snapshot(); if (var1.size() < (1727226768 ^ -1727226766) + ((1727226768 & -1727226766) << 1)) { var6 = -242760839; } else { var6 = -857498035; } break; case -242760839: notifyStatus("buffer is empty"); return; case 985883918: default: if (this.saving) { var6 = 1626806322; } else { var6 = -411039100; } break; case 1626806322: notifyStatus("already working"); return; } } } public void toggleRecording() { int var1 = 1831475424; while (true) { switch (var1) { case -1927287706: this.startRecording(); var1 = 487530995; break; case -699861389: this.stopRecording(); var1 = 487530995; break; case 487530995: return; case 1831475424: default: if (this.recording) { var1 = -699861389; } else { var1 = -1927287706; } } } } public void startRecording() { int var1 = 0; int var2 = 0; int var3 = 0; int var4 = -702390882; while (true) { switch (var4) { case -1786782579: notifyStatus("already working"); return; case -1113527304: var1 = this.frameWidth; var2 = this.frameHeight; var3 = ti.INSTANCE.captureFps(); this.recording = (boolean)(((1)) != 0); this.recordingStartedMillis = System.currentTimeMillis(); this.io.execute(() -> this.openWriter(var1, var2, var3)); notifyStatus("recording"); return; case -702390882: default: if (!this.recording) { var4 = 1164277836; } else { var4 = -1786782579; } break; case 1164277836: if (this.saving) { var4 = -1786782579; } else { var4 = -1113527304; } } } } public void stopRecording() { this.stopRecording((boolean)((((-899973690 | 899973690) + (-899973690 & 899973690))) != 0)); } public void stopRecording(boolean var1) { int var2 = -2042369646; while (true) { switch (var2) { case -2042369646: default: if (!this.recording) { var2 = 1576689565; } else { var2 = -847555136; } break; case -847555136: this.recording = (boolean)(((0)) != 0); this.forcedExport = var1; this.io.execute(this::finishRecording); return; case 1576689565: return; } } } public void export(ake var1, int var2, int var3) { int var4 = -879081490; while (true) { switch (var4) { case -2010159792: this.exporting = (boolean)(((1)) != 0); this.exportProgress = 0.0F; this.exports.execute(() -> this.runExport(var1, var2, var3, this.library.allocateExport(var1.name()))); return; case -879081490: default: if (this.exporting) { var4 = 357208169; } else { var4 = -2010159792; } break; case 357208169: notifyStatus("already working"); return; } } } public void exportAs(ake var1, int var2, int var3) { int var4 = 1766889993; while (true) { switch (var4) { case -46915276: this.exporting = (boolean)(((1)) != 0); this.exportProgress = 0.0F; this.exports.execute(() -> { Path var4x = null; int var5 = -1440358899; while (true) { switch (var5) { case -1756571626: this.runExport(var1, var2, var3, var4x); return; case -1440358899: default: var4x = chooseTarget(this.library.allocateExport(var1.name())); if (var4x == null) { var5 = 657811488; } else { var5 = -1756571626; } break; case 657811488: this.exporting = (boolean)(((0)) != 0); notifyStatus("export cancelled"); return; } } }); return; case 1702042761: notifyStatus("already working"); return; case 1766889993: } if (this.exporting) { var4 = 1702042761; } else { var4 = -46915276; } } } private static Path chooseTarget(Path var0) { try { MemoryStack var1 = MemoryStack.stackPush(); Path var4; try { PointerBuffer var2 = var1.mallocPointer((402000002 ^ -402000001) + ((402000002 & -402000001) << 1)); var2.put(var1.UTF8("*.mp4")); var2.flip(); String var3 = TinyFileDialogs.tinyfd_saveFileDialog( "Save clip", var0.toString(), var2, "MP4 video" ); var4 = var3 != null && !var3.isBlank() ? Path.of(var3) : null; } catch (Throwable var6) { if (var1 != null) { try { var1.close(); } catch (Throwable var5) { var6.addSuppressed(var5); } } throw var6; } if (var1 != null) { var1.close(); } return var4; } catch (Throwable var7) { id.error(Cif.MEDIA, "clip export failed", var7); return var0; } } public void shutdown() { this.stopRecording(); this.io.execute(this::stopEncoders); aqg.mc.execute(ajl::release); } private void stopEncoders() { synchronized (this.poolLock) { ajj var2 = this.encoders; if (var2 != null) { var2.flush((1837848887167015651L ^ -1837848887167012651L) + ((1837848887167015651L & -1837848887167012651L) << 1)); var2.stop(); this.encoders = null; } } this.ring.clear(); this.pixels.clear(); this.closeAudio(); } private void startEncoders() { int var1 = desiredWorkers(); ajj var2 = this.encoders; if (var2 == null || !var2.isRunning() || var2.workerCount() != var1) { synchronized (this.poolLock) { if (this.encoders != null && this.encoders.workerCount() != var1) { this.encoders.stop(); this.encoders = null; } if (this.encoders == null) { this.encoders = new ajj(this, this.pixels, var1); } if (!this.encoders.isRunning()) { this.encoders.start(); } } } } private static int desiredWorkers() { int var0 = Math.round(ti.INSTANCE.captureFps() / 30.0F); int var1 = Runtime.getRuntime().availableProcessors() / (2); return Math.clamp( Math.min(var0, var1), 1, (103338153 ^ -103338149) + ((103338153 & -103338149) << 1) ); } private void writeClip(List var1, List var2, int var3, int var4, int var5) { Path var6 = this.library.allocateClip(((akb)var1.getFirst()).timestampMillis()); try (ajz var7 = new ajz(var6, var3, var4, var5, ((akb)var1.getFirst()).timestampMillis())) { var7.appendAll(var1); var7.appendAllAudio(var2); } catch (IOException var12) { this.saving = (boolean)(((0)) != 0); id.error(Cif.MEDIA, "clip save failed", var12); notifyStatus("failed"); return; } this.saving = (boolean)(((0)) != 0); this.library.prune(ti.INSTANCE.storageBytes()); this.announceClip(var6, "saved"); } private void openWriter(int var1, int var2, int var3) { long var4 = System.currentTimeMillis(); Path var6 = this.library.allocateClip(var4); try { ajz var7 = new ajz(var6, var1, var2, var3, var4); synchronized (this.writerLock) { this.writer = var7; } } catch (IOException var11) { this.recording = (boolean)(((0)) != 0); id.error(Cif.MEDIA, "clip recording failed", var11); notifyStatus("failed"); } } private void finishRecording() { ajj var1 = this.encoders; if (var1 != null) { var1.flushTo(var1.submittedSequence(), (-3082258856859192361L ^ 3082258856859195361L) + ((-3082258856859192361L & 3082258856859195361L) << 1)); } Path var2; synchronized (this.writerLock) { if (this.writer == null) { return; } var2 = this.writer.path(); this.closeWriterLocked(); } this.library.prune(ti.INSTANCE.storageBytes()); this.announceClip(var2, "recording saved"); } private void closeWriterLocked() { try { this.writer.close(); } catch (IOException var5) { id.error(Cif.MEDIA, "clip recording failed", var5); } finally { this.writer = null; } } private void announceClip(Path var1, String var2) { ake var3; try { var3 = aka.readMeta(var1); } catch (IOException var6) { id.error(Cif.MEDIA, "clip save failed", var6); notifyStatus("failed"); return; } notifyStatus(var2 + " " + elapsed(var3.durationMillis())); playSavedSound(); ti var4 = ti.INSTANCE; boolean var5 = this.forcedExport; this.forcedExport = (boolean)(((0)) != 0); if ((var4.autoExport.isEnabled() || var5) && var3.frameCount() > (2047021825 ^ -2047021824) + ((2047021825 & -2047021824) << 1)) { this.export( var3, 0, var3.frameCount() - ((1153039940 ^ -1153039939) + ((1153039940 & -1153039939) << 1)) ); } if (var4.openEditor.isEnabled()) { aqg.mc.execute(() -> aby.open(var3)); } } // $VF: Could not verify finally blocks. A semaphore variable has been added to preserve control flow. // Please report this to the Vineflower issue tracker, at https://github.com/Vineflower/vineflower/issues with a copy of the class file (if you have the rights to distribute it!) private void runExport(ake var1, int var2, int var3, Path var4) { int var5 = 0; boolean var15 = false /* VF: Semaphore variable */; label65: { label64: { try { var15 = true; try (ajy var6 = new ajy(var1.path())) { ti var7 = ti.INSTANCE; int var8 = Math.max(15, var7.exportFps()); ajw var9 = new ajw(var7.exportQuantizer(), var7.exportThreads(), var8, var7.exportFps()); var5 = (ajx.export( var6, Math.max(0, var2), Math.min(var3, var6.frameCount() - ((-1066600019 ^ 1066600020) + ((-1066600019 & 1066600020) << 1))), var4, var9, this.listener() )) ? 1 : 0; } var15 = false; break label64; } catch (IOException var18) { id.error( Cif.MEDIA, "clip export failed", var18 ); var15 = false; } finally { if (var15) { this.exporting = (boolean)(((0)) != 0); this.exportProgress = 0.0F; } } this.exporting = (boolean)(((0)) != 0); this.exportProgress = 0.0F; break label65; } this.exporting = (boolean)(((0)) != 0); this.exportProgress = 0.0F; } notifyStatus( var5 != 0 ? "exported" : "failed" ); } private ajv listener() { return new ajv() { @Override public void onProgress(float var1) { ajn.this.exportProgress = var1; } @Override public boolean cancelled() { return (boolean)(((0)) != 0); } }; } private static void playSavedSound() { aqg.mc.execute(() -> { mm var0 = null; int var1 = 541190834; while (true) { switch (var1) { case 541190834: default: var0 = bz.getInstance().getSoundManager(); if (var0 != null) { var1 = 911639411; } else { var1 = 1669065045; } break; case 911639411: var0.play(mj.CLIP_SAVED); var1 = 1669065045; break; case 1669065045: return; } } }); } static void notifyStatus(String var0) { aqg.mc .execute( () -> le.getInstance() .addCustomNotification(rl.MISC.getIcon(), "Clips", var0) ); } public static String elapsed(long var0) { long var2 = Math.max((-4278841967973501350L | 4278841967973501350L) + (-4278841967973501350L & 4278841967973501350L), var0) / ((6702982416488184899L ^ -6702982416488183899L) + ((6702982416488184899L & -6702982416488183899L) << 1)); long var4 = var2 / ((-7061273179146511698L ^ 7061273179146511758L) + ((-7061273179146511698L & 7061273179146511758L) << 1)); long var6 = var2 % ((8277252237356953933L ^ -8277252237356953873L) + ((8277252237356953933L & -8277252237356953873L) << 1)); return var4 + ":" + (var6 < (1037647348913914466L | -1037647348913914456L) + (1037647348913914466L & -1037647348913914456L) ? "0" + var6 : Long.toString(var6)); } private static ThreadFactory factory(String var0) { return var1 -> { Thread var2 = new Thread(var1, var0); var2.setDaemon((boolean)((((-1506489619 | 1506489620) + (-1506489619 & 1506489620))) != 0)); return var2; }; } }