Skip to content

Instantly share code, notes, and snippets.

@narkai
Created January 24, 2025 14:29
Show Gist options
  • Save narkai/da17716633cf33ded8baa6a0a29acada to your computer and use it in GitHub Desktop.
Save narkai/da17716633cf33ded8baa6a0a29acada to your computer and use it in GitHub Desktop.
VideoExport with alpha
// Simplified version of VideoExport by Abe Pazos
// https://funprogramming.org/VideoExport-for-Processing/
// https://github.com/hamoid/video_export_processing
// http://ffmpeg.org/
import java.io.OutputStream;
import java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.Wincon;
class VideoExport {
PApplet parent;
String outputFilePath;
PImage img;
String SETTINGS_FFMPEG_PATH = "ffmpeg_path";
String FFMPEG_PATH_UNSET = "ffmpeg_path_unset";
JSONArray FFMPEG_CMD;
float ffmpegFrameRate;
String ffmpegMetadataComment;
ProcessBuilder processBuilder;
Process process;
OutputStream ffmpeg;
File ffmpegOutputLog;
boolean ffmpegFound;
byte[] pixelsByte;
int frameC;
VideoExport(PApplet parent, String outputFileName, PImage img)
{
this.parent = parent;
this.img = img;
outputFilePath = parent.sketchPath(outputFileName);
ffmpegFrameRate = 30f;
ffmpegMetadataComment = "Made with Animaction";
ffmpegFound = false;
pixelsByte = null;
parent.registerMethod("dispose", this);
// -y = overwrite, otherwise it fails the second time you run
// "-i", "-" = pipe:0
// -an = no audio
FFMPEG_CMD = new JSONArray(new StringList(new String[]{
// --------------- init
"[ffmpeg]",
"-y",
// --------------- input
"-f", "rawvideo",
"-vcodec", "rawvideo",
"-pixel_format", "argb",
"-s", "[width]x[height]",
"-r", "[fps]",
"-i", "-",
// --------------- output
"-an",
// --------------- codecs
// > codec : output name / container
// *************** tested successfully :
// > Png sequence : "output.mov"
"-vcodec", "png",
// > Quicktime animation : "output.mov"
// "-vcodec", "qtrle",
// > Apple prores : "output.mov"
// "-vcodec", "prores_ks",
// "-pix_fmt", "yuva444p10le",
// "-profile", "4444",
// "-alpha_bits", "8",
// "-quant_mat", "hq",
// "-q:v", "0",
// > Png images : "output-%04d.png"
// "-f", "image2",
// *************** to test (does not open in Photoshop to check for alpha) :
// > HAP : "output.mov"
// "-vcodec", "hap",
// "-format", "hap_alpha",
// > VP9 : "output.webm"
// "-vcodec", "libvpx",
// "-pix_fmt", "yuva420p",
// "-metadata:s:v:0", "alpha_mode=\"1\"",
// "-auto-alt-ref", "0",
// > DNxHR : "output.mov"
// "-vcodec", "dnxhd",
// "-profile", "dnxhr_444",
// "-pix_fmt", "yuv444p10le",
// > DNxHR : "output.mov"
// "-vcodec", "dnxhd",
// "-profile", "dnxhr_hqx",
// "-pix_fmt", "yuv422p10le",
// *************** VideoExport default, no alpha
// > h264 : "output.mp4"
// "-vcodec", "h264",
// "-pix_fmt", "yuv420p",
// "-crf", "70",
// --------------- container
"-metadata", "comment=[comment]",
"[output]"
// ---------------
}));
}
void startMovie()
{
String ffmpeg_path = FFMPEG_PATH_UNSET;
String[] guess_paths = { "/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg" };
for (String guess_path : guess_paths) {
if ((new File(guess_path)).isFile()) {
ffmpeg_path = guess_path;
break;
}
}
if (ffmpeg_path.equals(FFMPEG_PATH_UNSET)) {
println("Video export requires ffmpeg, please install ffmpeg using your package manager");
} else {
startFfmpeg(ffmpeg_path);
}
}
void startFfmpeg(String executable)
{
if (img.pixelWidth == 0 || img.pixelHeight == 0) {
err("The export image size is 0!");
}
if (img.pixelWidth % 2 == 1 || img.pixelHeight % 2 == 1) {
err(
"Width and height can only be even numbers when using the h264 encoder\n"
+ "but the requested image size is " + img.pixelWidth + "x" + img.pixelHeight
);
}
JSONArray cmd = FFMPEG_CMD;
String[] cmdArgs = cmd.getStringArray();
for (int i = 0; i < cmdArgs.length; i++) {
if (cmdArgs[i].contains("[")) {
cmdArgs[i] = cmdArgs[i]
.replace("[ffmpeg]", executable)
.replace("[width]", "" + img.pixelWidth)
.replace("[height]", "" + img.pixelHeight)
.replace("[fps]", "" + ffmpegFrameRate)
.replace("[comment]", ffmpegMetadataComment)
.replace("[output]", outputFilePath)
;
}
}
processBuilder = new ProcessBuilder(cmdArgs);
processBuilder.redirectErrorStream(true);
ffmpegOutputLog = new File(parent.sketchPath("ffmpeg.txt"));
processBuilder.redirectOutput(ffmpegOutputLog);
processBuilder.redirectInput(ProcessBuilder.Redirect.PIPE);
try {
process = processBuilder.start();
} catch (Exception e) {
e.printStackTrace();
err(ffmpegOutputLog);
}
ffmpeg = process.getOutputStream();
ffmpegFound = true;
frameC = 0;
}
void saveMovie()
{
if (img == null || img.width == 0) return;
if (!ffmpegFound) return;
if (pixelsByte == null) pixelsByte = new byte[img.pixelWidth * img.pixelHeight * 4];
img.loadPixels();
int byteNum = 0;
for (final int px : img.pixels) {
pixelsByte[byteNum++] = (byte) (px >> 24);
pixelsByte[byteNum++] = (byte) (px >> 16);
pixelsByte[byteNum++] = (byte) (px >> 8);
pixelsByte[byteNum++] = (byte) (px);
}
try {
ffmpeg.write(pixelsByte);
frameC++;
} catch (Exception e) {
e.printStackTrace();
err(ffmpegOutputLog);
}
}
void endMovie()
{
if (ffmpeg != null) {
try {
ffmpeg.flush();
ffmpeg.close();
} catch (Exception e) {
e.printStackTrace();
}
ffmpeg = null;
}
if (process != null) {
try {
// Thread.sleep(500);
// CTRL+C keys to ffmpeg if using Windows
if (PApplet.platform == PConstants.WINDOWS) {
ProcessBuilder ps = new ProcessBuilder("tasklist");
Process pr = ps.start();
BufferedReader allProcesses = new BufferedReader(new InputStreamReader(pr.getInputStream()));
Pattern isFfmpeg = Pattern.compile("ffmpeg\\.exe.*?([0-9]+)");
String processDetails;
while ((processDetails = allProcesses.readLine()) != null) {
Matcher m = isFfmpeg.matcher(processDetails);
if (m.find()) {
Wincon wincon = Kernel32.INSTANCE;
wincon.GenerateConsoleCtrlEvent(Wincon.CTRL_C_EVENT, Integer.parseInt(m.group(1)));
break;
}
}
} else {
process.destroy();
}
process.waitFor();
println(outputFilePath, "saved.");
} catch (InterruptedException e) {
println("Waiting for ffmpeg timed out!");
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
processBuilder = null;
process = null;
}
}
void err(String msg)
{
System.err.println("\nExport error: " + msg + "\n");
System.exit(1);
}
void err(File f)
{
err("Ffmpeg failed. Study " + f + " for more details.");
}
int getCurrentFrame()
{
return frameC;
}
float getCurrentTime()
{
return frameC / ffmpegFrameRate;
}
void dispose()
{
endMovie();
}
}
@narkai
Copy link
Author

narkai commented Jan 24, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment