Skip to content
Open
132 changes: 105 additions & 27 deletions jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
import com.jme3.util.BufferUtils;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -221,25 +223,85 @@ public WorkItem(int width, int height) {
}
}

private class ResolutionWorker {
final int width;
final int height;
final LinkedBlockingQueue<WorkItem> freeItems;
final LinkedBlockingQueue<WorkItem> usedItems;
MjpegFileWriter writer;
File file;

ResolutionWorker(int width, int height, File file) {
this.width = width;
this.height = height;
this.file = file;
this.freeItems = new LinkedBlockingQueue<>();
this.usedItems = new LinkedBlockingQueue<>();
for (int i = 0; i < numCpus; i++) {
freeItems.add(new WorkItem(width, height));
}
}

boolean isFullyDrained() {
return freeItems.size() >= numCpus && usedItems.isEmpty();
}

void closeWriter() {
if (writer != null) {
try {
writer.finishAVI();
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.INFO,
"Recording saved to: {0}", file.getAbsolutePath());
} catch (Exception ex) {
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video", ex);
}
writer = null;
}
}
}

private class VideoProcessor implements SceneProcessor {

private Camera camera;
private int width;
private int height;
private RenderManager renderManager;
private boolean isInitialized = false;
private LinkedBlockingQueue<WorkItem> freeItems;
private LinkedBlockingQueue<WorkItem> usedItems = new LinkedBlockingQueue<>();
private MjpegFileWriter writer;
private ResolutionWorker currentWorker;
private Map<String, ResolutionWorker> workers = new HashMap<>();
private boolean fastMode = true;

private String getResolutionKey(int w, int h) {
return w + "x" + h;
}

private ResolutionWorker getWorker(int w, int h) {
String key = getResolutionKey(w, h);
return workers.computeIfAbsent(key, k -> {
// Generate filename for this resolution
File workerFile;
if (file == null) {
String filename = System.getProperty("user.home") + File.separator + "jMonkey-" + System.currentTimeMillis() / 1000 + ".avi";
workerFile = new File(filename);
} else {
String originalPath = file.getAbsolutePath();
int dotIndex = originalPath.lastIndexOf('.');
String basePath = dotIndex > 0 ? originalPath.substring(0, dotIndex) : originalPath;
String extension = dotIndex > 0 ? originalPath.substring(dotIndex) : ".avi";
workerFile = new File(basePath + "-" + w + "x" + h + "-" + (System.currentTimeMillis() / 1000) + extension);
}
return new ResolutionWorker(w, h, workerFile);
});
}

public void addImage(Renderer renderer, FrameBuffer out) {
if (freeItems == null) {
final ResolutionWorker worker = currentWorker;
if (worker == null) {
return;
}
try {
final WorkItem item = freeItems.take();
usedItems.add(item);
final WorkItem item = worker.freeItems.take();
worker.usedItems.add(item);
item.buffer.clear();
renderer.readFrameBufferWithFormat(out, item.buffer, Image.Format.BGRA8);
executor.submit(new Callable<Void>() {
Expand All @@ -250,14 +312,14 @@ public Void call() throws Exception {
item.data = item.buffer.array();
} else {
AndroidScreenshots.convertScreenShot(item.buffer, item.image);
item.data = writer.writeImageToBytes(item.image, quality);
item.data = worker.writer.writeImageToBytes(item.image, quality);
}
while (usedItems.peek() != item) {
while (worker.usedItems.peek() != item) {
Thread.sleep(1);
}
writer.addImage(item.data);
usedItems.poll();
freeItems.add(item);
worker.writer.addImage(item.data);
worker.usedItems.poll();
worker.freeItems.add(item);
return null;
}
});
Expand All @@ -274,16 +336,18 @@ public void initialize(RenderManager rm, ViewPort viewPort) {
this.height = camera.getHeight();
this.renderManager = rm;
this.isInitialized = true;
if (freeItems == null) {
freeItems = new LinkedBlockingQueue<WorkItem>();
for (int i = 0; i < numCpus; i++) {
freeItems.add(new WorkItem(width, height));
}
}
this.currentWorker = getWorker(width, height);
}

@Override
public void reshape(ViewPort vp, int w, int h) {
if (this.width == w && this.height == h) {
return;
}

this.width = w;
this.height = h;
this.currentWorker = getWorker(w, h);
}

@Override
Expand All @@ -293,9 +357,20 @@ public boolean isInitialized() {

@Override
public void preFrame(float tpf) {
if (null == writer) {
// Evict old workers that are fully drained
workers.entrySet().removeIf(entry -> {
ResolutionWorker worker = entry.getValue();
if (worker != currentWorker && worker.isFullyDrained()) {
worker.closeWriter();
return true;
}
return false;
});

// Ensure current worker has a writer
if (currentWorker != null && currentWorker.writer == null) {
try {
writer = new MjpegFileWriter(file, width, height, framerate);
currentWorker.writer = new MjpegFileWriter(currentWorker.file, currentWorker.width, currentWorker.height, framerate);
} catch (Exception ex) {
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error creating file writer: {0}", ex);
}
Expand All @@ -316,16 +391,19 @@ public void postFrame(FrameBuffer out) {
public void cleanup() {
logger.log(Level.INFO, "cleanup in VideoProcessor");
logger.log(Level.INFO, "VideoProcessor numFrames: {0}", numFrames);
try {
while (freeItems.size() < numCpus) {
Thread.sleep(10);
// Close all workers
for (ResolutionWorker worker : workers.values()) {
try {
while (!worker.isFullyDrained()) {
Thread.sleep(10);
}
logger.log(Level.INFO, "finishAVI in VideoProcessor for {0}x{1}", new Object[]{worker.width, worker.height});
worker.closeWriter();
} catch (Exception ex) {
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video: {0}", ex);
}
logger.log(Level.INFO, "finishAVI in VideoProcessor");
writer.finishAVI();
} catch (Exception ex) {
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video: {0}", ex);
}
writer = null;
workers.clear();
}

@Override
Expand Down
130 changes: 104 additions & 26 deletions jme3-desktop/src/main/java/com/jme3/app/state/VideoRecorderAppState.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -212,38 +214,98 @@ public WorkItem(int width, int height) {
}
}

private class ResolutionWorker {
final int width;
final int height;
final LinkedBlockingQueue<WorkItem> freeItems;
final LinkedBlockingQueue<WorkItem> usedItems;
MjpegFileWriter writer;
File file;

ResolutionWorker(int width, int height, File file) {
this.width = width;
this.height = height;
this.file = file;
this.freeItems = new LinkedBlockingQueue<>();
this.usedItems = new LinkedBlockingQueue<>();
for (int i = 0; i < numCpus; i++) {
freeItems.add(new WorkItem(width, height));
}
}

boolean isFullyDrained() {
return freeItems.size() >= numCpus && usedItems.isEmpty();
}

void closeWriter() {
if (writer != null) {
try {
writer.finishAVI();
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.INFO,
"Recording saved to: {0}", file.getAbsolutePath());
} catch (Exception ex) {
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video", ex);
}
writer = null;
}
}
}

private class VideoProcessor implements SceneProcessor {

private Camera camera;
private int width;
private int height;
private RenderManager renderManager;
private boolean isInitialized = false;
private LinkedBlockingQueue<WorkItem> freeItems;
private LinkedBlockingQueue<WorkItem> usedItems = new LinkedBlockingQueue<>();
private MjpegFileWriter writer;
private ResolutionWorker currentWorker;
private Map<String, ResolutionWorker> workers = new HashMap<>();

private String getResolutionKey(int w, int h) {
return w + "x" + h;
}

private ResolutionWorker getWorker(int w, int h) {
String key = getResolutionKey(w, h);
return workers.computeIfAbsent(key, k -> {
// Generate filename for this resolution
File workerFile;
if (file == null) {
String filename = System.getProperty("user.home") + File.separator + "jMonkey-" + System.currentTimeMillis() / 1000 + ".avi";
workerFile = new File(filename);
} else {
String originalPath = file.getAbsolutePath();
int dotIndex = originalPath.lastIndexOf('.');
String basePath = dotIndex > 0 ? originalPath.substring(0, dotIndex) : originalPath;
String extension = dotIndex > 0 ? originalPath.substring(dotIndex) : ".avi";
workerFile = new File(basePath + "-" + w + "x" + h + "-" + (System.currentTimeMillis() / 1000) + extension);
}
return new ResolutionWorker(w, h, workerFile);
});
}

public void addImage(Renderer renderer, FrameBuffer out) {
if (freeItems == null) {
final ResolutionWorker worker = currentWorker;
if (worker == null) {
return;
}
try {
final WorkItem item = freeItems.take();
usedItems.add(item);
final WorkItem item = worker.freeItems.take();
worker.usedItems.add(item);
item.buffer.clear();
renderer.readFrameBufferWithFormat(out, item.buffer, Image.Format.BGRA8);
executor.submit(new Callable<Void>() {

@Override
public Void call() throws Exception {
Screenshots.convertScreenShot(item.buffer, item.image);
item.data = writer.writeImageToBytes(item.image, quality);
while (usedItems.peek() != item) {
item.data = worker.writer.writeImageToBytes(item.image, quality);
while (worker.usedItems.peek() != item) {
Thread.sleep(1);
}
writer.addImage(item.data);
usedItems.poll();
freeItems.add(item);
worker.writer.addImage(item.data);
worker.usedItems.poll();
worker.freeItems.add(item);
return null;
}
});
Expand All @@ -259,16 +321,18 @@ public void initialize(RenderManager rm, ViewPort viewPort) {
this.height = camera.getHeight();
this.renderManager = rm;
this.isInitialized = true;
if (freeItems == null) {
freeItems = new LinkedBlockingQueue<WorkItem>();
for (int i = 0; i < numCpus; i++) {
freeItems.add(new WorkItem(width, height));
}
}
this.currentWorker = getWorker(width, height);
}

@Override
public void reshape(ViewPort vp, int w, int h) {
if (this.width == w && this.height == h) {
return;
}

this.width = w;
this.height = h;
this.currentWorker = getWorker(w, h);
}

@Override
Expand All @@ -278,9 +342,20 @@ public boolean isInitialized() {

@Override
public void preFrame(float tpf) {
if (null == writer) {
// Evict old workers that are fully drained
workers.entrySet().removeIf(entry -> {
ResolutionWorker worker = entry.getValue();
if (worker != currentWorker && worker.isFullyDrained()) {
worker.closeWriter();
return true;
}
return false;
});

// Ensure current worker has a writer
if (currentWorker != null && currentWorker.writer == null) {
try {
writer = new MjpegFileWriter(file, width, height, framerate);
currentWorker.writer = new MjpegFileWriter(currentWorker.file, currentWorker.width, currentWorker.height, framerate);
} catch (Exception ex) {
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error creating file writer: {0}", ex);
}
Expand All @@ -298,15 +373,18 @@ public void postFrame(FrameBuffer out) {

@Override
public void cleanup() {
try {
while (freeItems.size() < numCpus) {
Thread.sleep(10);
// Close all workers
for (ResolutionWorker worker : workers.values()) {
try {
while (!worker.isFullyDrained()) {
Thread.sleep(10);
}
worker.closeWriter();
} catch (Exception ex) {
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video: {0}", ex);
}
writer.finishAVI();
} catch (Exception ex) {
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video: {0}", ex);
}
writer = null;
workers.clear();
}

@Override
Expand Down
Loading