Skip to content

Commit 53e97fd

Browse files
Copilotriccardobl
andcommitted
Refactor to use per-resolution workers with automatic eviction
Use separate worker queues for each resolution as suggested by @riccardobl. This allows: - Concurrent processing of frames from different resolutions - No frame skipping during resize - Automatic cleanup when old resolutions are fully drained - Each resolution gets its own file with dimensions in filename Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com>
1 parent 1e0bb05 commit 53e97fd

File tree

2 files changed

+203
-133
lines changed

2 files changed

+203
-133
lines changed

jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java

Lines changed: 102 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@
4848
import com.jme3.util.BufferUtils;
4949
import java.io.File;
5050
import java.nio.ByteBuffer;
51+
import java.util.HashMap;
5152
import java.util.List;
53+
import java.util.Map;
5254
import java.util.concurrent.*;
5355
import java.util.logging.Level;
5456
import java.util.logging.Logger;
@@ -221,28 +223,88 @@ public WorkItem(int width, int height) {
221223
}
222224
}
223225

226+
private class ResolutionWorker {
227+
final int width;
228+
final int height;
229+
final LinkedBlockingQueue<WorkItem> freeItems;
230+
final LinkedBlockingQueue<WorkItem> usedItems;
231+
MjpegFileWriter writer;
232+
File file;
233+
234+
ResolutionWorker(int width, int height, File file) {
235+
this.width = width;
236+
this.height = height;
237+
this.file = file;
238+
this.freeItems = new LinkedBlockingQueue<>();
239+
this.usedItems = new LinkedBlockingQueue<>();
240+
for (int i = 0; i < numCpus; i++) {
241+
freeItems.add(new WorkItem(width, height));
242+
}
243+
}
244+
245+
boolean isFullyDrained() {
246+
return freeItems.size() >= numCpus && usedItems.isEmpty();
247+
}
248+
249+
void closeWriter() {
250+
if (writer != null) {
251+
try {
252+
writer.finishAVI();
253+
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.INFO,
254+
"Recording saved to: {0}", file.getAbsolutePath());
255+
} catch (Exception ex) {
256+
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video", ex);
257+
}
258+
writer = null;
259+
}
260+
}
261+
}
262+
224263
private class VideoProcessor implements SceneProcessor {
225264

226265
private Camera camera;
227266
private int width;
228267
private int height;
229268
private RenderManager renderManager;
230269
private boolean isInitialized = false;
231-
private LinkedBlockingQueue<WorkItem> freeItems;
232-
private LinkedBlockingQueue<WorkItem> usedItems = new LinkedBlockingQueue<>();
233-
private MjpegFileWriter writer;
270+
private ResolutionWorker currentWorker;
271+
private Map<String, ResolutionWorker> workers = new HashMap<>();
234272
private boolean fastMode = true;
235-
private boolean reshapePending = false;
236-
private int newWidth;
237-
private int newHeight;
273+
274+
private String getResolutionKey(int w, int h) {
275+
return w + "x" + h;
276+
}
277+
278+
private ResolutionWorker getWorker(int w, int h) {
279+
String key = getResolutionKey(w, h);
280+
ResolutionWorker worker = workers.get(key);
281+
if (worker == null) {
282+
// Generate filename for this resolution
283+
File workerFile;
284+
if (file == null) {
285+
String filename = System.getProperty("user.home") + File.separator + "jMonkey-" + System.currentTimeMillis() / 1000 + ".avi";
286+
workerFile = new File(filename);
287+
} else {
288+
String originalPath = file.getAbsolutePath();
289+
int dotIndex = originalPath.lastIndexOf('.');
290+
String basePath = dotIndex > 0 ? originalPath.substring(0, dotIndex) : originalPath;
291+
String extension = dotIndex > 0 ? originalPath.substring(dotIndex) : ".avi";
292+
workerFile = new File(basePath + "-" + w + "x" + h + "-" + (System.currentTimeMillis() / 1000) + extension);
293+
}
294+
worker = new ResolutionWorker(w, h, workerFile);
295+
workers.put(key, worker);
296+
}
297+
return worker;
298+
}
238299

239300
public void addImage(Renderer renderer, FrameBuffer out) {
240-
if (freeItems == null || reshapePending) {
301+
final ResolutionWorker worker = currentWorker;
302+
if (worker == null) {
241303
return;
242304
}
243305
try {
244-
final WorkItem item = freeItems.take();
245-
usedItems.add(item);
306+
final WorkItem item = worker.freeItems.take();
307+
worker.usedItems.add(item);
246308
item.buffer.clear();
247309
renderer.readFrameBufferWithFormat(out, item.buffer, Image.Format.BGRA8);
248310
executor.submit(new Callable<Void>() {
@@ -253,14 +315,14 @@ public Void call() throws Exception {
253315
item.data = item.buffer.array();
254316
} else {
255317
AndroidScreenshots.convertScreenShot(item.buffer, item.image);
256-
item.data = writer.writeImageToBytes(item.image, quality);
318+
item.data = worker.writer.writeImageToBytes(item.image, quality);
257319
}
258-
while (usedItems.peek() != item) {
320+
while (worker.usedItems.peek() != item) {
259321
Thread.sleep(1);
260322
}
261-
writer.addImage(item.data);
262-
usedItems.poll();
263-
freeItems.add(item);
323+
worker.writer.addImage(item.data);
324+
worker.usedItems.poll();
325+
worker.freeItems.add(item);
264326
return null;
265327
}
266328
});
@@ -277,12 +339,7 @@ public void initialize(RenderManager rm, ViewPort viewPort) {
277339
this.height = camera.getHeight();
278340
this.renderManager = rm;
279341
this.isInitialized = true;
280-
if (freeItems == null) {
281-
freeItems = new LinkedBlockingQueue<WorkItem>();
282-
for (int i = 0; i < numCpus; i++) {
283-
freeItems.add(new WorkItem(width, height));
284-
}
285-
}
342+
this.currentWorker = getWorker(width, height);
286343
}
287344

288345
@Override
@@ -291,10 +348,9 @@ public void reshape(ViewPort vp, int w, int h) {
291348
return;
292349
}
293350

294-
// Mark that reshape is pending and store new dimensions
295-
this.newWidth = w;
296-
this.newHeight = h;
297-
this.reshapePending = true;
351+
this.width = w;
352+
this.height = h;
353+
this.currentWorker = getWorker(w, h);
298354
}
299355

300356
@Override
@@ -304,44 +360,20 @@ public boolean isInitialized() {
304360

305361
@Override
306362
public void preFrame(float tpf) {
307-
// Handle pending reshape if all work items are available
308-
if (reshapePending && freeItems != null && freeItems.size() >= numCpus) {
309-
// All work items are free, safe to reshape
310-
this.width = newWidth;
311-
this.height = newHeight;
312-
this.reshapePending = false;
313-
314-
// Close the current writer and generate new filename for resized video
315-
if (writer != null) {
316-
try {
317-
writer.finishAVI();
318-
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.INFO,
319-
"Window resized from {0}x{1} to {2}x{3}. Previous recording saved to: {4}",
320-
new Object[]{writer.width, writer.height, width, height, file.getAbsolutePath()});
321-
} catch (Exception ex) {
322-
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video on reshape", ex);
323-
}
324-
writer = null;
325-
326-
// Generate a new filename for the resized video
327-
String originalPath = file.getAbsolutePath();
328-
int dotIndex = originalPath.lastIndexOf('.');
329-
String basePath = dotIndex > 0 ? originalPath.substring(0, dotIndex) : originalPath;
330-
String extension = dotIndex > 0 ? originalPath.substring(dotIndex) : ".avi";
331-
file = new File(basePath + "-" + (System.currentTimeMillis() / 1000) + extension);
363+
// Evict old workers that are fully drained
364+
workers.entrySet().removeIf(entry -> {
365+
ResolutionWorker worker = entry.getValue();
366+
if (worker != currentWorker && worker.isFullyDrained()) {
367+
worker.closeWriter();
368+
return true;
332369
}
333-
334-
// Recreate work items with new dimensions
335-
freeItems.clear();
336-
usedItems.clear();
337-
for (int i = 0; i < numCpus; i++) {
338-
freeItems.add(new WorkItem(width, height));
339-
}
340-
}
370+
return false;
371+
});
341372

342-
if (null == writer) {
373+
// Ensure current worker has a writer
374+
if (currentWorker != null && currentWorker.writer == null) {
343375
try {
344-
writer = new MjpegFileWriter(file, width, height, framerate);
376+
currentWorker.writer = new MjpegFileWriter(currentWorker.file, currentWorker.width, currentWorker.height, framerate);
345377
} catch (Exception ex) {
346378
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error creating file writer: {0}", ex);
347379
}
@@ -362,16 +394,19 @@ public void postFrame(FrameBuffer out) {
362394
public void cleanup() {
363395
logger.log(Level.INFO, "cleanup in VideoProcessor");
364396
logger.log(Level.INFO, "VideoProcessor numFrames: {0}", numFrames);
365-
try {
366-
while (freeItems.size() < numCpus) {
367-
Thread.sleep(10);
397+
// Close all workers
398+
for (ResolutionWorker worker : workers.values()) {
399+
try {
400+
while (!worker.isFullyDrained()) {
401+
Thread.sleep(10);
402+
}
403+
logger.log(Level.INFO, "finishAVI in VideoProcessor for {0}x{1}", new Object[]{worker.width, worker.height});
404+
worker.closeWriter();
405+
} catch (Exception ex) {
406+
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video: {0}", ex);
368407
}
369-
logger.log(Level.INFO, "finishAVI in VideoProcessor");
370-
writer.finishAVI();
371-
} catch (Exception ex) {
372-
Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video: {0}", ex);
373408
}
374-
writer = null;
409+
workers.clear();
375410
}
376411

377412
@Override

0 commit comments

Comments
 (0)