Motion JPEG Erzeugung aus Java heraus

vorhergehende Artikel in: Java Komponenten
07.02.2025

Da ich mich in den letzten Wochen wieder einmal mit Javas Sicherheitsmechanismen und dem Erzeugen von Animationen beschäftigt habe, habe ich den Entschluss gefasst, die bisher mittels JMF AVIs in dWb+ zu erstetzen - nur wodurch?

Ich konnte JMF schon seit längerem nicht mehr einsetzen, da es sich - sobald ein SecurityManager aktiv ist - standhaft weigert, überhaupt irgendwelche Dateien zu schreiben und es immer Dateien schreiben möchte - ich habe keine Möglichkeit gefunden, in Streams zu schreiben, die ich ja dann selber in eine Datei umleiten könnte.

Also muste eine Alternative her. Es ist nicht einfach, Animationen oder Videos aus Java heraus zu erzeugen - die einzige Bibliothek, mit der ich früher einige Erfolge gefeiert hatte - Xuggler - wird schon längere Zeit nicht mehr gepflegt.

Ich fand dann aber ein Beispiel, wie man mittels Java Motion JPEG Dateien erzeugen kann und war angetan - allerdings benötigte dieser auch schon wieder 12 Jahre alte Code eine Generalüberholung. Und so sieht meine modernisierte Variante davon nun so aus:

import java.awt.image.RenderedImage;	//Vector
import javax.imageio.IIOImage;
import javax.imageio.ImageIO; //encodingJPEG
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.Closeable;
import java.io.IOException;

//Original: https://github.com/tjrantal/JavaFrameByFrameVideo/blob/master/src/mjpeg/MjpegWriter.java public class MjpegWriter extends java.lang.Object implements Closeable {

private final static org.slf4j.Logger CLASS_LOGGER =org.slf4j.LoggerFactory.getLogger(MjpegWriter.class); private long frameNo; private java.util.List<Long> frames; private java.util.List<Integer> fsizes; private long movi; private long[] frameP; //Frame pointer private long[] position ; //File position pointer private MemoryCacheImageOutputStream videoFile; //Video file to write to private int height; private int width; private long frameCount; private final float quality; private enum State{ BeforeHeader, WritingImages, StreamClosed; }; private State state=State.BeforeHeader; private final int framerate; private java.io.OutputStream os; //Constructor

public MjpegWriter(java.io.OutputStream os) { this(os,0.9f,25); } public MjpegWriter(java.io.OutputStream os, float quality,int framerate) { frameP = new long[3]; position = new long[3]; frames = new java.util.LinkedList<Long>(); fsizes = new java.util.LinkedList<Integer>(); frameCount = 0; this.os=os; videoFile = new MemoryCacheImageOutputStream(os); this.quality=quality; this.framerate=framerate; }

private void writeHeader() throws java.io.IOException { String text = "RIFF"; videoFile.write(text.getBytes("ISO-8859-1")); //Riff videoFile.write(intToByteArray(0)); //File size in bytes-8

//AVI text = "AVI "; videoFile.write(text.getBytes("ISO-8859-1")); //AVI text = "LIST"; videoFile.write(text.getBytes("ISO-8859-1")); //LIST videoFile.write(intToByteArray(192)); //list size (header size-12) text = "hdrl"; videoFile.write(text.getBytes("ISO-8859-1")); //main header text = "avih"; videoFile.write(text.getBytes("ISO-8859-1")); //avi header videoFile.write(intToByteArray(14*4)); //avih size in bytes

//AVI header data videoFile.write(intToByteArray(1000*1000/framerate)); //usec per frame videoFile.write(intToByteArray(framerate*3*width*height)); //bytes per sec videoFile.write(intToByteArray(0)); //reserved videoFile.write(intToByteArray(2320)); //flags (none required for raw) frameP[0] = videoFile.getStreamPosition(); videoFile.write(intToByteArray(0)); //total frames (none required for raw) videoFile.write(intToByteArray(0)); //initial frames (0 for non-interleaved) videoFile.write(intToByteArray(1)); //No. of streams videoFile.write(intToByteArray(width*height*3)); //Buffer size videoFile.write(intToByteArray(width)); //Width videoFile.write(intToByteArray(height)); //Height videoFile.write(intToByteArray(0)); //Reserved[4] set to zero videoFile.write(intToByteArray(0)); //Reserved[4] set to zero videoFile.write(intToByteArray(0)); //Reserved[4] set to zero videoFile.write(intToByteArray(0)); //Reserved[4] set to zero //AVI header written

//Stream header text = "LIST"; videoFile.write(text.getBytes("ISO-8859-1")); videoFile.write(intToByteArray(116)); //list size in bytes text = "strl"; //Stream list videoFile.write(text.getBytes("ISO-8859-1")); //Stream list text = "strh"; //Stream header videoFile.write(text.getBytes("ISO-8859-1")); //videoFileStream header videoFile.write(intToByteArray(56)); //videoFileStream header size in bytes //Stream header data text = "vids"; //Stream header videoFile.write(text.getBytes("ISO-8859-1")); //videoFileStream header text = "MJPG"; //Stream handler videoFile.write(text.getBytes("ISO-8859-1")); //Stream handler videoFile.write(intToByteArray(0)); //Flags videoFile.write(intToByteArray(0)); //Priority none needed for raw.. videoFile.write(intToByteArray(0)); //Language needed for raw.. //InitialFrames not used -> no audio... videoFile.write(intToByteArray(1)); //dwScale videoFile.write(intToByteArray(framerate)); //dwRate videoFile.write(intToByteArray(0)); //dwStart frameP[1] = videoFile.getStreamPosition();

videoFile.write(intToByteArray(0)); //dwLength videoFile.write(intToByteArray(0)); //dwBuffer videoFile.write(intToByteArray(-1)); //dwQuality videoFile.write(intToByteArray(0)); //dwSampleSize videoFile.write(intToByteArray(0)); //rcFrame videoFile.write(intToByteArray(0)); //rcFrame //Stream header data written text = "strf"; //Stream format videoFile.write(text.getBytes("ISO-8859-1")); //videoFileStream format videoFile.write(intToByteArray(40)); //videoFileStream fomat size in bytes videoFile.write(intToByteArray(40)); //Format header length = 40 //format data videoFile.write(intToByteArray(width)); //width videoFile.write(intToByteArray(height)); //height videoFile.write(shortToByteArray((short) 1)); //planes = must be 1 videoFile.write(shortToByteArray((short) 0)); //bitCount (0 =jpeg tai png) text = "MJPG"; //Compression videoFile.write(text.getBytes("ISO-8859-1")); //compression videoFile.write(intToByteArray(width*height*3)); //image size in bytes videoFile.write(intToByteArray(0)); //xpixels per meter videoFile.write(intToByteArray(0)); //ypixels per meter videoFile.write(intToByteArray(0)); //ClrUsed videoFile.write(intToByteArray(0)); //ClrImportant //format data written...

//DATA chunk!! text = "LIST"; //MOVI chunk videoFile.write(text.getBytes("ISO-8859-1")); //Movi chunk frameP[2] = videoFile.getStreamPosition(); //Insert pointer for decoded data videoFile.write(intToByteArray(0)); //Decoded Data size!!

movi = videoFile.getStreamPosition(); CLASS_LOGGER.trace("setting movi {}",movi); text = "movi"; //MOVI list name videoFile.write(text.getBytes("ISO-8859-1")); //Movi list name }

//Functions public void write_frame(RenderedImage image) throws IOException { if(state==State.BeforeHeader) { width=image.getWidth(); height= image.getHeight(); writeHeader(); state=State.WritingImages; } if(state==State.WritingImages) { String text = "00dc"; //MOVI chunk position[0] = videoFile.getStreamPosition(); videoFile.write(text.getBytes("ISO-8859-1")); //Movi chunk videoFile.write(intToByteArray(width * height * 3));//Movi chunk size place holder.... position[1] = videoFile.getStreamPosition(); write_JPEG(image); if ((videoFile.getStreamPosition() - position[0]) % 4 > 0) { //Zero padding byte[] padding = new byte[(int) ((videoFile.getStreamPosition() - position[0]) % 4)]; videoFile.write(padding); } position[2] = videoFile.getStreamPosition(); videoFile.seek(position[0] + 4); videoFile.write(intToByteArray((int) (position[2] - position[1]))); //Movi chunk size videoFile.seek(position[2]); frames.add(position[0] - movi); fsizes.add((int) (position[2] - position[1])); ++frameCount; } else { throw new IllegalStateException("not in state "+State.WritingImages+"!"); } }

public void close() throws java.io.IOException{

long currentPosition = videoFile.getStreamPosition(); long idxPos = currentPosition; CLASS_LOGGER.trace("seeking to {}",movi-4); videoFile.seek(movi-4); videoFile.write(intToByteArray((int) (currentPosition-movi))); //Movi chunk size videoFile.seek(currentPosition); //Write index String text = "idx1"; //idx1 chunk videoFile.write(text.getBytes("ISO-8859-1")); //idx1 chunk videoFile.write(intToByteArray((int) (frameCount*4*4))); //index size for (int k = 0;k<frameCount;k++){ text = "00dc"; //idx1 chunk videoFile.write(text.getBytes("ISO-8859-1")); //idx1 chunk videoFile.write(intToByteArray((int) 16)); //Key frame flag!! videoFile.write(intToByteArray(frames.get(k).intValue())); //offset from BEGINNING of (thus the +4) movi!! videoFile.write(intToByteArray((int) (fsizes.get(k)))); //chunk size } currentPosition = videoFile.getStreamPosition(); videoFile.seek(4); videoFile.write(intToByteArray((int) (currentPosition-8))); //File size videoFile.seek(frameP[0]);

videoFile.write(intToByteArray((int) frameCount)); videoFile.seek(frameP[1]);

videoFile.write(intToByteArray((int) frameCount)); //frames2

videoFile.seek(frameP[2]); videoFile.write(intToByteArray((int) (idxPos-movi))); //encoded image data amount //Index written... //Close videoFile videoFile.close(); os.close(); state=State.StreamClosed; }

private void write_JPEG(RenderedImage data) throws IOException { BufferedImage bi; //if the image has an alpha channel, the JPEG writer fails silently without outputting any data //https://stackoverflow.com/a/66954103 if(data.getColorModel().hasAlpha()) { CLASS_LOGGER.warn("Image has alpha channel - needs to be converted..."); bi=de.elbosso.ui.image.Utilities.applyAlphaAndRemoveAlphaChannel(data); } else { bi=de.elbosso.ui.image.Utilities.toBufferedImage(data); } java.io.ByteArrayOutputStream outData = new java.io.ByteArrayOutputStream(); ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next(); ImageWriteParam jpgWriteParam = jpgWriter.getDefaultWriteParam(); jpgWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); jpgWriteParam.setCompressionQuality(quality);

ImageOutputStream ios=new MemoryCacheImageOutputStream(outData); jpgWriter.setOutput(ios); IIOImage outputImage = new IIOImage(bi, null, null); jpgWriter.write(null, outputImage, jpgWriteParam); jpgWriter.dispose(); outData.close(); byte[] compressedData = outData.toByteArray(); CLASS_LOGGER.trace("compressed frame size {}",compressedData.length); videoFile.write(compressedData); }

private byte[] intToByteArray(int value){ // return new byte[] {(byte) (value >> 24 & 0xFF),(byte) (value >> 16 & 0xFF),(byte) (value >> 8 & 0xFF),(byte) (value & 0xFF)}; return new byte[] {(byte) (value & 0xFF),(byte) ((value >> 8) & 0xFF),(byte) ((value >> 16) & 0xFF),(byte) ((value >> 24) & 0xFF)}; } private byte[] shortToByteArray(short value){ // return new byte[] {(byte) (value >> 8 & 0xFF),(byte) (value & 0xFF)}; return new byte[] {(byte) (value & 0xFF),(byte) ((value>>8) & 0xFF)};

}

private byte[] intArrayToByteArray(int[] arrayIn){ byte[] arrayOut = new byte[arrayIn.length*4]; byte[] temp; for (int i= 0; i<arrayIn.length; ++i){ temp = intToByteArray(arrayIn[i]); for (int j = 0; j< 4; ++j){ arrayOut[4*i+j] = temp[j]; } } return arrayOut; } }

Artikel, die hierher verlinken

Animationen mit Multi-Scale Truchet Patterns

07.02.2025

Ich hatte neulich hier einen Link zu Multi-Scale Truchet Patterns und habe seitdem den Algorithmus mit java umgesetzt und ihn als Teil meines Projekts zur Testdatengenerierung veröffentlicht.

Alle Artikel rss Wochenübersicht Monatsübersicht Codeberg Repositories Mastodon Über mich home xmpp


Vor 5 Jahren hier im Blog

  • OAuth und OTP

    16.02.2020

    Wie bereits beschrieben will ich mich demnächst näher mit OAuth befassen...

    Weiterlesen...

Neueste Artikel

  • Split von Filesets in Apache ANT

    Ich musste neulich darüber nachdenken, eine Parallelisierung für einen meiner ANT-Tasks in meinem Static Site Generator einzubauen.

    Weiterlesen
  • Ein Doclet zur Erzeugung von DocBook aus Javadoc

    Ich habe mich mit der Idee zu diesem Projekt Monate abgequält - hätte ich gewusst, was die eigentliche Implementierung für Qualen verursachen würde, hätte ich sie wahrscheinlich eingestampft.

    Weiterlesen
  • Animationen mit Multi-Scale Truchet Patterns

    Ich hatte neulich hier einen Link zu Multi-Scale Truchet Patterns und habe seitdem den Algorithmus mit java umgesetzt und ihn als Teil meines Projekts zur Testdatengenerierung veröffentlicht.

    Weiterlesen

Manche nennen es Blog, manche Web-Seite - ich schreibe hier hin und wieder über meine Erlebnisse, Rückschläge und Erleuchtungen bei meinen Hobbies.

Wer daran teilhaben und eventuell sogar davon profitieren möchte, muss damit leben, daß ich hin und wieder kleine Ausflüge in Bereiche mache, die nichts mit IT, Administration oder Softwareentwicklung zu tun haben.

Ich wünsche allen Lesern viel Spaß und hin und wieder einen kleinen AHA!-Effekt...

PS: Meine öffentlichen Codeberg-Repositories findet man hier.