Created
January 24, 2025 14:29
-
-
Save narkai/da17716633cf33ded8baa6a0a29acada to your computer and use it in GitHub Desktop.
VideoExport with alpha
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Also see hamoid/video_export_processing#49