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 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