Rework rendering
Adds default, stereoscopic, tiling, cubic and equirectangular frame rendering
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
package eu.crushedpixel.replaymod.video;
|
||||
|
||||
import eu.crushedpixel.replaymod.ReplayMod;
|
||||
import com.google.common.base.Preconditions;
|
||||
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
|
||||
import org.monte.media.*;
|
||||
import org.monte.media.FormatKeys.MediaType;
|
||||
@@ -9,132 +9,170 @@ import org.monte.media.math.Rational;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class VideoWriter {
|
||||
|
||||
private static final String DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss";
|
||||
private static final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
|
||||
private static final SimpleDateFormat FILE_FORMAT = new SimpleDateFormat(DATE_FORMAT);
|
||||
|
||||
private static final String VIDEO_EXTENSION = ".avi";
|
||||
|
||||
private static MovieWriter out;
|
||||
private static File file;
|
||||
private static boolean isRecording = false;
|
||||
private static boolean requestFinish = false;
|
||||
private static boolean abort = false;
|
||||
private final File file;
|
||||
private final MovieWriter out;
|
||||
private final Buffer buf;
|
||||
private final int track;
|
||||
|
||||
private static Buffer buf;
|
||||
private static int track;
|
||||
private static Queue<BufferedImage> toWrite = new LinkedBlockingQueue<BufferedImage>();
|
||||
private volatile boolean active = true;
|
||||
private volatile boolean cancelled = false;
|
||||
|
||||
public static boolean isRecording() {
|
||||
return isRecording;
|
||||
private int queueLimit;
|
||||
private final Queue<BufferedImage> toWrite;
|
||||
private final Thread writerThread;
|
||||
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private final Condition emptyCondition = lock.newCondition();
|
||||
private final Condition noLongerEmptyCondition = lock.newCondition();
|
||||
private final Condition noLongerFullCondition = lock.newCondition();
|
||||
|
||||
public VideoWriter(int width, int height, int fps, float quality) throws IOException {
|
||||
this(width, height, fps, quality, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static void startRecording(int width, int height) {
|
||||
if(isRecording) {
|
||||
IllegalStateException up = new IllegalStateException("VideoWriter is already recording!");
|
||||
throw up; //lolololo
|
||||
}
|
||||
isRecording = true;
|
||||
public VideoWriter(int width, int height, int fps, float quality, int queueLimit) throws IOException {
|
||||
this.queueLimit = queueLimit;
|
||||
this.toWrite = new LinkedList<BufferedImage>();
|
||||
|
||||
toWrite = new LinkedBlockingQueue<BufferedImage>();
|
||||
File folder = ReplayFileIO.getRenderFolder();
|
||||
String fileName = FILE_FORMAT.format(Calendar.getInstance().getTime());
|
||||
file = new File(folder, fileName + VIDEO_EXTENSION);
|
||||
Files.createFile(file.toPath());
|
||||
|
||||
try {
|
||||
File folder = ReplayFileIO.getRenderFolder();
|
||||
out = Registry.getInstance().getWriter(file);
|
||||
Format format = new Format(FormatKeys.MediaTypeKey, MediaType.VIDEO,
|
||||
FormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_MJPG,
|
||||
FormatKeys.FrameRateKey, new Rational(fps, 1),
|
||||
VideoFormatKeys.WidthKey, width,
|
||||
VideoFormatKeys.HeightKey, height,
|
||||
VideoFormatKeys.DepthKey, 24,
|
||||
VideoFormatKeys.QualityKey, quality);
|
||||
|
||||
String fileName = sdf.format(Calendar.getInstance().getTime());
|
||||
track = out.addTrack(format);
|
||||
|
||||
file = new File(folder, fileName + VIDEO_EXTENSION);
|
||||
file.createNewFile();
|
||||
buf = new Buffer();
|
||||
buf.format = new Format(VideoFormatKeys.DataClassKey, BufferedImage.class);
|
||||
buf.sampleDuration = out.getFormat(track).get(VideoFormatKeys.FrameRateKey).inverse();
|
||||
|
||||
out = Registry.getInstance().getWriter(file);
|
||||
Format format = new Format(FormatKeys.MediaTypeKey, MediaType.VIDEO,
|
||||
FormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_MJPG,
|
||||
FormatKeys.FrameRateKey, new Rational(ReplayMod.replaySettings.getVideoFramerate(), 1),
|
||||
VideoFormatKeys.WidthKey, width,
|
||||
VideoFormatKeys.HeightKey, height,
|
||||
VideoFormatKeys.DepthKey, 24,
|
||||
VideoFormatKeys.QualityKey, (float) ReplayMod.replaySettings.getVideoQuality());
|
||||
|
||||
|
||||
track = out.addTrack(format);
|
||||
|
||||
buf = new Buffer();
|
||||
buf.format = new Format(VideoFormatKeys.DataClassKey, BufferedImage.class);
|
||||
buf.sampleDuration = out.getFormat(track).get(VideoFormatKeys.FrameRateKey).inverse();
|
||||
} catch(IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
Thread t = new Thread(new Runnable() {
|
||||
writerThread = new Thread(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while(true) {
|
||||
if(toWrite.isEmpty() || abort) {
|
||||
if(requestFinish) {
|
||||
requestFinish = false;
|
||||
isRecording = false;
|
||||
try {
|
||||
out.close();
|
||||
if(abort) {
|
||||
file.delete();
|
||||
}
|
||||
} catch(IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
abort = false;
|
||||
toWrite = new LinkedBlockingQueue<BufferedImage>();
|
||||
return;
|
||||
}
|
||||
while(!cancelled && (active || !toWrite.isEmpty())) {
|
||||
try {
|
||||
lock.lockInterruptibly();
|
||||
BufferedImage img;
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch(Exception e) {
|
||||
img = toWrite.poll();
|
||||
if (img == null) {
|
||||
noLongerEmptyCondition.await();
|
||||
img = toWrite.poll();
|
||||
}
|
||||
noLongerFullCondition.signal();
|
||||
if (toWrite.isEmpty()) {
|
||||
emptyCondition.signalAll();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
buf.data = img;
|
||||
try {
|
||||
out.write(track, buf);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
write();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
toWrite.clear();
|
||||
out.close();
|
||||
if (cancelled) {
|
||||
Files.delete(file.toPath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
t.start();
|
||||
writerThread.start();
|
||||
}
|
||||
|
||||
public static void writeImage(BufferedImage image) {
|
||||
if(requestFinish || !isRecording) {
|
||||
IllegalStateException up = new IllegalStateException(
|
||||
"The VideoWriter is currently not available. Please try again later.");
|
||||
throw up; //lolololo^2
|
||||
}
|
||||
|
||||
toWrite.add(image);
|
||||
public void setQueueLimit(int limit) {
|
||||
this.queueLimit = limit;
|
||||
}
|
||||
|
||||
public static void endRecording() {
|
||||
if(!isRecording) return;
|
||||
requestFinish = true;
|
||||
}
|
||||
/**
|
||||
* Add the image to the writer queue.
|
||||
* @param image The image
|
||||
* @param waitIfFull Whether to wait if the queue is full or to return immediately
|
||||
* @return {@code} true if the image was added, {@code false} if it could not be added due to the queue being full
|
||||
* and {@code waitIfFull} being {@code false}
|
||||
*/
|
||||
public boolean writeImage(BufferedImage image, boolean waitIfFull) {
|
||||
Preconditions.checkState(active, "This VideoWriter has already been closed.");
|
||||
|
||||
private static void write() {
|
||||
lock.lock();
|
||||
try {
|
||||
BufferedImage img = toWrite.poll();
|
||||
if(img != null) {
|
||||
buf.data = img;
|
||||
out.write(track, buf);
|
||||
while (toWrite.size() >= queueLimit) {
|
||||
if (waitIfFull) {
|
||||
noLongerFullCondition.await();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
toWrite.offer(image);
|
||||
noLongerEmptyCondition.signal();
|
||||
return true;
|
||||
} catch (InterruptedException ignored) {
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void endRecording() {
|
||||
active = false;
|
||||
writerThread.interrupt();
|
||||
}
|
||||
|
||||
public void abortRecording() {
|
||||
cancelled = true;
|
||||
active = false;
|
||||
writerThread.interrupt();
|
||||
}
|
||||
|
||||
public void waitTillQueueEmpty() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (!toWrite.isEmpty()) {
|
||||
emptyCondition.await();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static void abortRecording() {
|
||||
requestFinish = true;
|
||||
abort = true;
|
||||
public void waitForFinish() throws InterruptedException {
|
||||
Preconditions.checkState(!active, "Video writer still active.");
|
||||
writerThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user