All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jme3.app.state.VideoRecorderAppState Maven / Gradle / Ivy

There is a newer version: 3.7.0-stable
Show newest version
/*
 * Copyright (c) 2009-2021 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.app.state;

import android.graphics.Bitmap;
import com.jme3.app.Application;
import com.jme3.post.SceneProcessor;
import com.jme3.profile.AppProfiler;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.system.JmeSystem;
import com.jme3.system.Timer;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image;
import com.jme3.util.AndroidScreenshots;
import com.jme3.util.BufferUtils;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A Video recording AppState that records the screen output into an AVI file with
 * M-JPEG content. The file should be playable on any OS in any video player.
* The video recording starts when the state is attached and stops when it is detached * or the application is quit. You can set the fileName of the file to be written when the * state is detached, else the old file will be overwritten. If you specify no file * the AppState will attempt to write a file into the user home directory, made unique * by a timestamp. * @author normenhansen, Robert McIntyre, entrusC */ public class VideoRecorderAppState extends AbstractAppState { private static final Logger logger = Logger.getLogger(VideoRecorderAppState.class.getName()); private int numFrames = 0; private int framerate = 30; private VideoProcessor processor; private File file; private Application app; private ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread th = new Thread(r); th.setName("jME3 Video Processor"); th.setDaemon(true); return th; } }); private int numCpus = Runtime.getRuntime().availableProcessors(); private ViewPort lastViewPort; private float quality; private Timer oldTimer; /** * Using this constructor the video files will be written sequentially to the user's home directory with * a quality of 0.8 and a framerate of 30fps. */ public VideoRecorderAppState() { this(null, 0.8f); } /** * Using this constructor the video files will be written sequentially to the user's home directory. * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file) */ public VideoRecorderAppState(float quality) { this(null, quality); } /** * Using this constructor the video files will be written sequentially to the user's home directory. * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file) * @param framerate the frame rate of the resulting video, the application will be locked to this framerate */ public VideoRecorderAppState(float quality, int framerate) { this(null, quality, framerate); } /** * This constructor allows you to specify the output file of the video. The quality is set * to 0.8 and framerate to 30 fps. * @param file the video file */ public VideoRecorderAppState(File file) { this(file, 0.8f); } /** * This constructor allows you to specify the output file of the video as well as the quality * @param file the video file * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file) */ public VideoRecorderAppState(File file, float quality) { this.file = file; this.quality = quality; Logger.getLogger(this.getClass().getName()).log(Level.FINE, "JME3 VideoRecorder running on {0} CPU's", numCpus); } /** * This constructor allows you to specify the output file of the video as * well as the quality. * * @param file the video file * @param quality the quality of the jpegs in the video stream (0.0 smallest * file - 1.0 largest file) * @param framerate the frame rate of the resulting video, the application * will be locked to this framerate */ public VideoRecorderAppState(File file, float quality, int framerate) { this.file = file; this.quality = quality; this.framerate = framerate; Logger.getLogger(this.getClass().getName()).log(Level.FINE, "JME3 VideoRecorder running on {0} CPU's", numCpus); } public File getFile() { return file; } public void setFile(File file) { if (isInitialized()) { throw new IllegalStateException("Cannot set file while attached!"); } this.file = file; } /** * Get the quality used to compress the video images. * @return the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file) */ public float getQuality() { return quality; } /** * Set the video image quality from 0(worst/smallest) to 1(best/largest). * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file) */ public void setQuality(float quality) { this.quality = quality; } @Override public void initialize(AppStateManager stateManager, Application app) { super.initialize(stateManager, app); this.app = app; this.oldTimer = app.getTimer(); app.setTimer(new IsoTimer(framerate)); if (file == null) { String filename = JmeSystem.getStorageFolder(JmeSystem.StorageFolderType.External) + File.separator + "jMonkey-" + System.currentTimeMillis() / 1000 + ".avi"; logger.log(Level.INFO, "fileName: {0}", filename); file = new File(filename); } processor = new VideoProcessor(); List vps = app.getRenderManager().getPostViews(); for (int i = vps.size() - 1; i >= 0; i-- ) { lastViewPort = vps.get(i); if (lastViewPort.isEnabled()) { break; } } lastViewPort.addProcessor(processor); } @Override public void cleanup() { logger.log(Level.INFO, "removing processor"); lastViewPort.removeProcessor(processor); app.setTimer(oldTimer); initialized = false; file = null; super.cleanup(); } private class WorkItem { ByteBuffer buffer; Bitmap image; byte[] data; public WorkItem(int width, int height) { image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); buffer = BufferUtils.createByteBuffer(width * height * 4); } } private class VideoProcessor implements SceneProcessor { private Camera camera; private int width; private int height; private RenderManager renderManager; private boolean isInitialized = false; private LinkedBlockingQueue freeItems; private LinkedBlockingQueue usedItems = new LinkedBlockingQueue<>(); private MjpegFileWriter writer; private boolean fastMode = true; public void addImage(Renderer renderer, FrameBuffer out) { if (freeItems == null) { return; } try { final WorkItem item = freeItems.take(); usedItems.add(item); item.buffer.clear(); renderer.readFrameBufferWithFormat(out, item.buffer, Image.Format.BGRA8); executor.submit(new Callable() { @Override public Void call() throws Exception { if (fastMode) { item.data = item.buffer.array(); } else { AndroidScreenshots.convertScreenShot(item.buffer, item.image); item.data = writer.writeImageToBytes(item.image, quality); } while (usedItems.peek() != item) { Thread.sleep(1); } writer.addImage(item.data); usedItems.poll(); freeItems.add(item); return null; } }); } catch (InterruptedException ex) { Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, null, ex); } } @Override public void initialize(RenderManager rm, ViewPort viewPort) { logger.log(Level.INFO, "initialize in VideoProcessor"); this.camera = viewPort.getCamera(); this.width = camera.getWidth(); this.height = camera.getHeight(); this.renderManager = rm; this.isInitialized = true; if (freeItems == null) { freeItems = new LinkedBlockingQueue(); for (int i = 0; i < numCpus; i++) { freeItems.add(new WorkItem(width, height)); } } } @Override public void reshape(ViewPort vp, int w, int h) { } @Override public boolean isInitialized() { return this.isInitialized; } @Override public void preFrame(float tpf) { if (null == writer) { try { writer = new MjpegFileWriter(file, width, height, framerate); } catch (Exception ex) { Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error creating file writer: {0}", ex); } } } @Override public void postQueue(RenderQueue rq) { } @Override public void postFrame(FrameBuffer out) { numFrames++; addImage(renderManager.getRenderer(), out); } @Override 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); } 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; } @Override public void setProfiler(AppProfiler profiler) { // not implemented } } public static final class IsoTimer extends com.jme3.system.Timer { private float framerate; private int ticks; private long lastTime = 0; public IsoTimer(float framerate) { this.framerate = framerate; this.ticks = 0; } @Override public long getTime() { return (long) (this.ticks * (1.0f / this.framerate) * 1000f); } @Override public long getResolution() { return 1000L; } @Override public float getFrameRate() { return this.framerate; } @Override public float getTimePerFrame() { return 1.0f / this.framerate; } @Override public void update() { long time = System.currentTimeMillis(); long difference = time - lastTime; lastTime = time; if (difference < (1.0f / this.framerate) * 1000.0f) { try { Thread.sleep(difference); } catch (InterruptedException ex) { } } else if (logger.isLoggable(Level.INFO)) { logger.log(Level.INFO, "actual tpf(ms): {0}, 1/framerate(ms): {1}", new Object[]{difference, (1.0f / this.framerate) * 1000.0f}); } this.ticks++; } @Override public void reset() { this.ticks = 0; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy