1package com.jme3.app.state;
2
3import java.awt.Graphics2D;
4import java.awt.Image;
5import java.awt.image.BufferedImage;
6import java.io.ByteArrayOutputStream;
7import java.io.File;
8import java.io.FileOutputStream;
9import java.io.RandomAccessFile;
10import java.nio.channels.FileChannel;
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.List;
14import javax.imageio.ImageIO;
15
16/**
17 * Released under BSD License
18 * @author monceaux, normenhansen
19 */
20public 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
33    public MjpegFileWriter(File aviFile, int width, int height, double framerate) throws Exception {
34        this(aviFile, width, height, framerate, 0);
35    }
36
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
58    public void addImage(Image image) throws Exception {
59        addImage(writeImageToBytes(image));
60    }
61
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
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    // }
108    public static int swapInt(int v) {
109        return (v >>> 24) | (v << 24) | ((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00);
110    }
111
112    public static short swapShort(short v) {
113        return (short) ((v >>> 8) | (v << 8));
114    }
115
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
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
143        public RIFFHeader() {
144        }
145
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
199        public AVIMainHeader() {
200            dwMicroSecPerFrame = (int) ((1.0 / framerate) * 1000000.0);
201            dwWidth = width;
202            dwHeight = height;
203            dwTotalFrames = numFrames;
204        }
205
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
236        public AVIStreamList() {
237        }
238
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
288        public AVIStreamHeader() {
289            dwScale = (int) ((1.0 / framerate) * 1000000.0);
290            dwLength = numFrames;
291        }
292
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
348        public AVIStreamFormat() {
349            biWidth = width;
350            biHeight = height;
351            biSizeImage = width * height;
352        }
353
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 ...
382        public AVIMovieList() {
383        }
384
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
402        public AVIIndexList() {
403        }
404
405        @SuppressWarnings("unused")
406        public void addAVIIndex(AVIIndex ai) {
407            ind.add(ai);
408        }
409
410        public void addAVIIndex(int dwOffset, int dwSize) {
411            ind.add(new AVIIndex(dwOffset, dwSize));
412        }
413
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
438        public AVIIndex(int dwOffset, int dwSize) {
439            this.dwOffset = dwOffset;
440            this.dwSize = dwSize;
441        }
442
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
461        public AVIJunk() {
462            Arrays.fill(data, (byte) 0);
463        }
464
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
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