/*
* JaamSim Discrete Event Simulation
* Copyright (C) 2013 Ausenco Engineering Canada Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jaamsim.ui;
import java.util.ArrayList;
import javax.swing.JOptionPane;
import com.jaamsim.Graphics.DisplayEntity;
import com.jaamsim.controllers.RenderManager;
import com.jaamsim.controllers.VideoRecorder;
import com.jaamsim.datatypes.IntegerVector;
import com.jaamsim.events.EventHandle;
import com.jaamsim.events.EventManager;
import com.jaamsim.events.ProcessTarget;
import com.jaamsim.input.BooleanInput;
import com.jaamsim.input.ColourInput;
import com.jaamsim.input.EntityListInput;
import com.jaamsim.input.Input;
import com.jaamsim.input.InputAgent;
import com.jaamsim.input.InputErrorException;
import com.jaamsim.input.IntegerInput;
import com.jaamsim.input.IntegerListInput;
import com.jaamsim.input.Keyword;
import com.jaamsim.input.StringInput;
import com.jaamsim.input.ValueInput;
import com.jaamsim.units.TimeUnit;
public class VideoRecorderEntity extends DisplayEntity {
@Keyword(description = "Simulation time at which to capture the first frame.",
exampleList = {"200 h"})
private final ValueInput captureStartTime;
@Keyword(description = "Simulation time between captured frames.",
exampleList = {"60 s"})
private final ValueInput captureInterval;
@Keyword(description = "Total number of frames to capture for the video.\n"
+ "The recorded video assumes 30 frames per second. Therefore, if a "
+ "2 minute video is required, the number of frames should be set to "
+ "120 x 30 = 3600.",
exampleList = {"3600"})
private final IntegerInput captureFrames;
@Keyword(description = "The size of the video/image, expressed as the number of horizontal "
+ "and vertical pixels.\n"
+ "The top left hand corner of the captured frames will be the same as "
+ "the top left hand corner of the image on the monitor. If the "
+ "specified image size is larger than the monitor resolution, then the "
+ "image will be extented beyond the bottom and/or right sides of the "
+ "monitor.",
exampleList = {"1920 1080"})
private final IntegerListInput captureArea;
@Keyword(description = "The list of View windows to be captured.",
exampleList = {"View1 View2 View3"})
private final EntityListInput<View> captureViews;
@Keyword(description = "The background color for the captured frames.\n"
+ "Only the 3D view portion of the specified windows will be captured. "
+ "The remainder of the frame, such as the Control Panel or any gaps "
+ "between the view windows, will be replaced by the background color.",
exampleList = {"skyblue", "135 206 235"})
private final ColourInput videoBGColor;
@Keyword(description = "A label to append to the run name when the AVI file is saved.\n"
+ "The saved file will be named <run name>_<VideoName>.avi.",
exampleList = {"video"})
private final StringInput videoName;
@Keyword(description = "If TRUE, an individual PNG file will be saved for each frame.",
exampleList = {"TRUE"})
private final BooleanInput saveImages;
@Keyword(description = "If TRUE, an AVI file containing the video will be saved.\n"
+ "The AVI file will be encoded using the VP8 codec, which is NOT "
+ "supported by Windows Media Player. Furthermore, the present encoding "
+ "algorithm is quite inefficient making the file size much larger than "
+ "necessary. Both problems can be solved by recoding the video using "
+ "free open-source software such as HandBrake (https://handbrake.fr/).",
exampleList = {"TRUE"})
private final BooleanInput saveVideo;
private boolean hasRunStartup;
private int numFramesWritten;
private final EventHandle captureHandle = new EventHandle();
{
attributeDefinitionList.setHidden(true);
captureStartTime = new ValueInput("CaptureStartTime", "Key Inputs", 0.0d);
captureStartTime.setUnitType(TimeUnit.class);
captureStartTime.setValidRange(0, Double.POSITIVE_INFINITY);
this.addInput(captureStartTime);
captureInterval = new ValueInput("CaptureInterval", "Key Inputs", 3600.0d);
captureInterval.setUnitType(TimeUnit.class);
captureInterval.setValidRange(0.1d, Double.POSITIVE_INFINITY);
this.addInput(captureInterval);
captureFrames = new IntegerInput("CaptureFrames", "Key Inputs", 0);
captureFrames.setValidRange(0, 30000);
this.addInput(captureFrames);
IntegerVector defArea = new IntegerVector(2);
defArea.add(1920);
defArea.add(1080);
captureArea = new IntegerListInput("CaptureArea", "Key Inputs", defArea);
captureArea.setValidCount(2);
captureArea.setValidRange(0, 3000);
this.addInput(captureArea);
captureViews = new EntityListInput<>(View.class, "CaptureViews", "Key Inputs", new ArrayList<View>(0));
captureViews.setRequired(true);
this.addInput(captureViews);
videoBGColor = new ColourInput("VideoBackgroundColor", "Key Inputs", ColourInput.WHITE);
this.addInput(videoBGColor);
this.addSynonym(videoBGColor, "Colour");
videoName = new StringInput("VideoName", "Key Inputs", "");
this.addInput(videoName);
saveImages = new BooleanInput("SaveImages", "Key Inputs", false);
this.addInput(saveImages);
saveVideo = new BooleanInput("SaveVideo", "Key Inputs", false);
this.addInput(saveVideo);
}
@Override
public void validate() {
super.validate();
if( ( saveImages.getValue() || saveVideo.getValue() ) && captureViews.getValue().size() == 0 )
throw new InputErrorException( "CaptureViews must be set when SaveImages or SaveVideo is TRUE" );
}
@Override
public void earlyInit() {
super.earlyInit();
hasRunStartup = false;
numFramesWritten = 0;
}
@Override
public void startUp() {
super.startUp();
if (saveVideo.getValue() || saveImages.getValue())
startProcess(new CaptureNetworkTarget(this));
this.hasRunStartup = true;
}
@Override
public void updateForInput(Input<?> in) {
super.updateForInput(in);
if (in == saveVideo) {
// Start the capture if we are already running and we set the input
// to true
if (hasRunStartup && saveVideo.getValue())
EventManager.scheduleTicks(0, 10, false, new CaptureNetworkTarget(this), null);
}
}
private static class CaptureNetworkTarget extends ProcessTarget {
final VideoRecorderEntity rec;
CaptureNetworkTarget(VideoRecorderEntity rec) {
this.rec = rec;
}
@Override
public String getDescription() {
return rec.getName() + ".doCaptureNetwork";
}
@Override
public void process() {
rec.doCaptureNetwork();
}
}
/**
* Capture JPEG images of the screen at regular simulated intervals
*/
public void doCaptureNetwork() {
// If the capture network is already in progress, then stop the previous network
EventManager.killEvent(captureHandle);
simWait(captureStartTime.getValue(), 10, captureHandle);
if (!RenderManager.isGood()) {
RenderManager.initialize(false);
}
if (!RenderManager.canRenderOffscreen()) {
JOptionPane.showMessageDialog(null, "Your hardware does not support Video Recording.");
return;
}
int width = captureArea.getValue().get(0);
int height = captureArea.getValue().get(1);
ArrayList<View> views = captureViews.getValue();
String videoFileName = String.format("%s_%s", InputAgent.getRunName(), videoName.getValue());
String fullVideoFile = InputAgent.getReportFileName(videoFileName); // getReportFileName() prepends the report directory onto a filename
VideoRecorder recorder = new VideoRecorder(views, fullVideoFile, width, height, captureFrames.getDefaultValue(),
saveImages.getValue(), saveVideo.getValue(), videoBGColor.getValue());
// Otherwise, start capturing
while (saveVideo.getValue() || saveImages.getValue()) {
RenderManager.inst().blockOnScreenShot(recorder);
++numFramesWritten;
if (numFramesWritten == captureFrames.getValue()) {
break;
}
// Wait until the next time to capture a frame
// (priority 10 is used to allow higher priority events to complete first)
simWait(captureInterval.getValue(), 10, captureHandle);
}
recorder.freeResources();
}
}