-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added option to save video on death #270
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
package com.github.manolo8.darkbot.gui.utils; | ||
|
||
import com.github.manolo8.darkbot.utils.LogUtils; | ||
import eu.darkbot.util.Timer; | ||
import net.jpountz.lz4.LZ4Compressor; | ||
import net.jpountz.lz4.LZ4Exception; | ||
import net.jpountz.lz4.LZ4Factory; | ||
import net.jpountz.lz4.LZ4FastDecompressor; | ||
import org.jcodec.api.SequenceEncoder; | ||
import org.jcodec.common.model.ColorSpace; | ||
import org.jcodec.common.model.Picture; | ||
|
||
import javax.swing.JFrame; | ||
import java.awt.Graphics2D; | ||
import java.awt.image.BufferedImage; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.time.LocalDateTime; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
public class DeathRecorder { | ||
public static final int FPS = 4; | ||
|
||
private static final int WIDTH = 640; | ||
private static final int HEIGHT = 480; | ||
private static final int PICTURE_LENGTH = WIDTH * HEIGHT * 3; | ||
|
||
private static final int MAX_FRAMES = 100; | ||
private static final int MAX_COMPRESSION_LENGTH = PICTURE_LENGTH / 8; //115_200 | ||
|
||
private final byte[] bitmapBuffer = new byte[PICTURE_LENGTH]; | ||
private final byte[] compressionBuffer = new byte[MAX_COMPRESSION_LENGTH]; | ||
|
||
private final List<CompressedFrame> compressedFrames = new ArrayList<>(MAX_FRAMES); | ||
private final BufferedImage imageCache = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); | ||
|
||
private final Timer frameTimer = Timer.get(1000 / FPS); | ||
|
||
private final JFrame mainGui; | ||
private final LZ4Compressor compressor; | ||
private final LZ4FastDecompressor decompressor; | ||
|
||
private int currentFrame, validFrames; | ||
private boolean saving; | ||
|
||
public DeathRecorder(JFrame mainGui) { | ||
this.mainGui = mainGui; | ||
|
||
LZ4Factory factory = LZ4Factory.fastestInstance(); | ||
this.compressor = factory.fastCompressor(); | ||
this.decompressor = factory.fastDecompressor(); | ||
} | ||
|
||
public void onTick() { | ||
if (frameTimer.tryActivate()) { | ||
saveFrame(); | ||
} | ||
} | ||
|
||
public void onDeath() { | ||
synchronized (this) { | ||
if (saving) return; | ||
saving = true; | ||
} | ||
|
||
new Thread(() -> { | ||
try { | ||
saveVideo(); | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
} | ||
|
||
validFrames = currentFrame = 0; | ||
saving = false; | ||
}).start(); | ||
} | ||
|
||
private synchronized void saveFrame() { | ||
if (saving) return; | ||
Graphics2D g2 = (Graphics2D) imageCache.getGraphics(); | ||
|
||
// cut native border from FlatLaf - only on Windows? | ||
double frameWidth = mainGui.getWidth() - 16; | ||
double frameHeight = mainGui.getHeight() - 8; | ||
|
||
g2.scale(WIDTH / frameWidth, HEIGHT / frameHeight); | ||
g2.translate(-8, 0); | ||
mainGui.print(g2); | ||
//g2.dispose(); | ||
|
||
for (int offset = 0, h = 0; h < HEIGHT; h++) { | ||
for (int w = 0; w < WIDTH; w++) { | ||
int v = imageCache.getRGB(w, h); | ||
bitmapBuffer[offset++] = (byte) (((v >>> 16) & 0xff) - 128); | ||
bitmapBuffer[offset++] = (byte) (((v >>> 8) & 0xff) - 128); | ||
bitmapBuffer[offset++] = (byte) (((v) & 0xff) - 128); | ||
} | ||
} | ||
Comment on lines
+92
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are we totally certain there's no other way to extract a graphics2d's rendered output to a byte array if you don't do it yourself? From what i could find, you can create a BufferedImage, then call @Override
public void paint(Graphics g) {
BufferedImage img = new BufferedImage(width(), height(), BufferedImage.TYPE_INT_RGB);
Graphics2D imgGraphics = img.getGraphics();
// All the logic to draw, do it based on img.
// Optionally only do this whole render-to-image if you want the frame saved,
// otherwise rendering to g directly.
doPaintStuff(imgGraphics);
// Normally use the image as if it was the proper render
g.drawImage(img, 0, 0, width, height);
img.getRaster().getDataBuffer(); // here's your pixel data to save
} If course you'll have to benchmark this, but i'm pretty sure this is going to be faster than manually iterating and calling a method to get rgb manually for each pixel in the image, especially if using any higher resolution. |
||
|
||
CompressedFrame compressedImage; | ||
if (compressedFrames.size() <= currentFrame) { | ||
compressedFrames.add(compressedImage = new CompressedFrame()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of add & remove, consider simply using a circular buffer (or ring buffer), much more convenient. Essentially you'll be writing to a different index each time and once you get to the end it starts re-writing the beginning, once you want to finally save it as video you can just iterate from head + 1 up till you loop arround to head (head being the current "index" you're writing to. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is actually kinda a circular buffer, no |
||
} else { | ||
compressedImage = compressedFrames.get(currentFrame); | ||
} | ||
compressedImage.compress(); | ||
|
||
if (++currentFrame >= MAX_FRAMES) | ||
currentFrame = 0; | ||
|
||
if (validFrames < MAX_FRAMES) | ||
validFrames++; | ||
} | ||
|
||
private void saveVideo() throws IOException { | ||
if (validFrames > 0) { | ||
long time = System.currentTimeMillis(); | ||
|
||
File outputFile = new File("logs/" + LocalDateTime.now().format(LogUtils.FILENAME_DATE) + ".mov"); | ||
SequenceEncoder sequenceEncoder = SequenceEncoder.createSequenceEncoder(outputFile, FPS); | ||
|
||
Picture picture = Picture.create(WIDTH, HEIGHT, ColorSpace.RGB); | ||
for (int i = 0; i < validFrames; i++) { | ||
int frame = (currentFrame + i) % validFrames; | ||
|
||
CompressedFrame compressedFrame = compressedFrames.get(frame); | ||
compressedFrame.decompressToPicture(picture); | ||
|
||
sequenceEncoder.encodeNativeFrame(picture); | ||
} | ||
sequenceEncoder.finish(); | ||
System.out.println("Saved video in: " + (System.currentTimeMillis() - time) + "ms | " + validFrames); | ||
} | ||
} | ||
|
||
private class CompressedFrame { | ||
private byte[] compressed; | ||
private int size; | ||
|
||
private void compress() { | ||
try { | ||
size = compressor.compress(bitmapBuffer, compressionBuffer); | ||
} catch (LZ4Exception e) { | ||
size = 0; | ||
return; | ||
} | ||
|
||
if (compressed == null || compressed.length < size) { | ||
compressed = new byte[(int) Math.min(MAX_COMPRESSION_LENGTH, size * 1.1)]; | ||
} | ||
|
||
System.arraycopy(compressionBuffer, 0, compressed, 0, size); | ||
} | ||
|
||
private void decompressToPicture(Picture picture) { | ||
if (size == 0) return; // keep old data? | ||
|
||
byte[] data = picture.getPlaneData(0); | ||
decompressor.decompress(compressed, data, PICTURE_LENGTH); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you're manually trying to get rid of the border, does that mean you just want the map being rendered? can't you dump the graphics being written for that component instead of the whole frame?
I think it'd probably be useful, especially given we fully render that ourselves to a graphics 2d, and that we probably can straight up copy it to a buffer with some method in there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to
record
whole bot GUIRendering image this way, shows a weird white border around gui, probably it is casused by flatlaf's native border on Windows*
So 16 pixels less are there only to remove white border