1 package com.jme3.app.state;
2 
3 import java.awt.Graphics2D;
4 import java.awt.Image;
5 import java.awt.image.BufferedImage;
6 import java.io.ByteArrayOutputStream;
7 import java.io.File;
8 import java.io.FileOutputStream;
9 import java.io.RandomAccessFile;
10 import java.nio.channels.FileChannel;
11 import java.util.ArrayList;
12 import java.util.Arrays;
13 import java.util.List;
14 import javax.imageio.ImageIO;
15 
16 /**
17  * Released under BSD License
18  * @author monceaux, normenhansen
19  */
20 public class MjpegFileWriter {
21 
22     int width = 0;
23     int height = 0;
24     double framerate = 0;
25     int numFrames = 0;
26     File aviFile = null;
27     FileOutputStream aviOutput = null;
28     FileChannel aviChannel = null;
29     long riffOffset = 0;
30     long aviMovieOffset = 0;
31     AVIIndexList indexlist = null;
32 
MjpegFileWriter(File aviFile, int width, int height, double framerate)33     public MjpegFileWriter(File aviFile, int width, int height, double framerate) throws Exception {
34         this(aviFile, width, height, framerate, 0);
35     }
36 
MjpegFileWriter(File aviFile, int width, int height, double framerate, int numFrames)37     public MjpegFileWriter(File aviFile, int width, int height, double framerate, int numFrames) throws Exception {
38         this.aviFile = aviFile;
39         this.width = width;
40         this.height = height;
41         this.framerate = framerate;
42         this.numFrames = numFrames;
43         aviOutput = new FileOutputStream(aviFile);
44         aviChannel = aviOutput.getChannel();
45 
46         RIFFHeader rh = new RIFFHeader();
47         aviOutput.write(rh.toBytes());
48         aviOutput.write(new AVIMainHeader().toBytes());
49         aviOutput.write(new AVIStreamList().toBytes());
50         aviOutput.write(new AVIStreamHeader().toBytes());
51         aviOutput.write(new AVIStreamFormat().toBytes());
52         aviOutput.write(new AVIJunk().toBytes());
53         aviMovieOffset = aviChannel.position();
54         aviOutput.write(new AVIMovieList().toBytes());
55         indexlist = new AVIIndexList();
56     }
57 
addImage(Image image)58     public void addImage(Image image) throws Exception {
59         addImage(writeImageToBytes(image));
60     }
61 
addImage(byte[] imagedata)62     public void addImage(byte[] imagedata) throws Exception {
63         byte[] fcc = new byte[]{'0', '0', 'd', 'b'};
64         int useLength = imagedata.length;
65         long position = aviChannel.position();
66         int extra = (useLength + (int) position) % 4;
67         if (extra > 0) {
68             useLength = useLength + extra;
69         }
70 
71         indexlist.addAVIIndex((int) position, useLength);
72 
73         aviOutput.write(fcc);
74         aviOutput.write(intBytes(swapInt(useLength)));
75         aviOutput.write(imagedata);
76         if (extra > 0) {
77             for (int i = 0; i < extra; i++) {
78                 aviOutput.write(0);
79             }
80         }
81         imagedata = null;
82     }
83 
finishAVI()84     public void finishAVI() throws Exception {
85         byte[] indexlistBytes = indexlist.toBytes();
86         aviOutput.write(indexlistBytes);
87         aviOutput.close();
88         long size = aviFile.length();
89         RandomAccessFile raf = new RandomAccessFile(aviFile, "rw");
90         raf.seek(4);
91         raf.write(intBytes(swapInt((int) size - 8)));
92         raf.seek(aviMovieOffset + 4);
93         raf.write(intBytes(swapInt((int) (size - 8 - aviMovieOffset - indexlistBytes.length))));
94         raf.close();
95     }
96 
97     // public void writeAVI(File file) throws Exception
98     // {
99     // OutputStream os = new FileOutputStream(file);
100     //
101     // // RIFFHeader
102     // // AVIMainHeader
103     // // AVIStreamList
104     // // AVIStreamHeader
105     // // AVIStreamFormat
106     // // write 00db and image bytes...
107     // }
swapInt(int v)108     public static int swapInt(int v) {
109         return (v >>> 24) | (v << 24) | ((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00);
110     }
111 
swapShort(short v)112     public static short swapShort(short v) {
113         return (short) ((v >>> 8) | (v << 8));
114     }
115 
intBytes(int i)116     public static byte[] intBytes(int i) {
117         byte[] b = new byte[4];
118         b[0] = (byte) (i >>> 24);
119         b[1] = (byte) ((i >>> 16) & 0x000000FF);
120         b[2] = (byte) ((i >>> 8) & 0x000000FF);
121         b[3] = (byte) (i & 0x000000FF);
122 
123         return b;
124     }
125 
shortBytes(short i)126     public static byte[] shortBytes(short i) {
127         byte[] b = new byte[2];
128         b[0] = (byte) (i >>> 8);
129         b[1] = (byte) (i & 0x000000FF);
130 
131         return b;
132     }
133 
134     private class RIFFHeader {
135 
136         public byte[] fcc = new byte[]{'R', 'I', 'F', 'F'};
137         public int fileSize = 0;
138         public byte[] fcc2 = new byte[]{'A', 'V', 'I', ' '};
139         public byte[] fcc3 = new byte[]{'L', 'I', 'S', 'T'};
140         public int listSize = 200;
141         public byte[] fcc4 = new byte[]{'h', 'd', 'r', 'l'};
142 
RIFFHeader()143         public RIFFHeader() {
144         }
145 
toBytes()146         public byte[] toBytes() throws Exception {
147             ByteArrayOutputStream baos = new ByteArrayOutputStream();
148             baos.write(fcc);
149             baos.write(intBytes(swapInt(fileSize)));
150             baos.write(fcc2);
151             baos.write(fcc3);
152             baos.write(intBytes(swapInt(listSize)));
153             baos.write(fcc4);
154             baos.close();
155 
156             return baos.toByteArray();
157         }
158     }
159 
160     private class AVIMainHeader {
161         /*
162          *
163          * FOURCC fcc; DWORD cb; DWORD dwMicroSecPerFrame; DWORD
164          * dwMaxBytesPerSec; DWORD dwPaddingGranularity; DWORD dwFlags; DWORD
165          * dwTotalFrames; DWORD dwInitialFrames; DWORD dwStreams; DWORD
166          * dwSuggestedBufferSize; DWORD dwWidth; DWORD dwHeight; DWORD
167          * dwReserved[4];
168          */
169 
170         public byte[] fcc = new byte[]{'a', 'v', 'i', 'h'};
171         public int cb = 56;
172         public int dwMicroSecPerFrame = 0;                                // (1
173         // /
174         // frames
175         // per
176         // sec)
177         // *
178         // 1,000,000
179         public int dwMaxBytesPerSec = 10000000;
180         public int dwPaddingGranularity = 0;
181         public int dwFlags = 65552;
182         public int dwTotalFrames = 0;                                // replace
183         // with
184         // correct
185         // value
186         public int dwInitialFrames = 0;
187         public int dwStreams = 1;
188         public int dwSuggestedBufferSize = 0;
189         public int dwWidth = 0;                                // replace
190         // with
191         // correct
192         // value
193         public int dwHeight = 0;                                // replace
194         // with
195         // correct
196         // value
197         public int[] dwReserved = new int[4];
198 
AVIMainHeader()199         public AVIMainHeader() {
200             dwMicroSecPerFrame = (int) ((1.0 / framerate) * 1000000.0);
201             dwWidth = width;
202             dwHeight = height;
203             dwTotalFrames = numFrames;
204         }
205 
toBytes()206         public byte[] toBytes() throws Exception {
207             ByteArrayOutputStream baos = new ByteArrayOutputStream();
208             baos.write(fcc);
209             baos.write(intBytes(swapInt(cb)));
210             baos.write(intBytes(swapInt(dwMicroSecPerFrame)));
211             baos.write(intBytes(swapInt(dwMaxBytesPerSec)));
212             baos.write(intBytes(swapInt(dwPaddingGranularity)));
213             baos.write(intBytes(swapInt(dwFlags)));
214             baos.write(intBytes(swapInt(dwTotalFrames)));
215             baos.write(intBytes(swapInt(dwInitialFrames)));
216             baos.write(intBytes(swapInt(dwStreams)));
217             baos.write(intBytes(swapInt(dwSuggestedBufferSize)));
218             baos.write(intBytes(swapInt(dwWidth)));
219             baos.write(intBytes(swapInt(dwHeight)));
220             baos.write(intBytes(swapInt(dwReserved[0])));
221             baos.write(intBytes(swapInt(dwReserved[1])));
222             baos.write(intBytes(swapInt(dwReserved[2])));
223             baos.write(intBytes(swapInt(dwReserved[3])));
224             baos.close();
225 
226             return baos.toByteArray();
227         }
228     }
229 
230     private class AVIStreamList {
231 
232         public byte[] fcc = new byte[]{'L', 'I', 'S', 'T'};
233         public int size = 124;
234         public byte[] fcc2 = new byte[]{'s', 't', 'r', 'l'};
235 
AVIStreamList()236         public AVIStreamList() {
237         }
238 
toBytes()239         public byte[] toBytes() throws Exception {
240             ByteArrayOutputStream baos = new ByteArrayOutputStream();
241             baos.write(fcc);
242             baos.write(intBytes(swapInt(size)));
243             baos.write(fcc2);
244             baos.close();
245 
246             return baos.toByteArray();
247         }
248     }
249 
250     private class AVIStreamHeader {
251         /*
252          * FOURCC fcc; DWORD cb; FOURCC fccType; FOURCC fccHandler; DWORD
253          * dwFlags; WORD wPriority; WORD wLanguage; DWORD dwInitialFrames; DWORD
254          * dwScale; DWORD dwRate; DWORD dwStart; DWORD dwLength; DWORD
255          * dwSuggestedBufferSize; DWORD dwQuality; DWORD dwSampleSize; struct {
256          * short int left; short int top; short int right; short int bottom; }
257          * rcFrame;
258          */
259 
260         public byte[] fcc = new byte[]{'s', 't', 'r', 'h'};
261         public int cb = 64;
262         public byte[] fccType = new byte[]{'v', 'i', 'd', 's'};
263         public byte[] fccHandler = new byte[]{'M', 'J', 'P', 'G'};
264         public int dwFlags = 0;
265         public short wPriority = 0;
266         public short wLanguage = 0;
267         public int dwInitialFrames = 0;
268         public int dwScale = 0;                                // microseconds
269         // per
270         // frame
271         public int dwRate = 1000000;                          // dwRate
272         // /
273         // dwScale
274         // =
275         // frame
276         // rate
277         public int dwStart = 0;
278         public int dwLength = 0;                                // num
279         // frames
280         public int dwSuggestedBufferSize = 0;
281         public int dwQuality = -1;
282         public int dwSampleSize = 0;
283         public int left = 0;
284         public int top = 0;
285         public int right = 0;
286         public int bottom = 0;
287 
AVIStreamHeader()288         public AVIStreamHeader() {
289             dwScale = (int) ((1.0 / framerate) * 1000000.0);
290             dwLength = numFrames;
291         }
292 
toBytes()293         public byte[] toBytes() throws Exception {
294             ByteArrayOutputStream baos = new ByteArrayOutputStream();
295             baos.write(fcc);
296             baos.write(intBytes(swapInt(cb)));
297             baos.write(fccType);
298             baos.write(fccHandler);
299             baos.write(intBytes(swapInt(dwFlags)));
300             baos.write(shortBytes(swapShort(wPriority)));
301             baos.write(shortBytes(swapShort(wLanguage)));
302             baos.write(intBytes(swapInt(dwInitialFrames)));
303             baos.write(intBytes(swapInt(dwScale)));
304             baos.write(intBytes(swapInt(dwRate)));
305             baos.write(intBytes(swapInt(dwStart)));
306             baos.write(intBytes(swapInt(dwLength)));
307             baos.write(intBytes(swapInt(dwSuggestedBufferSize)));
308             baos.write(intBytes(swapInt(dwQuality)));
309             baos.write(intBytes(swapInt(dwSampleSize)));
310             baos.write(intBytes(swapInt(left)));
311             baos.write(intBytes(swapInt(top)));
312             baos.write(intBytes(swapInt(right)));
313             baos.write(intBytes(swapInt(bottom)));
314             baos.close();
315 
316             return baos.toByteArray();
317         }
318     }
319 
320     private class AVIStreamFormat {
321         /*
322          * FOURCC fcc; DWORD cb; DWORD biSize; LONG biWidth; LONG biHeight; WORD
323          * biPlanes; WORD biBitCount; DWORD biCompression; DWORD biSizeImage;
324          * LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD
325          * biClrImportant;
326          */
327 
328         public byte[] fcc = new byte[]{'s', 't', 'r', 'f'};
329         public int cb = 40;
330         public int biSize = 40;                               // same
331         // as
332         // cb
333         public int biWidth = 0;
334         public int biHeight = 0;
335         public short biPlanes = 1;
336         public short biBitCount = 24;
337         public byte[] biCompression = new byte[]{'M', 'J', 'P', 'G'};
338         public int biSizeImage = 0;                                // width
339         // x
340         // height
341         // in
342         // pixels
343         public int biXPelsPerMeter = 0;
344         public int biYPelsPerMeter = 0;
345         public int biClrUsed = 0;
346         public int biClrImportant = 0;
347 
AVIStreamFormat()348         public AVIStreamFormat() {
349             biWidth = width;
350             biHeight = height;
351             biSizeImage = width * height;
352         }
353 
toBytes()354         public byte[] toBytes() throws Exception {
355             ByteArrayOutputStream baos = new ByteArrayOutputStream();
356             baos.write(fcc);
357             baos.write(intBytes(swapInt(cb)));
358             baos.write(intBytes(swapInt(biSize)));
359             baos.write(intBytes(swapInt(biWidth)));
360             baos.write(intBytes(swapInt(biHeight)));
361             baos.write(shortBytes(swapShort(biPlanes)));
362             baos.write(shortBytes(swapShort(biBitCount)));
363             baos.write(biCompression);
364             baos.write(intBytes(swapInt(biSizeImage)));
365             baos.write(intBytes(swapInt(biXPelsPerMeter)));
366             baos.write(intBytes(swapInt(biYPelsPerMeter)));
367             baos.write(intBytes(swapInt(biClrUsed)));
368             baos.write(intBytes(swapInt(biClrImportant)));
369             baos.close();
370 
371             return baos.toByteArray();
372         }
373     }
374 
375     private class AVIMovieList {
376 
377         public byte[] fcc = new byte[]{'L', 'I', 'S', 'T'};
378         public int listSize = 0;
379         public byte[] fcc2 = new byte[]{'m', 'o', 'v', 'i'};
380 
381         // 00db size jpg image data ...
AVIMovieList()382         public AVIMovieList() {
383         }
384 
toBytes()385         public byte[] toBytes() throws Exception {
386             ByteArrayOutputStream baos = new ByteArrayOutputStream();
387             baos.write(fcc);
388             baos.write(intBytes(swapInt(listSize)));
389             baos.write(fcc2);
390             baos.close();
391 
392             return baos.toByteArray();
393         }
394     }
395 
396     private class AVIIndexList {
397 
398         public byte[] fcc = new byte[]{'i', 'd', 'x', '1'};
399         public int cb = 0;
400         public List<AVIIndex> ind = new ArrayList<AVIIndex>();
401 
AVIIndexList()402         public AVIIndexList() {
403         }
404 
405         @SuppressWarnings("unused")
addAVIIndex(AVIIndex ai)406         public void addAVIIndex(AVIIndex ai) {
407             ind.add(ai);
408         }
409 
addAVIIndex(int dwOffset, int dwSize)410         public void addAVIIndex(int dwOffset, int dwSize) {
411             ind.add(new AVIIndex(dwOffset, dwSize));
412         }
413 
toBytes()414         public byte[] toBytes() throws Exception {
415             cb = 16 * ind.size();
416 
417             ByteArrayOutputStream baos = new ByteArrayOutputStream();
418             baos.write(fcc);
419             baos.write(intBytes(swapInt(cb)));
420             for (int i = 0; i < ind.size(); i++) {
421                 AVIIndex in = (AVIIndex) ind.get(i);
422                 baos.write(in.toBytes());
423             }
424 
425             baos.close();
426 
427             return baos.toByteArray();
428         }
429     }
430 
431     private class AVIIndex {
432 
433         public byte[] fcc = new byte[]{'0', '0', 'd', 'b'};
434         public int dwFlags = 16;
435         public int dwOffset = 0;
436         public int dwSize = 0;
437 
AVIIndex(int dwOffset, int dwSize)438         public AVIIndex(int dwOffset, int dwSize) {
439             this.dwOffset = dwOffset;
440             this.dwSize = dwSize;
441         }
442 
toBytes()443         public byte[] toBytes() throws Exception {
444             ByteArrayOutputStream baos = new ByteArrayOutputStream();
445             baos.write(fcc);
446             baos.write(intBytes(swapInt(dwFlags)));
447             baos.write(intBytes(swapInt(dwOffset)));
448             baos.write(intBytes(swapInt(dwSize)));
449             baos.close();
450 
451             return baos.toByteArray();
452         }
453     }
454 
455     private class AVIJunk {
456 
457         public byte[] fcc = new byte[]{'J', 'U', 'N', 'K'};
458         public int size = 1808;
459         public byte[] data = new byte[size];
460 
AVIJunk()461         public AVIJunk() {
462             Arrays.fill(data, (byte) 0);
463         }
464 
toBytes()465         public byte[] toBytes() throws Exception {
466             ByteArrayOutputStream baos = new ByteArrayOutputStream();
467             baos.write(fcc);
468             baos.write(intBytes(swapInt(size)));
469             baos.write(data);
470             baos.close();
471 
472             return baos.toByteArray();
473         }
474     }
475 
writeImageToBytes(Image image)476     public byte[] writeImageToBytes(Image image) throws Exception {
477         BufferedImage bi;
478         if (image instanceof BufferedImage && ((BufferedImage) image).getType() == BufferedImage.TYPE_INT_RGB) {
479             bi = (BufferedImage) image;
480         } else {
481             bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
482             Graphics2D g = bi.createGraphics();
483             g.drawImage(image, 0, 0, width, height, null);
484         }
485         ByteArrayOutputStream baos = new ByteArrayOutputStream();
486         ImageIO.write(bi, "jpg", baos);
487         baos.close();
488         return baos.toByteArray();
489     }
490 }
491