Mercurial > jmeCapture
comparison src/ca/randelshofer/AVIOutputStream.java @ 10:4c5fc53778c1
moved randelshofer stuff to rightfull place, enabled XuggleVideoRecorder
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Wed, 26 Oct 2011 09:38:27 -0700 |
parents | |
children | 784a3f4e6202 |
comparison
equal
deleted
inserted
replaced
9:5dfc9e768816 | 10:4c5fc53778c1 |
---|---|
1 /** | |
2 * @(#)AVIOutputStream.java 1.5.1 2011-01-17 | |
3 * | |
4 * Copyright (c) 2008-2011 Werner Randelshofer, Immensee, Switzerland. | |
5 * All rights reserved. | |
6 * | |
7 * You may not use, copy or modify this file, except in compliance with the | |
8 * license agreement you entered into with Werner Randelshofer. | |
9 * For details see accompanying license terms. | |
10 */ | |
11 package ca.randelshofer; | |
12 | |
13 import java.awt.Dimension; | |
14 import java.awt.image.BufferedImage; | |
15 import java.awt.image.DataBufferByte; | |
16 import java.awt.image.IndexColorModel; | |
17 import java.awt.image.WritableRaster; | |
18 import java.io.File; | |
19 import java.io.FileInputStream; | |
20 import java.io.IOException; | |
21 import java.io.InputStream; | |
22 import java.io.OutputStream; | |
23 import java.util.Arrays; | |
24 import java.util.Date; | |
25 import java.util.LinkedList; | |
26 | |
27 import javax.imageio.IIOImage; | |
28 import javax.imageio.ImageIO; | |
29 import javax.imageio.ImageWriteParam; | |
30 import javax.imageio.ImageWriter; | |
31 import javax.imageio.stream.FileImageOutputStream; | |
32 import javax.imageio.stream.ImageOutputStream; | |
33 import javax.imageio.stream.MemoryCacheImageOutputStream; | |
34 | |
35 /** | |
36 * This class supports writing of images into an AVI 1.0 video file. | |
37 * <p> | |
38 * The images are written as video frames. | |
39 * <p> | |
40 * Video frames can be encoded with one of the following formats: | |
41 * <ul> | |
42 * <li>JPEG</li> | |
43 * <li>PNG</li> | |
44 * <li>RAW</li> | |
45 * <li>RLE</li> | |
46 * </ul> | |
47 * All frames must have the same format. | |
48 * When JPG is used each frame can have an individual encoding quality. | |
49 * <p> | |
50 * All frames in an AVI file must have the same duration. The duration can | |
51 * be set by setting an appropriate pair of values using methods | |
52 * {@link #setFrameRate} and {@link #setTimeScale}. | |
53 * <p> | |
54 * The length of an AVI 1.0 file is limited to 1 GB. | |
55 * This class supports lengths of up to 4 GB, but such files may not work on | |
56 * all players. | |
57 * <p> | |
58 * For detailed information about the AVI RIFF file format see:<br> | |
59 * <a href="http://msdn.microsoft.com/en-us/library/ms779636.aspx">msdn.microsoft.com AVI RIFF</a><br> | |
60 * <a href="http://www.microsoft.com/whdc/archive/fourcc.mspx">www.microsoft.com FOURCC for Video Compression</a><br> | |
61 * <a href="http://www.saettler.com/RIFFMCI/riffmci.html">www.saettler.com RIFF</a><br> | |
62 * | |
63 * @author Werner Randelshofer | |
64 * @version 1.5.1 2011-01-17 Fixes unintended closing of output stream.. | |
65 * <br>1.5 2011-01-06 Adds support for RLE 8-bit video format. | |
66 * <br>1.4 2011-01-04 Adds support for RAW 4-bit and 8-bit video format. Fixes offsets | |
67 * in "idx1" chunk. | |
68 * <br>1.3.2 2010-12-27 File size limit is 1 GB. | |
69 * <br>1.3.1 2010-07-19 Fixes seeking and calculation of offsets. | |
70 * <br>1.3 2010-07-08 Adds constructor with ImageOutputStream. | |
71 * Added method getVideoDimension(). | |
72 * <br>1.2 2009-08-29 Adds support for RAW video format. | |
73 * <br>1.1 2008-08-27 Fixes computation of dwMicroSecPerFrame in avih | |
74 * chunk. Changed the API to reflect that AVI works with frame rates instead of | |
75 * with frame durations. | |
76 * <br>1.0.1 2008-08-13 Uses FourCC "MJPG" instead of "jpg " for JPG | |
77 * encoded video. | |
78 * <br>1.0 2008-08-11 Created. | |
79 */ | |
80 public class AVIOutputStream { | |
81 | |
82 /** | |
83 * Underlying output stream. | |
84 */ | |
85 private ImageOutputStream out; | |
86 /** The offset of the QuickTime stream in the underlying ImageOutputStream. | |
87 * Normally this is 0 unless the underlying stream already contained data | |
88 * when it was passed to the constructor. | |
89 */ | |
90 private long streamOffset; | |
91 /** Previous frame for delta compression. */ | |
92 | |
93 /** | |
94 * Supported video encodings. | |
95 */ | |
96 public static enum VideoFormat { | |
97 | |
98 RAW, RLE, JPG, PNG; | |
99 } | |
100 /** | |
101 * Current video formats. | |
102 */ | |
103 private VideoFormat videoFormat; | |
104 /** | |
105 * Quality of JPEG encoded video frames. | |
106 */ | |
107 private float quality = 0.9f; | |
108 /** | |
109 * Width of the video frames. All frames must have the same width. | |
110 * The value -1 is used to mark unspecified width. | |
111 */ | |
112 private int imgWidth = -1; | |
113 /** | |
114 * Height of the video frames. All frames must have the same height. | |
115 * The value -1 is used to mark unspecified height. | |
116 */ | |
117 private int imgHeight = -1; | |
118 /** Number of bits per pixel. */ | |
119 private int imgDepth = 24; | |
120 /** Index color model for RAW_RGB4 and RAW_RGB8 formats. */ | |
121 private IndexColorModel palette; | |
122 private IndexColorModel previousPalette; | |
123 /** Video encoder. */ | |
124 | |
125 /** | |
126 * The timeScale of the movie. | |
127 * <p> | |
128 * Used with frameRate to specify the time scale that this stream will use. | |
129 * Dividing frameRate by timeScale gives the number of samples per second. | |
130 * For video streams, this is the frame rate. For audio streams, this rate | |
131 * corresponds to the time needed to play nBlockAlign bytes of audio, which | |
132 * for PCM audio is the just the sample rate. | |
133 */ | |
134 private int timeScale = 1; | |
135 /** | |
136 * The frameRate of the movie in timeScale units. | |
137 * <p> | |
138 * @see timeScale | |
139 */ | |
140 private int frameRate = 30; | |
141 /** | |
142 * The states of the movie output stream. | |
143 */ | |
144 private static enum States { | |
145 | |
146 STARTED, FINISHED, CLOSED; | |
147 } | |
148 /** | |
149 * The current state of the movie output stream. | |
150 */ | |
151 private States state = States.FINISHED; | |
152 | |
153 /** | |
154 * AVI stores media data in samples. | |
155 * A sample is a single element in a sequence of time-ordered data. | |
156 */ | |
157 private static class Sample { | |
158 | |
159 String chunkType; | |
160 /** Offset of the sample relative to the start of the AVI file. | |
161 */ | |
162 long offset; | |
163 /** Data length of the sample. */ | |
164 long length; | |
165 /** Whether the sample is a sync-sample. */ | |
166 boolean isSync; | |
167 | |
168 /** | |
169 * Creates a new sample. | |
170 * @param duration | |
171 * @param offset | |
172 * @param length | |
173 */ | |
174 public Sample(String chunkId, int duration, long offset, long length, boolean isSync) { | |
175 this.chunkType = chunkId; | |
176 this.offset = offset; | |
177 this.length = length; | |
178 this.isSync = isSync; | |
179 } | |
180 } | |
181 /** | |
182 * List of video frames. | |
183 */ | |
184 private LinkedList<Sample> videoFrames; | |
185 /** | |
186 * This chunk holds the whole AVI content. | |
187 */ | |
188 private CompositeChunk aviChunk; | |
189 /** | |
190 * This chunk holds the movie frames. | |
191 */ | |
192 private CompositeChunk moviChunk; | |
193 /** | |
194 * This chunk holds the AVI Main Header. | |
195 */ | |
196 FixedSizeDataChunk avihChunk; | |
197 /** | |
198 * This chunk holds the AVI Stream Header. | |
199 */ | |
200 FixedSizeDataChunk strhChunk; | |
201 /** | |
202 * This chunk holds the AVI Stream Format Header. | |
203 */ | |
204 FixedSizeDataChunk strfChunk; | |
205 | |
206 /** | |
207 * Chunk base class. | |
208 */ | |
209 private abstract class Chunk { | |
210 | |
211 /** | |
212 * The chunkType of the chunk. A String with the length of 4 characters. | |
213 */ | |
214 protected String chunkType; | |
215 /** | |
216 * The offset of the chunk relative to the start of the | |
217 * ImageOutputStream. | |
218 */ | |
219 protected long offset; | |
220 | |
221 /** | |
222 * Creates a new Chunk at the current position of the ImageOutputStream. | |
223 * @param chunkType The chunkType of the chunk. A string with a length of 4 characters. | |
224 */ | |
225 public Chunk(String chunkType) throws IOException { | |
226 this.chunkType = chunkType; | |
227 offset = getRelativeStreamPosition(); | |
228 } | |
229 | |
230 /** | |
231 * Writes the chunk to the ImageOutputStream and disposes it. | |
232 */ | |
233 public abstract void finish() throws IOException; | |
234 | |
235 /** | |
236 * Returns the size of the chunk including the size of the chunk header. | |
237 * @return The size of the chunk. | |
238 */ | |
239 public abstract long size(); | |
240 } | |
241 | |
242 /** | |
243 * A CompositeChunk contains an ordered list of Chunks. | |
244 */ | |
245 private class CompositeChunk extends Chunk { | |
246 | |
247 /** | |
248 * The type of the composite. A String with the length of 4 characters. | |
249 */ | |
250 protected String compositeType; | |
251 private LinkedList<Chunk> children; | |
252 private boolean finished; | |
253 | |
254 /** | |
255 * Creates a new CompositeChunk at the current position of the | |
256 * ImageOutputStream. | |
257 * @param compositeType The type of the composite. | |
258 * @param chunkType The type of the chunk. | |
259 */ | |
260 public CompositeChunk(String compositeType, String chunkType) throws IOException { | |
261 super(chunkType); | |
262 this.compositeType = compositeType; | |
263 //out.write | |
264 out.writeLong(0); // make room for the chunk header | |
265 out.writeInt(0); // make room for the chunk header | |
266 children = new LinkedList<Chunk>(); | |
267 } | |
268 | |
269 public void add(Chunk child) throws IOException { | |
270 if (children.size() > 0) { | |
271 children.getLast().finish(); | |
272 } | |
273 children.add(child); | |
274 } | |
275 | |
276 /** | |
277 * Writes the chunk and all its children to the ImageOutputStream | |
278 * and disposes of all resources held by the chunk. | |
279 * @throws java.io.IOException | |
280 */ | |
281 @Override | |
282 public void finish() throws IOException { | |
283 if (!finished) { | |
284 if (size() > 0xffffffffL) { | |
285 throw new IOException("CompositeChunk \"" + chunkType + "\" is too large: " + size()); | |
286 } | |
287 | |
288 long pointer = getRelativeStreamPosition(); | |
289 seekRelative(offset); | |
290 | |
291 DataChunkOutputStream headerData = new DataChunkOutputStream(new ImageOutputStreamAdapter(out),false); | |
292 headerData.writeType(compositeType); | |
293 headerData.writeUInt(size() - 8); | |
294 headerData.writeType(chunkType); | |
295 for (Chunk child : children) { | |
296 child.finish(); | |
297 } | |
298 seekRelative(pointer); | |
299 if (size() % 2 == 1) { | |
300 out.writeByte(0); // write pad byte | |
301 } | |
302 finished = true; | |
303 } | |
304 } | |
305 | |
306 @Override | |
307 public long size() { | |
308 long length = 12; | |
309 for (Chunk child : children) { | |
310 length += child.size() + child.size() % 2; | |
311 } | |
312 return length; | |
313 } | |
314 } | |
315 | |
316 /** | |
317 * Data Chunk. | |
318 */ | |
319 private class DataChunk extends Chunk { | |
320 | |
321 private DataChunkOutputStream data; | |
322 private boolean finished; | |
323 | |
324 /** | |
325 * Creates a new DataChunk at the current position of the | |
326 * ImageOutputStream. | |
327 * @param chunkType The chunkType of the chunk. | |
328 */ | |
329 public DataChunk(String name) throws IOException { | |
330 super(name); | |
331 out.writeLong(0); // make room for the chunk header | |
332 data = new DataChunkOutputStream(new ImageOutputStreamAdapter(out), false); | |
333 } | |
334 | |
335 public DataChunkOutputStream getOutputStream() { | |
336 if (finished) { | |
337 throw new IllegalStateException("DataChunk is finished"); | |
338 } | |
339 return data; | |
340 } | |
341 | |
342 @Override | |
343 public void finish() throws IOException { | |
344 if (!finished) { | |
345 long sizeBefore = size(); | |
346 | |
347 if (size() > 0xffffffffL) { | |
348 throw new IOException("DataChunk \"" + chunkType + "\" is too large: " + size()); | |
349 } | |
350 | |
351 long pointer = getRelativeStreamPosition(); | |
352 seekRelative(offset); | |
353 | |
354 DataChunkOutputStream headerData = new DataChunkOutputStream(new ImageOutputStreamAdapter(out),false); | |
355 headerData.writeType(chunkType); | |
356 headerData.writeUInt(size() - 8); | |
357 seekRelative(pointer); | |
358 if (size() % 2 == 1) { | |
359 out.writeByte(0); // write pad byte | |
360 } | |
361 finished = true; | |
362 long sizeAfter = size(); | |
363 if (sizeBefore != sizeAfter) { | |
364 System.err.println("size mismatch " + sizeBefore + ".." + sizeAfter); | |
365 } | |
366 } | |
367 } | |
368 | |
369 @Override | |
370 public long size() { | |
371 return 8 + data.size(); | |
372 } | |
373 } | |
374 | |
375 /** | |
376 * A DataChunk with a fixed size. | |
377 */ | |
378 private class FixedSizeDataChunk extends Chunk { | |
379 | |
380 private DataChunkOutputStream data; | |
381 private boolean finished; | |
382 private long fixedSize; | |
383 | |
384 /** | |
385 * Creates a new DataChunk at the current position of the | |
386 * ImageOutputStream. | |
387 * @param chunkType The chunkType of the chunk. | |
388 */ | |
389 public FixedSizeDataChunk(String chunkType, long fixedSize) throws IOException { | |
390 super(chunkType); | |
391 this.fixedSize = fixedSize; | |
392 data = new DataChunkOutputStream(new ImageOutputStreamAdapter(out),false); | |
393 data.writeType(chunkType); | |
394 data.writeUInt(fixedSize); | |
395 data.clearCount(); | |
396 | |
397 // Fill fixed size with nulls | |
398 byte[] buf = new byte[(int) Math.min(512, fixedSize)]; | |
399 long written = 0; | |
400 while (written < fixedSize) { | |
401 data.write(buf, 0, (int) Math.min(buf.length, fixedSize - written)); | |
402 written += Math.min(buf.length, fixedSize - written); | |
403 } | |
404 if (fixedSize % 2 == 1) { | |
405 out.writeByte(0); // write pad byte | |
406 } | |
407 seekToStartOfData(); | |
408 } | |
409 | |
410 public DataChunkOutputStream getOutputStream() { | |
411 /*if (finished) { | |
412 throw new IllegalStateException("DataChunk is finished"); | |
413 }*/ | |
414 return data; | |
415 } | |
416 | |
417 public void seekToStartOfData() throws IOException { | |
418 seekRelative(offset + 8); | |
419 data.clearCount(); | |
420 } | |
421 | |
422 public void seekToEndOfChunk() throws IOException { | |
423 seekRelative(offset + 8 + fixedSize + fixedSize % 2); | |
424 } | |
425 | |
426 @Override | |
427 public void finish() throws IOException { | |
428 if (!finished) { | |
429 finished = true; | |
430 } | |
431 } | |
432 | |
433 @Override | |
434 public long size() { | |
435 return 8 + fixedSize; | |
436 } | |
437 } | |
438 | |
439 /** | |
440 * Creates a new AVI file with the specified video format and | |
441 * frame rate. The video has 24 bits per pixel. | |
442 * | |
443 * @param file the output file | |
444 * @param format Selects an encoder for the video format. | |
445 * @param bitsPerPixel the number of bits per pixel. | |
446 * @exception IllegalArgumentException if videoFormat is null or if | |
447 * frame rate is <= 0 | |
448 */ | |
449 public AVIOutputStream(File file, VideoFormat format) throws IOException { | |
450 this(file,format,24); | |
451 } | |
452 /** | |
453 * Creates a new AVI file with the specified video format and | |
454 * frame rate. | |
455 * | |
456 * @param file the output file | |
457 * @param format Selects an encoder for the video format. | |
458 * @param bitsPerPixel the number of bits per pixel. | |
459 * @exception IllegalArgumentException if videoFormat is null or if | |
460 * frame rate is <= 0 | |
461 */ | |
462 public AVIOutputStream(File file, VideoFormat format, int bitsPerPixel) throws IOException { | |
463 if (format == null) { | |
464 throw new IllegalArgumentException("format must not be null"); | |
465 } | |
466 | |
467 if (file.exists()) { | |
468 file.delete(); | |
469 } | |
470 this.out = new FileImageOutputStream(file); | |
471 this.streamOffset = 0; | |
472 this.videoFormat = format; | |
473 this.videoFrames = new LinkedList<Sample>(); | |
474 this.imgDepth = bitsPerPixel; | |
475 if (imgDepth == 4) { | |
476 byte[] gray = new byte[16]; | |
477 for (int i = 0; i < gray.length; i++) { | |
478 gray[i] = (byte) ((i << 4) | i); | |
479 } | |
480 palette = new IndexColorModel(4, 16, gray, gray, gray); | |
481 } else if (imgDepth == 8) { | |
482 byte[] gray = new byte[256]; | |
483 for (int i = 0; i < gray.length; i++) { | |
484 gray[i] = (byte) i; | |
485 } | |
486 palette = new IndexColorModel(8, 256, gray, gray, gray); | |
487 } | |
488 | |
489 } | |
490 | |
491 /** | |
492 * Creates a new AVI output stream with the specified video format and | |
493 * framerate. | |
494 * | |
495 * @param out the underlying output stream | |
496 * @param format Selects an encoder for the video format. | |
497 * @exception IllegalArgumentException if videoFormat is null or if | |
498 * framerate is <= 0 | |
499 */ | |
500 public AVIOutputStream(ImageOutputStream out, VideoFormat format) throws IOException { | |
501 if (format == null) { | |
502 throw new IllegalArgumentException("format must not be null"); | |
503 } | |
504 this.out = out; | |
505 this.streamOffset = out.getStreamPosition(); | |
506 this.videoFormat = format; | |
507 this.videoFrames = new LinkedList<Sample>(); | |
508 } | |
509 | |
510 /** | |
511 * Used with frameRate to specify the time scale that this stream will use. | |
512 * Dividing frameRate by timeScale gives the number of samples per second. | |
513 * For video streams, this is the frame rate. For audio streams, this rate | |
514 * corresponds to the time needed to play nBlockAlign bytes of audio, which | |
515 * for PCM audio is the just the sample rate. | |
516 * <p> | |
517 * The default value is 1. | |
518 * | |
519 * @param newValue | |
520 */ | |
521 public void setTimeScale(int newValue) { | |
522 if (newValue <= 0) { | |
523 throw new IllegalArgumentException("timeScale must be greater 0"); | |
524 } | |
525 this.timeScale = newValue; | |
526 } | |
527 | |
528 /** | |
529 * Returns the time scale of this media. | |
530 * | |
531 * @return time scale | |
532 */ | |
533 public int getTimeScale() { | |
534 return timeScale; | |
535 } | |
536 | |
537 /** | |
538 * Sets the rate of video frames in time scale units. | |
539 * <p> | |
540 * The default value is 30. Together with the default value 1 of timeScale | |
541 * this results in 30 frames pers second. | |
542 * | |
543 * @param newValue | |
544 */ | |
545 public void setFrameRate(int newValue) { | |
546 if (newValue <= 0) { | |
547 throw new IllegalArgumentException("frameDuration must be greater 0"); | |
548 } | |
549 if (state == States.STARTED) { | |
550 throw new IllegalStateException("frameDuration must be set before the first frame is written"); | |
551 } | |
552 this.frameRate = newValue; | |
553 } | |
554 | |
555 /** | |
556 * Returns the frame rate of this media. | |
557 * | |
558 * @return frame rate | |
559 */ | |
560 public int getFrameRate() { | |
561 return frameRate; | |
562 } | |
563 | |
564 /** Sets the global color palette. */ | |
565 public void setPalette(IndexColorModel palette) { | |
566 this.palette = palette; | |
567 } | |
568 | |
569 /** | |
570 * Sets the compression quality of the video track. | |
571 * A value of 0 stands for "high compression is important" a value of | |
572 * 1 for "high image quality is important". | |
573 * <p> | |
574 * Changing this value affects frames which are subsequently written | |
575 * to the AVIOutputStream. Frames which have already been written | |
576 * are not changed. | |
577 * <p> | |
578 * This value has only effect on videos encoded with JPG format. | |
579 * <p> | |
580 * The default value is 0.9. | |
581 * | |
582 * @param newValue | |
583 */ | |
584 public void setVideoCompressionQuality(float newValue) { | |
585 this.quality = newValue; | |
586 } | |
587 | |
588 /** | |
589 * Returns the video compression quality. | |
590 * | |
591 * @return video compression quality | |
592 */ | |
593 public float getVideoCompressionQuality() { | |
594 return quality; | |
595 } | |
596 | |
597 /** | |
598 * Sets the dimension of the video track. | |
599 * <p> | |
600 * You need to explicitly set the dimension, if you add all frames from | |
601 * files or input streams. | |
602 * <p> | |
603 * If you add frames from buffered images, then AVIOutputStream | |
604 * can determine the video dimension from the image width and height. | |
605 * | |
606 * @param width Must be greater than 0. | |
607 * @param height Must be greater than 0. | |
608 */ | |
609 public void setVideoDimension(int width, int height) { | |
610 if (width < 1 || height < 1) { | |
611 throw new IllegalArgumentException("width and height must be greater zero."); | |
612 } | |
613 this.imgWidth = width; | |
614 this.imgHeight = height; | |
615 } | |
616 | |
617 /** | |
618 * Gets the dimension of the video track. | |
619 * <p> | |
620 * Returns null if the dimension is not known. | |
621 */ | |
622 public Dimension getVideoDimension() { | |
623 if (imgWidth < 1 || imgHeight < 1) { | |
624 return null; | |
625 } | |
626 return new Dimension(imgWidth, imgHeight); | |
627 } | |
628 | |
629 /** | |
630 * Sets the state of the QuickTimeOutpuStream to started. | |
631 * <p> | |
632 * If the state is changed by this method, the prolog is | |
633 * written. | |
634 */ | |
635 private void ensureStarted() throws IOException { | |
636 if (state != States.STARTED) { | |
637 new Date(); | |
638 writeProlog(); | |
639 state = States.STARTED; | |
640 } | |
641 } | |
642 | |
643 /** | |
644 * Writes a frame to the video track. | |
645 * <p> | |
646 * If the dimension of the video track has not been specified yet, it | |
647 * is derived from the first buffered image added to the AVIOutputStream. | |
648 * | |
649 * @param image The frame image. | |
650 * | |
651 * @throws IllegalArgumentException if the duration is less than 1, or | |
652 * if the dimension of the frame does not match the dimension of the video | |
653 * track. | |
654 * @throws IOException if writing the image failed. | |
655 */ | |
656 public void writeFrame(BufferedImage image) throws IOException { | |
657 ensureOpen(); | |
658 ensureStarted(); | |
659 | |
660 // Get the dimensions of the first image | |
661 if (imgWidth == -1) { | |
662 imgWidth = image.getWidth(); | |
663 imgHeight = image.getHeight(); | |
664 } else { | |
665 // The dimension of the image must match the dimension of the video track | |
666 if (imgWidth != image.getWidth() || imgHeight != image.getHeight()) { | |
667 throw new IllegalArgumentException("Dimensions of image[" + videoFrames.size() | |
668 + "] (width=" + image.getWidth() + ", height=" + image.getHeight() | |
669 + ") differs from image[0] (width=" | |
670 + imgWidth + ", height=" + imgHeight); | |
671 } | |
672 } | |
673 | |
674 DataChunk videoFrameChunk; | |
675 long offset = getRelativeStreamPosition(); | |
676 boolean isSync = true; | |
677 switch (videoFormat) { | |
678 case RAW: { | |
679 switch (imgDepth) { | |
680 case 4: { | |
681 IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); | |
682 int[] imgRGBs = new int[16]; | |
683 imgPalette.getRGBs(imgRGBs); | |
684 int[] previousRGBs = new int[16]; | |
685 if (previousPalette == null) { | |
686 previousPalette = palette; | |
687 } | |
688 previousPalette.getRGBs(previousRGBs); | |
689 if (!Arrays.equals(imgRGBs, previousRGBs)) { | |
690 previousPalette = imgPalette; | |
691 DataChunk paletteChangeChunk = new DataChunk("00pc"); | |
692 /* | |
693 int first = imgPalette.getMapSize(); | |
694 int last = -1; | |
695 for (int i = 0; i < 16; i++) { | |
696 if (previousRGBs[i] != imgRGBs[i] && i < first) { | |
697 first = i; | |
698 } | |
699 if (previousRGBs[i] != imgRGBs[i] && i > last) { | |
700 last = i; | |
701 } | |
702 }*/ | |
703 int first = 0; | |
704 int last = imgPalette.getMapSize() - 1; | |
705 /* | |
706 * typedef struct { | |
707 BYTE bFirstEntry; | |
708 BYTE bNumEntries; | |
709 WORD wFlags; | |
710 PALETTEENTRY peNew[]; | |
711 } AVIPALCHANGE; | |
712 * | |
713 * typedef struct tagPALETTEENTRY { | |
714 BYTE peRed; | |
715 BYTE peGreen; | |
716 BYTE peBlue; | |
717 BYTE peFlags; | |
718 } PALETTEENTRY; | |
719 */ | |
720 DataChunkOutputStream pOut = paletteChangeChunk.getOutputStream(); | |
721 pOut.writeByte(first);//bFirstEntry | |
722 pOut.writeByte(last - first + 1);//bNumEntries | |
723 pOut.writeShort(0);//wFlags | |
724 | |
725 for (int i = first; i <= last; i++) { | |
726 pOut.writeByte((imgRGBs[i] >>> 16) & 0xff); // red | |
727 pOut.writeByte((imgRGBs[i] >>> 8) & 0xff); // green | |
728 pOut.writeByte(imgRGBs[i] & 0xff); // blue | |
729 pOut.writeByte(0); // reserved*/ | |
730 } | |
731 | |
732 moviChunk.add(paletteChangeChunk); | |
733 paletteChangeChunk.finish(); | |
734 long length = getRelativeStreamPosition() - offset; | |
735 videoFrames.add(new Sample(paletteChangeChunk.chunkType, 0, offset, length - 8, false)); | |
736 offset = getRelativeStreamPosition(); | |
737 } | |
738 | |
739 videoFrameChunk = new DataChunk("00db"); | |
740 byte[] rgb8 = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); | |
741 byte[] rgb4 = new byte[imgWidth / 2]; | |
742 for (int y = (imgHeight - 1) * imgWidth; y >= 0; y -= imgWidth) { // Upside down | |
743 for (int x = 0, xx = 0, n = imgWidth; x < n; x += 2, ++xx) { | |
744 rgb4[xx] = (byte) (((rgb8[y + x] & 0xf) << 4) | (rgb8[y + x + 1] & 0xf)); | |
745 } | |
746 videoFrameChunk.getOutputStream().write(rgb4); | |
747 } | |
748 break; | |
749 } | |
750 case 8: { | |
751 IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); | |
752 int[] imgRGBs = new int[256]; | |
753 imgPalette.getRGBs(imgRGBs); | |
754 int[] previousRGBs = new int[256]; | |
755 if (previousPalette == null) { | |
756 previousPalette = palette; | |
757 } | |
758 previousPalette.getRGBs(previousRGBs); | |
759 if (!Arrays.equals(imgRGBs, previousRGBs)) { | |
760 previousPalette = imgPalette; | |
761 DataChunk paletteChangeChunk = new DataChunk("00pc"); | |
762 /* | |
763 int first = imgPalette.getMapSize(); | |
764 int last = -1; | |
765 for (int i = 0; i < 16; i++) { | |
766 if (previousRGBs[i] != imgRGBs[i] && i < first) { | |
767 first = i; | |
768 } | |
769 if (previousRGBs[i] != imgRGBs[i] && i > last) { | |
770 last = i; | |
771 } | |
772 }*/ | |
773 int first = 0; | |
774 int last = imgPalette.getMapSize() - 1; | |
775 /* | |
776 * typedef struct { | |
777 BYTE bFirstEntry; | |
778 BYTE bNumEntries; | |
779 WORD wFlags; | |
780 PALETTEENTRY peNew[]; | |
781 } AVIPALCHANGE; | |
782 * | |
783 * typedef struct tagPALETTEENTRY { | |
784 BYTE peRed; | |
785 BYTE peGreen; | |
786 BYTE peBlue; | |
787 BYTE peFlags; | |
788 } PALETTEENTRY; | |
789 */ | |
790 DataChunkOutputStream pOut = paletteChangeChunk.getOutputStream(); | |
791 pOut.writeByte(first);//bFirstEntry | |
792 pOut.writeByte(last - first + 1);//bNumEntries | |
793 pOut.writeShort(0);//wFlags | |
794 | |
795 for (int i = first; i <= last; i++) { | |
796 pOut.writeByte((imgRGBs[i] >>> 16) & 0xff); // red | |
797 pOut.writeByte((imgRGBs[i] >>> 8) & 0xff); // green | |
798 pOut.writeByte(imgRGBs[i] & 0xff); // blue | |
799 pOut.writeByte(0); // reserved*/ | |
800 } | |
801 | |
802 moviChunk.add(paletteChangeChunk); | |
803 paletteChangeChunk.finish(); | |
804 long length = getRelativeStreamPosition() - offset; | |
805 videoFrames.add(new Sample(paletteChangeChunk.chunkType, 0, offset, length - 8, false)); | |
806 offset = getRelativeStreamPosition(); | |
807 } | |
808 | |
809 videoFrameChunk = new DataChunk("00db"); | |
810 byte[] rgb8 = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); | |
811 for (int y = (imgHeight - 1) * imgWidth; y >= 0; y -= imgWidth) { // Upside down | |
812 videoFrameChunk.getOutputStream().write(rgb8, y, imgWidth); | |
813 } | |
814 break; | |
815 } | |
816 default: { | |
817 videoFrameChunk = new DataChunk("00db"); | |
818 WritableRaster raster = image.getRaster(); | |
819 int[] raw = new int[imgWidth * 3]; // holds a scanline of raw image data with 3 channels of 32 bit data | |
820 byte[] bytes = new byte[imgWidth * 3]; // holds a scanline of raw image data with 3 channels of 8 bit data | |
821 for (int y = imgHeight - 1; y >= 0; --y) { // Upside down | |
822 raster.getPixels(0, y, imgWidth, 1, raw); | |
823 for (int x = 0, n = imgWidth * 3; x < n; x += 3) { | |
824 bytes[x + 2] = (byte) raw[x]; // Blue | |
825 bytes[x + 1] = (byte) raw[x + 1]; // Green | |
826 bytes[x] = (byte) raw[x + 2]; // Red | |
827 } | |
828 videoFrameChunk.getOutputStream().write(bytes); | |
829 } | |
830 break; | |
831 } | |
832 } | |
833 break; | |
834 } | |
835 | |
836 case JPG: { | |
837 videoFrameChunk = new DataChunk("00dc"); | |
838 ImageWriter iw = (ImageWriter) ImageIO.getImageWritersByMIMEType("image/jpeg").next(); | |
839 ImageWriteParam iwParam = iw.getDefaultWriteParam(); | |
840 iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); | |
841 iwParam.setCompressionQuality(quality); | |
842 MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(videoFrameChunk.getOutputStream()); | |
843 iw.setOutput(imgOut); | |
844 IIOImage img = new IIOImage(image, null, null); | |
845 iw.write(null, img, iwParam); | |
846 iw.dispose(); | |
847 break; | |
848 } | |
849 case PNG: | |
850 default: { | |
851 videoFrameChunk = new DataChunk("00dc"); | |
852 ImageWriter iw = (ImageWriter) ImageIO.getImageWritersByMIMEType("image/png").next(); | |
853 ImageWriteParam iwParam = iw.getDefaultWriteParam(); | |
854 MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(videoFrameChunk.getOutputStream()); | |
855 iw.setOutput(imgOut); | |
856 IIOImage img = new IIOImage(image, null, null); | |
857 iw.write(null, img, iwParam); | |
858 iw.dispose(); | |
859 break; | |
860 } | |
861 } | |
862 long length = getRelativeStreamPosition() - offset; | |
863 moviChunk.add(videoFrameChunk); | |
864 videoFrameChunk.finish(); | |
865 | |
866 videoFrames.add(new Sample(videoFrameChunk.chunkType, frameRate, offset, length - 8, isSync)); | |
867 if (getRelativeStreamPosition() > 1L << 32) { | |
868 throw new IOException("AVI file is larger than 4 GB"); | |
869 } | |
870 } | |
871 | |
872 /** | |
873 * Writes a frame from a file to the video track. | |
874 * <p> | |
875 * This method does not inspect the contents of the file. | |
876 * For example, Its your responsibility to only add JPG files if you have | |
877 * chosen the JPEG video format. | |
878 * <p> | |
879 * If you add all frames from files or from input streams, then you | |
880 * have to explicitly set the dimension of the video track before you | |
881 * call finish() or close(). | |
882 * | |
883 * @param file The file which holds the image data. | |
884 * | |
885 * @throws IllegalStateException if the duration is less than 1. | |
886 * @throws IOException if writing the image failed. | |
887 */ | |
888 public void writeFrame(File file) throws IOException { | |
889 FileInputStream in = null; | |
890 try { | |
891 in = new FileInputStream(file); | |
892 writeFrame(in); | |
893 } finally { | |
894 if (in != null) { | |
895 in.close(); | |
896 } | |
897 } | |
898 } | |
899 | |
900 /** | |
901 * Writes a frame to the video track. | |
902 * <p> | |
903 * This method does not inspect the contents of the file. | |
904 * For example, its your responsibility to only add JPG files if you have | |
905 * chosen the JPEG video format. | |
906 * <p> | |
907 * If you add all frames from files or from input streams, then you | |
908 * have to explicitly set the dimension of the video track before you | |
909 * call finish() or close(). | |
910 * | |
911 * @param in The input stream which holds the image data. | |
912 * | |
913 * @throws IllegalArgumentException if the duration is less than 1. | |
914 * @throws IOException if writing the image failed. | |
915 */ | |
916 public void writeFrame(InputStream in) throws IOException { | |
917 ensureOpen(); | |
918 ensureStarted(); | |
919 | |
920 DataChunk videoFrameChunk = new DataChunk( | |
921 videoFormat == VideoFormat.RAW ? "00db" : "00dc"); | |
922 moviChunk.add(videoFrameChunk); | |
923 OutputStream mdatOut = videoFrameChunk.getOutputStream(); | |
924 long offset = getRelativeStreamPosition(); | |
925 byte[] buf = new byte[512]; | |
926 int len; | |
927 while ((len = in.read(buf)) != -1) { | |
928 mdatOut.write(buf, 0, len); | |
929 } | |
930 long length = getRelativeStreamPosition() - offset; | |
931 videoFrameChunk.finish(); | |
932 videoFrames.add(new Sample(videoFrameChunk.chunkType, frameRate, offset, length - 8, true)); | |
933 if (getRelativeStreamPosition() > 1L << 32) { | |
934 throw new IOException("AVI file is larger than 4 GB"); | |
935 } | |
936 } | |
937 | |
938 /** | |
939 * Closes the movie file as well as the stream being filtered. | |
940 * | |
941 * @exception IOException if an I/O error has occurred | |
942 */ | |
943 public void close() throws IOException { | |
944 if (state == States.STARTED) { | |
945 finish(); | |
946 } | |
947 if (state != States.CLOSED) { | |
948 out.close(); | |
949 state = States.CLOSED; | |
950 } | |
951 } | |
952 | |
953 /** | |
954 * Finishes writing the contents of the AVI output stream without closing | |
955 * the underlying stream. Use this method when applying multiple filters | |
956 * in succession to the same output stream. | |
957 * | |
958 * @exception IllegalStateException if the dimension of the video track | |
959 * has not been specified or determined yet. | |
960 * @exception IOException if an I/O exception has occurred | |
961 */ | |
962 public void finish() throws IOException { | |
963 ensureOpen(); | |
964 if (state != States.FINISHED) { | |
965 if (imgWidth == -1 || imgHeight == -1) { | |
966 throw new IllegalStateException("image width and height must be specified"); | |
967 } | |
968 | |
969 moviChunk.finish(); | |
970 writeEpilog(); | |
971 state = States.FINISHED; | |
972 imgWidth = imgHeight = -1; | |
973 } | |
974 } | |
975 | |
976 /** | |
977 * Check to make sure that this stream has not been closed | |
978 */ | |
979 private void ensureOpen() throws IOException { | |
980 if (state == States.CLOSED) { | |
981 throw new IOException("Stream closed"); | |
982 } | |
983 } | |
984 | |
985 /** Gets the position relative to the beginning of the QuickTime stream. | |
986 * <p> | |
987 * Usually this value is equal to the stream position of the underlying | |
988 * ImageOutputStream, but can be larger if the underlying stream already | |
989 * contained data. | |
990 * | |
991 * @return The relative stream position. | |
992 * @throws IOException | |
993 */ | |
994 private long getRelativeStreamPosition() throws IOException { | |
995 return out.getStreamPosition() - streamOffset; | |
996 } | |
997 | |
998 /** Seeks relative to the beginning of the QuickTime stream. | |
999 * <p> | |
1000 * Usually this equal to seeking in the underlying ImageOutputStream, but | |
1001 * can be different if the underlying stream already contained data. | |
1002 * | |
1003 */ | |
1004 private void seekRelative(long newPosition) throws IOException { | |
1005 out.seek(newPosition + streamOffset); | |
1006 } | |
1007 | |
1008 private void writeProlog() throws IOException { | |
1009 // The file has the following structure: | |
1010 // | |
1011 // .RIFF AVI | |
1012 // ..avih (AVI Header Chunk) | |
1013 // ..LIST strl | |
1014 // ...strh (Stream Header Chunk) | |
1015 // ...strf (Stream Format Chunk) | |
1016 // ..LIST movi | |
1017 // ...00dc (Compressed video data chunk in Track 00, repeated for each frame) | |
1018 // ..idx1 (List of video data chunks and their location in the file) | |
1019 | |
1020 // The RIFF AVI Chunk holds the complete movie | |
1021 aviChunk = new CompositeChunk("RIFF", "AVI "); | |
1022 CompositeChunk hdrlChunk = new CompositeChunk("LIST", "hdrl"); | |
1023 | |
1024 // Write empty AVI Main Header Chunk - we fill the data in later | |
1025 aviChunk.add(hdrlChunk); | |
1026 avihChunk = new FixedSizeDataChunk("avih", 56); | |
1027 avihChunk.seekToEndOfChunk(); | |
1028 hdrlChunk.add(avihChunk); | |
1029 | |
1030 CompositeChunk strlChunk = new CompositeChunk("LIST", "strl"); | |
1031 hdrlChunk.add(strlChunk); | |
1032 | |
1033 // Write empty AVI Stream Header Chunk - we fill the data in later | |
1034 strhChunk = new FixedSizeDataChunk("strh", 56); | |
1035 strhChunk.seekToEndOfChunk(); | |
1036 strlChunk.add(strhChunk); | |
1037 strfChunk = new FixedSizeDataChunk("strf", palette == null ? 40 : 40 + palette.getMapSize() * 4); | |
1038 strfChunk.seekToEndOfChunk(); | |
1039 strlChunk.add(strfChunk); | |
1040 | |
1041 moviChunk = new CompositeChunk("LIST", "movi"); | |
1042 aviChunk.add(moviChunk); | |
1043 | |
1044 | |
1045 } | |
1046 | |
1047 private void writeEpilog() throws IOException { | |
1048 | |
1049 long bufferSize = 0; | |
1050 for (Sample s : videoFrames) { | |
1051 if (s.length > bufferSize) { | |
1052 bufferSize = s.length; | |
1053 } | |
1054 } | |
1055 | |
1056 | |
1057 DataChunkOutputStream d; | |
1058 | |
1059 /* Create Idx1 Chunk and write data | |
1060 * ------------- | |
1061 typedef struct _avioldindex { | |
1062 FOURCC fcc; | |
1063 DWORD cb; | |
1064 struct _avioldindex_entry { | |
1065 DWORD dwChunkId; | |
1066 DWORD dwFlags; | |
1067 DWORD dwOffset; | |
1068 DWORD dwSize; | |
1069 } aIndex[]; | |
1070 } AVIOLDINDEX; | |
1071 */ | |
1072 DataChunk idx1Chunk = new DataChunk("idx1"); | |
1073 aviChunk.add(idx1Chunk); | |
1074 d = idx1Chunk.getOutputStream(); | |
1075 long moviListOffset = moviChunk.offset + 8; | |
1076 //moviListOffset = 0; | |
1077 for (Sample f : videoFrames) { | |
1078 | |
1079 d.writeType(f.chunkType); // dwChunkId | |
1080 // Specifies a FOURCC that identifies a stream in the AVI file. The | |
1081 // FOURCC must have the form 'xxyy' where xx is the stream number and yy | |
1082 // is a two-character code that identifies the contents of the stream: | |
1083 // | |
1084 // Two-character code Description | |
1085 // db Uncompressed video frame | |
1086 // dc Compressed video frame | |
1087 // pc Palette change | |
1088 // wb Audio data | |
1089 | |
1090 d.writeUInt((f.chunkType.endsWith("pc") ? 0x100 : 0x0)// | |
1091 | (f.isSync ? 0x10 : 0x0)); // dwFlags | |
1092 // Specifies a bitwise combination of zero or more of the following | |
1093 // flags: | |
1094 // | |
1095 // Value Name Description | |
1096 // 0x10 AVIIF_KEYFRAME The data chunk is a key frame. | |
1097 // 0x1 AVIIF_LIST The data chunk is a 'rec ' list. | |
1098 // 0x100 AVIIF_NO_TIME The data chunk does not affect the timing of the | |
1099 // stream. For example, this flag should be set for | |
1100 // palette changes. | |
1101 | |
1102 d.writeUInt(f.offset - moviListOffset); // dwOffset | |
1103 // Specifies the location of the data chunk in the file. The value | |
1104 // should be specified as an offset, in bytes, from the start of the | |
1105 // 'movi' list; however, in some AVI files it is given as an offset from | |
1106 // the start of the file. | |
1107 | |
1108 d.writeUInt(f.length); // dwSize | |
1109 // Specifies the size of the data chunk, in bytes. | |
1110 } | |
1111 idx1Chunk.finish(); | |
1112 | |
1113 /* Write Data into AVI Main Header Chunk | |
1114 * ------------- | |
1115 * The AVIMAINHEADER structure defines global information in an AVI file. | |
1116 * see http://msdn.microsoft.com/en-us/library/ms779632(VS.85).aspx | |
1117 typedef struct _avimainheader { | |
1118 FOURCC fcc; | |
1119 DWORD cb; | |
1120 DWORD dwMicroSecPerFrame; | |
1121 DWORD dwMaxBytesPerSec; | |
1122 DWORD dwPaddingGranularity; | |
1123 DWORD dwFlags; | |
1124 DWORD dwTotalFrames; | |
1125 DWORD dwInitialFrames; | |
1126 DWORD dwStreams; | |
1127 DWORD dwSuggestedBufferSize; | |
1128 DWORD dwWidth; | |
1129 DWORD dwHeight; | |
1130 DWORD dwReserved[4]; | |
1131 } AVIMAINHEADER; */ | |
1132 avihChunk.seekToStartOfData(); | |
1133 d = avihChunk.getOutputStream(); | |
1134 | |
1135 d.writeUInt((1000000L * (long) timeScale) / (long) frameRate); // dwMicroSecPerFrame | |
1136 // Specifies the number of microseconds between frames. | |
1137 // This value indicates the overall timing for the file. | |
1138 | |
1139 d.writeUInt(0); // dwMaxBytesPerSec | |
1140 // Specifies the approximate maximum data rate of the file. | |
1141 // This value indicates the number of bytes per second the system | |
1142 // must handle to present an AVI sequence as specified by the other | |
1143 // parameters contained in the main header and stream header chunks. | |
1144 | |
1145 d.writeUInt(0); // dwPaddingGranularity | |
1146 // Specifies the alignment for data, in bytes. Pad the data to multiples | |
1147 // of this value. | |
1148 | |
1149 d.writeUInt(0x10); // dwFlags (0x10 == hasIndex) | |
1150 // Contains a bitwise combination of zero or more of the following | |
1151 // flags: | |
1152 // | |
1153 // Value Name Description | |
1154 // 0x10 AVIF_HASINDEX Indicates the AVI file has an index. | |
1155 // 0x20 AVIF_MUSTUSEINDEX Indicates that application should use the | |
1156 // index, rather than the physical ordering of the | |
1157 // chunks in the file, to determine the order of | |
1158 // presentation of the data. For example, this flag | |
1159 // could be used to create a list of frames for | |
1160 // editing. | |
1161 // 0x100 AVIF_ISINTERLEAVED Indicates the AVI file is interleaved. | |
1162 // 0x1000 AVIF_WASCAPTUREFILE Indicates the AVI file is a specially | |
1163 // allocated file used for capturing real-time | |
1164 // video. Applications should warn the user before | |
1165 // writing over a file with this flag set because | |
1166 // the user probably defragmented this file. | |
1167 // 0x20000 AVIF_COPYRIGHTED Indicates the AVI file contains copyrighted | |
1168 // data and software. When this flag is used, | |
1169 // software should not permit the data to be | |
1170 // duplicated. | |
1171 | |
1172 d.writeUInt(videoFrames.size()); // dwTotalFrames | |
1173 // Specifies the total number of frames of data in the file. | |
1174 | |
1175 d.writeUInt(0); // dwInitialFrames | |
1176 // Specifies the initial frame for interleaved files. Noninterleaved | |
1177 // files should specify zero. If you are creating interleaved files, | |
1178 // specify the number of frames in the file prior to the initial frame | |
1179 // of the AVI sequence in this member. | |
1180 // To give the audio driver enough audio to work with, the audio data in | |
1181 // an interleaved file must be skewed from the video data. Typically, | |
1182 // the audio data should be moved forward enough frames to allow | |
1183 // approximately 0.75 seconds of audio data to be preloaded. The | |
1184 // dwInitialRecords member should be set to the number of frames the | |
1185 // audio is skewed. Also set the same value for the dwInitialFrames | |
1186 // member of the AVISTREAMHEADER structure in the audio stream header | |
1187 | |
1188 d.writeUInt(1); // dwStreams | |
1189 // Specifies the number of streams in the file. For example, a file with | |
1190 // audio and video has two streams. | |
1191 | |
1192 d.writeUInt(bufferSize); // dwSuggestedBufferSize | |
1193 // Specifies the suggested buffer size for reading the file. Generally, | |
1194 // this size should be large enough to contain the largest chunk in the | |
1195 // file. If set to zero, or if it is too small, the playback software | |
1196 // will have to reallocate memory during playback, which will reduce | |
1197 // performance. For an interleaved file, the buffer size should be large | |
1198 // enough to read an entire record, and not just a chunk. | |
1199 | |
1200 | |
1201 d.writeUInt(imgWidth); // dwWidth | |
1202 // Specifies the width of the AVI file in pixels. | |
1203 | |
1204 d.writeUInt(imgHeight); // dwHeight | |
1205 // Specifies the height of the AVI file in pixels. | |
1206 | |
1207 d.writeUInt(0); // dwReserved[0] | |
1208 d.writeUInt(0); // dwReserved[1] | |
1209 d.writeUInt(0); // dwReserved[2] | |
1210 d.writeUInt(0); // dwReserved[3] | |
1211 // Reserved. Set this array to zero. | |
1212 | |
1213 /* Write Data into AVI Stream Header Chunk | |
1214 * ------------- | |
1215 * The AVISTREAMHEADER structure contains information about one stream | |
1216 * in an AVI file. | |
1217 * see http://msdn.microsoft.com/en-us/library/ms779638(VS.85).aspx | |
1218 typedef struct _avistreamheader { | |
1219 FOURCC fcc; | |
1220 DWORD cb; | |
1221 FOURCC fccType; | |
1222 FOURCC fccHandler; | |
1223 DWORD dwFlags; | |
1224 WORD wPriority; | |
1225 WORD wLanguage; | |
1226 DWORD dwInitialFrames; | |
1227 DWORD dwScale; | |
1228 DWORD dwRate; | |
1229 DWORD dwStart; | |
1230 DWORD dwLength; | |
1231 DWORD dwSuggestedBufferSize; | |
1232 DWORD dwQuality; | |
1233 DWORD dwSampleSize; | |
1234 struct { | |
1235 short int left; | |
1236 short int top; | |
1237 short int right; | |
1238 short int bottom; | |
1239 } rcFrame; | |
1240 } AVISTREAMHEADER; | |
1241 */ | |
1242 strhChunk.seekToStartOfData(); | |
1243 d = strhChunk.getOutputStream(); | |
1244 d.writeType("vids"); // fccType - vids for video stream | |
1245 // Contains a FOURCC that specifies the type of the data contained in | |
1246 // the stream. The following standard AVI values for video and audio are | |
1247 // defined: | |
1248 // | |
1249 // FOURCC Description | |
1250 // 'auds' Audio stream | |
1251 // 'mids' MIDI stream | |
1252 // 'txts' Text stream | |
1253 // 'vids' Video stream | |
1254 | |
1255 switch (videoFormat) { | |
1256 case RAW: | |
1257 d.writeType("DIB "); // fccHandler - DIB for Raw RGB | |
1258 break; | |
1259 case RLE: | |
1260 d.writeType("RLE "); // fccHandler - Microsoft RLE | |
1261 break; | |
1262 case JPG: | |
1263 d.writeType("MJPG"); // fccHandler - MJPG for Motion JPEG | |
1264 break; | |
1265 case PNG: | |
1266 default: | |
1267 d.writeType("png "); // fccHandler - png for PNG | |
1268 break; | |
1269 } | |
1270 // Optionally, contains a FOURCC that identifies a specific data | |
1271 // handler. The data handler is the preferred handler for the stream. | |
1272 // For audio and video streams, this specifies the codec for decoding | |
1273 // the stream. | |
1274 | |
1275 if (imgDepth <= 8) { | |
1276 d.writeUInt(0x00010000); // dwFlags - AVISF_VIDEO_PALCHANGES | |
1277 } else { | |
1278 d.writeUInt(0); // dwFlags | |
1279 } | |
1280 | |
1281 // Contains any flags for the data stream. The bits in the high-order | |
1282 // word of these flags are specific to the type of data contained in the | |
1283 // stream. The following standard flags are defined: | |
1284 // | |
1285 // Value Name Description | |
1286 // AVISF_DISABLED 0x00000001 Indicates this stream should not | |
1287 // be enabled by default. | |
1288 // AVISF_VIDEO_PALCHANGES 0x00010000 | |
1289 // Indicates this video stream contains | |
1290 // palette changes. This flag warns the playback | |
1291 // software that it will need to animate the | |
1292 // palette. | |
1293 | |
1294 d.writeUShort(0); // wPriority | |
1295 // Specifies priority of a stream type. For example, in a file with | |
1296 // multiple audio streams, the one with the highest priority might be | |
1297 // the default stream. | |
1298 | |
1299 d.writeUShort(0); // wLanguage | |
1300 // Language tag. | |
1301 | |
1302 d.writeUInt(0); // dwInitialFrames | |
1303 // Specifies how far audio data is skewed ahead of the video frames in | |
1304 // interleaved files. Typically, this is about 0.75 seconds. If you are | |
1305 // creating interleaved files, specify the number of frames in the file | |
1306 // prior to the initial frame of the AVI sequence in this member. For | |
1307 // more information, see the remarks for the dwInitialFrames member of | |
1308 // the AVIMAINHEADER structure. | |
1309 | |
1310 d.writeUInt(timeScale); // dwScale | |
1311 // Used with dwRate to specify the time scale that this stream will use. | |
1312 // Dividing dwRate by dwScale gives the number of samples per second. | |
1313 // For video streams, this is the frame rate. For audio streams, this | |
1314 // rate corresponds to the time needed to play nBlockAlign bytes of | |
1315 // audio, which for PCM audio is the just the sample rate. | |
1316 | |
1317 d.writeUInt(frameRate); // dwRate | |
1318 // See dwScale. | |
1319 | |
1320 d.writeUInt(0); // dwStart | |
1321 // Specifies the starting time for this stream. The units are defined by | |
1322 // the dwRate and dwScale members in the main file header. Usually, this | |
1323 // is zero, but it can specify a delay time for a stream that does not | |
1324 // start concurrently with the file. | |
1325 | |
1326 d.writeUInt(videoFrames.size()); // dwLength | |
1327 // Specifies the length of this stream. The units are defined by the | |
1328 // dwRate and dwScale members of the stream's header. | |
1329 | |
1330 d.writeUInt(bufferSize); // dwSuggestedBufferSize | |
1331 // Specifies how large a buffer should be used to read this stream. | |
1332 // Typically, this contains a value corresponding to the largest chunk | |
1333 // present in the stream. Using the correct buffer size makes playback | |
1334 // more efficient. Use zero if you do not know the correct buffer size. | |
1335 | |
1336 d.writeInt(-1); // dwQuality | |
1337 // Specifies an indicator of the quality of the data in the stream. | |
1338 // Quality is represented as a number between 0 and 10,000. | |
1339 // For compressed data, this typically represents the value of the | |
1340 // quality parameter passed to the compression software. If set to –1, | |
1341 // drivers use the default quality value. | |
1342 | |
1343 d.writeUInt(0); // dwSampleSize | |
1344 // Specifies the size of a single sample of data. This is set to zero | |
1345 // if the samples can vary in size. If this number is nonzero, then | |
1346 // multiple samples of data can be grouped into a single chunk within | |
1347 // the file. If it is zero, each sample of data (such as a video frame) | |
1348 // must be in a separate chunk. For video streams, this number is | |
1349 // typically zero, although it can be nonzero if all video frames are | |
1350 // the same size. For audio streams, this number should be the same as | |
1351 // the nBlockAlign member of the WAVEFORMATEX structure describing the | |
1352 // audio. | |
1353 | |
1354 d.writeUShort(0); // rcFrame.left | |
1355 d.writeUShort(0); // rcFrame.top | |
1356 d.writeUShort(imgWidth); // rcFrame.right | |
1357 d.writeUShort(imgHeight); // rcFrame.bottom | |
1358 // Specifies the destination rectangle for a text or video stream within | |
1359 // the movie rectangle specified by the dwWidth and dwHeight members of | |
1360 // the AVI main header structure. The rcFrame member is typically used | |
1361 // in support of multiple video streams. Set this rectangle to the | |
1362 // coordinates corresponding to the movie rectangle to update the whole | |
1363 // movie rectangle. Units for this member are pixels. The upper-left | |
1364 // corner of the destination rectangle is relative to the upper-left | |
1365 // corner of the movie rectangle. | |
1366 | |
1367 /* Write BITMAPINFOHEADR Data into AVI Stream Format Chunk | |
1368 /* ------------- | |
1369 * see http://msdn.microsoft.com/en-us/library/ms779712(VS.85).aspx | |
1370 typedef struct tagBITMAPINFOHEADER { | |
1371 DWORD biSize; | |
1372 LONG biWidth; | |
1373 LONG biHeight; | |
1374 WORD biPlanes; | |
1375 WORD biBitCount; | |
1376 DWORD biCompression; | |
1377 DWORD biSizeImage; | |
1378 LONG biXPelsPerMeter; | |
1379 LONG biYPelsPerMeter; | |
1380 DWORD biClrUsed; | |
1381 DWORD biClrImportant; | |
1382 } BITMAPINFOHEADER; | |
1383 */ | |
1384 strfChunk.seekToStartOfData(); | |
1385 d = strfChunk.getOutputStream(); | |
1386 d.writeUInt(40); // biSize | |
1387 // Specifies the number of bytes required by the structure. This value | |
1388 // does not include the size of the color table or the size of the color | |
1389 // masks, if they are appended to the end of structure. | |
1390 | |
1391 d.writeInt(imgWidth); // biWidth | |
1392 // Specifies the width of the bitmap, in pixels. | |
1393 | |
1394 d.writeInt(imgHeight); // biHeight | |
1395 // Specifies the height of the bitmap, in pixels. | |
1396 // | |
1397 // For uncompressed RGB bitmaps, if biHeight is positive, the bitmap is | |
1398 // a bottom-up DIB with the origin at the lower left corner. If biHeight | |
1399 // is negative, the bitmap is a top-down DIB with the origin at the | |
1400 // upper left corner. | |
1401 // For YUV bitmaps, the bitmap is always top-down, regardless of the | |
1402 // sign of biHeight. Decoders should offer YUV formats with postive | |
1403 // biHeight, but for backward compatibility they should accept YUV | |
1404 // formats with either positive or negative biHeight. | |
1405 // For compressed formats, biHeight must be positive, regardless of | |
1406 // image orientation. | |
1407 | |
1408 d.writeShort(1); // biPlanes | |
1409 // Specifies the number of planes for the target device. This value must | |
1410 // be set to 1. | |
1411 | |
1412 d.writeShort(imgDepth); // biBitCount | |
1413 // Specifies the number of bits per pixel (bpp). For uncompressed | |
1414 // formats, this value is the average number of bits per pixel. For | |
1415 // compressed formats, this value is the implied bit depth of the | |
1416 // uncompressed image, after the image has been decoded. | |
1417 | |
1418 switch (videoFormat) { | |
1419 case RAW: | |
1420 default: | |
1421 d.writeInt(0); // biCompression - BI_RGB for uncompressed RGB | |
1422 break; | |
1423 case RLE: | |
1424 if (imgDepth == 8) { | |
1425 d.writeInt(1); // biCompression - BI_RLE8 | |
1426 } else if (imgDepth == 4) { | |
1427 d.writeInt(2); // biCompression - BI_RLE4 | |
1428 } else { | |
1429 throw new UnsupportedOperationException("RLE only supports 4-bit and 8-bit images"); | |
1430 } | |
1431 break; | |
1432 case JPG: | |
1433 d.writeType("MJPG"); // biCompression - MJPG for Motion JPEG | |
1434 break; | |
1435 case PNG: | |
1436 d.writeType("png "); // biCompression - png for PNG | |
1437 break; | |
1438 } | |
1439 // For compressed video and YUV formats, this member is a FOURCC code, | |
1440 // specified as a DWORD in little-endian order. For example, YUYV video | |
1441 // has the FOURCC 'VYUY' or 0x56595559. For more information, see FOURCC | |
1442 // Codes. | |
1443 // | |
1444 // For uncompressed RGB formats, the following values are possible: | |
1445 // | |
1446 // Value Description | |
1447 // BI_RGB 0x00000000 Uncompressed RGB. | |
1448 // BI_BITFIELDS 0x00000003 Uncompressed RGB with color masks. | |
1449 // Valid for 16-bpp and 32-bpp bitmaps. | |
1450 // | |
1451 // Note that BI_JPG and BI_PNG are not valid video formats. | |
1452 // | |
1453 // For 16-bpp bitmaps, if biCompression equals BI_RGB, the format is | |
1454 // always RGB 555. If biCompression equals BI_BITFIELDS, the format is | |
1455 // either RGB 555 or RGB 565. Use the subtype GUID in the AM_MEDIA_TYPE | |
1456 // structure to determine the specific RGB type. | |
1457 | |
1458 switch (videoFormat) { | |
1459 case RAW: | |
1460 d.writeInt(0); // biSizeImage | |
1461 break; | |
1462 case RLE: | |
1463 case JPG: | |
1464 case PNG: | |
1465 default: | |
1466 if (imgDepth == 4) { | |
1467 d.writeInt(imgWidth * imgHeight / 2); // biSizeImage | |
1468 } else { | |
1469 int bytesPerPixel = Math.max(1, imgDepth / 8); | |
1470 d.writeInt(imgWidth * imgHeight * bytesPerPixel); // biSizeImage | |
1471 } | |
1472 break; | |
1473 } | |
1474 // Specifies the size, in bytes, of the image. This can be set to 0 for | |
1475 // uncompressed RGB bitmaps. | |
1476 | |
1477 d.writeInt(0); // biXPelsPerMeter | |
1478 // Specifies the horizontal resolution, in pixels per meter, of the | |
1479 // target device for the bitmap. | |
1480 | |
1481 d.writeInt(0); // biYPelsPerMeter | |
1482 // Specifies the vertical resolution, in pixels per meter, of the target | |
1483 // device for the bitmap. | |
1484 | |
1485 d.writeInt(palette == null ? 0 : palette.getMapSize()); // biClrUsed | |
1486 // Specifies the number of color indices in the color table that are | |
1487 // actually used by the bitmap. | |
1488 | |
1489 d.writeInt(0); // biClrImportant | |
1490 // Specifies the number of color indices that are considered important | |
1491 // for displaying the bitmap. If this value is zero, all colors are | |
1492 // important. | |
1493 | |
1494 if (palette != null) { | |
1495 for (int i = 0, n = palette.getMapSize(); i < n; ++i) { | |
1496 /* | |
1497 * typedef struct tagRGBQUAD { | |
1498 BYTE rgbBlue; | |
1499 BYTE rgbGreen; | |
1500 BYTE rgbRed; | |
1501 BYTE rgbReserved; // This member is reserved and must be zero. | |
1502 } RGBQUAD; | |
1503 */ | |
1504 d.write(palette.getBlue(i)); | |
1505 d.write(palette.getGreen(i)); | |
1506 d.write(palette.getRed(i)); | |
1507 d.write(0); | |
1508 } | |
1509 } | |
1510 | |
1511 | |
1512 // ----------------- | |
1513 aviChunk.finish(); | |
1514 } | |
1515 } |