179 lines
6.0 KiB
Java
Executable File
179 lines
6.0 KiB
Java
Executable File
package eu.crushedpixel.replaymod.video;
|
|
|
|
import com.google.common.base.Preconditions;
|
|
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
|
|
import org.monte.media.*;
|
|
import org.monte.media.FormatKeys.MediaType;
|
|
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.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 FILE_FORMAT = new SimpleDateFormat(DATE_FORMAT);
|
|
|
|
private static final String VIDEO_EXTENSION = ".avi";
|
|
|
|
private final File file;
|
|
private final MovieWriter out;
|
|
private final Buffer buf;
|
|
private final int track;
|
|
|
|
private volatile boolean active = true;
|
|
private volatile boolean cancelled = false;
|
|
|
|
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 VideoWriter(int width, int height, int fps, float quality, int queueLimit) throws IOException {
|
|
this.queueLimit = queueLimit;
|
|
this.toWrite = new LinkedList<BufferedImage>();
|
|
|
|
File folder = ReplayFileIO.getRenderFolder();
|
|
String fileName = FILE_FORMAT.format(Calendar.getInstance().getTime());
|
|
file = new File(folder, fileName + VIDEO_EXTENSION);
|
|
Files.createFile(file.toPath());
|
|
|
|
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);
|
|
|
|
track = out.addTrack(format);
|
|
|
|
buf = new Buffer();
|
|
buf.format = new Format(VideoFormatKeys.DataClassKey, BufferedImage.class);
|
|
buf.sampleDuration = out.getFormat(track).get(VideoFormatKeys.FrameRateKey).inverse();
|
|
|
|
writerThread = new Thread(new Runnable() {
|
|
|
|
@Override
|
|
public void run() {
|
|
while(!cancelled && (active || !toWrite.isEmpty())) {
|
|
try {
|
|
lock.lockInterruptibly();
|
|
BufferedImage img;
|
|
try {
|
|
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();
|
|
}
|
|
} catch (InterruptedException ignored) {
|
|
}
|
|
}
|
|
try {
|
|
toWrite.clear();
|
|
out.close();
|
|
if (cancelled) {
|
|
Files.delete(file.toPath());
|
|
}
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}, "replaymod-video-writer");
|
|
writerThread.start();
|
|
}
|
|
|
|
public void setQueueLimit(int limit) {
|
|
this.queueLimit = limit;
|
|
}
|
|
|
|
/**
|
|
* 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.");
|
|
|
|
lock.lock();
|
|
try {
|
|
while (toWrite.size() >= queueLimit) {
|
|
if (waitIfFull) {
|
|
noLongerFullCondition.await();
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
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 void waitForFinish() throws InterruptedException {
|
|
Preconditions.checkState(!active, "Video writer still active.");
|
|
writerThread.join();
|
|
}
|
|
}
|