-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAVIWriter.java
More file actions
473 lines (430 loc) · 19.7 KB
/
AVIWriter.java
File metadata and controls
473 lines (430 loc) · 19.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
/**
* @(#)AVIWriter.java
*
* Copyright (c) 2011 Werner Randelshofer, Immensee, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance onlyWith the
* license agreement you entered into onlyWith Werner Randelshofer.
* For details see accompanying license terms.
*/
import java.util.EnumSet;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.io.*;
import java.nio.ByteOrder;
import java.util.Arrays;
import javax.imageio.stream.*;
/**
* Provides high-level support for encoding and writing audio and video samples
* into an AVI 1.0 file.
*
* @author Werner Randelshofer
* @version $Id: AVIWriter.java 192 2012-03-29 22:00:37Z werner $
*/
public class AVIWriter extends AVIOutputStream implements MovieWriter {
public final static Format AVI = new Format(VideoFormatKeys.MediaTypeKey, FormatKeys.MediaType.FILE, VideoFormatKeys.MimeTypeKey, VideoFormatKeys.MIME_AVI);
public final static Format VIDEO_RAW = new Format(
FormatKeys.MediaTypeKey, FormatKeys.MediaType.VIDEO, FormatKeys.MimeTypeKey, FormatKeys.MIME_AVI,
FormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_DIB, VideoFormatKeys.CompressorNameKey, VideoFormatKeys.COMPRESSOR_NAME_QUICKTIME_RAW);
public final static Format VIDEO_JPEG = new Format(
VideoFormatKeys.MediaTypeKey, FormatKeys.MediaType.VIDEO, VideoFormatKeys.MimeTypeKey, VideoFormatKeys.MIME_AVI,
VideoFormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_MJPG, VideoFormatKeys.CompressorNameKey, VideoFormatKeys.COMPRESSOR_NAME_QUICKTIME_RAW);
public final static Format VIDEO_PNG = new Format(
VideoFormatKeys.MediaTypeKey, FormatKeys.MediaType.VIDEO, VideoFormatKeys.MimeTypeKey, VideoFormatKeys.MIME_AVI,
VideoFormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_PNG, VideoFormatKeys.CompressorNameKey, VideoFormatKeys.COMPRESSOR_NAME_QUICKTIME_RAW);
public final static Format VIDEO_RLE = new Format(
VideoFormatKeys.MediaTypeKey, FormatKeys.MediaType.VIDEO, VideoFormatKeys.MimeTypeKey, VideoFormatKeys.MIME_AVI,
VideoFormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_RLE, VideoFormatKeys.CompressorNameKey, VideoFormatKeys.COMPRESSOR_NAME_QUICKTIME_RAW);
public final static Format VIDEO_SCREEN_CAPTURE = new Format(
VideoFormatKeys.MediaTypeKey, FormatKeys.MediaType.VIDEO, VideoFormatKeys.MimeTypeKey, VideoFormatKeys.MIME_AVI,
VideoFormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, VideoFormatKeys.CompressorNameKey, VideoFormatKeys.COMPRESSOR_NAME_QUICKTIME_RAW);
/**
* Creates a new AVI writer.
*
* @param file the output file
*/
public AVIWriter(File file) throws IOException {
super(file);
}
/**
* Creates a new AVI writer.
*
* @param out the output stream.
*/
public AVIWriter(ImageOutputStream out) throws IOException {
super(out);
}
@Override
public Format getFileFormat() throws IOException {
return AVI;
}
@Override
public Format getFormat(int track) {
return tracks.get(track).format;
}
/** Returns the media duration of the track in seconds. */
@Override
public Rational getDuration(int track) {
Track tr=tracks.get(track);
long duration=getMediaDuration(track);
return new Rational(duration*tr.scale,tr.rate);
}
/** Adds a track.
*
* @param format The format of the track.
* @return The track number.
*/
@Override
public int addTrack(Format format) throws IOException {
if (format.get(VideoFormatKeys.MediaTypeKey) == FormatKeys.MediaType.VIDEO) {
return addVideoTrack(format);
} else {
return addAudioTrack(format);
}
}
/** Adds a video track.
*
* @param format The format of the track.
* @return The track number.
*/
private int addVideoTrack(Format vf) throws IOException {
if (!vf.containsKey(VideoFormatKeys.EncodingKey)) {
throw new IllegalArgumentException("EncodingKey missing in "+vf);
}
if (!vf.containsKey(VideoFormatKeys.FrameRateKey)) {
throw new IllegalArgumentException("FrameRateKey missing in "+vf);
}
if (!vf.containsKey(VideoFormatKeys.WidthKey)) {
throw new IllegalArgumentException("WidthKey missing in "+vf);
}
if (!vf.containsKey(VideoFormatKeys.HeightKey)) {
throw new IllegalArgumentException("HeightKey missing in "+vf);
}
if (!vf.containsKey(VideoFormatKeys.DepthKey)) {
throw new IllegalArgumentException("DepthKey missing in "+vf);
}
return addVideoTrack(vf.get(VideoFormatKeys.EncodingKey),
vf.get(VideoFormatKeys.FrameRateKey).getDenominator(), vf.get(VideoFormatKeys.FrameRateKey).getNumerator(),
vf.get(VideoFormatKeys.WidthKey), vf.get(VideoFormatKeys.HeightKey), vf.get(VideoFormatKeys.DepthKey),
vf.get(VideoFormatKeys.FrameRateKey).floor(1).intValue());
}
/** Adds an audio track.
*
* @param format The format of the track.
* @return The track number.
*/
private int addAudioTrack(Format format) throws IOException {
int waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
long timeScale = 1;
long sampleRate = format.get(AudioFormatKeys.SampleRateKey, new Rational(41000, 0)).longValue();
int numberOfChannels = format.get(AudioFormatKeys.ChannelsKey, 1);
int sampleSizeInBits = format.get(AudioFormatKeys.SampleSizeInBitsKey, 16); //
boolean isCompressed = false; // FIXME
int frameDuration = 1;
int frameSize = format.get(AudioFormatKeys.FrameSizeKey, (sampleSizeInBits + 7) / 8 * numberOfChannels);
String enc = format.get(AudioFormatKeys.EncodingKey);
if (enc == null) {
waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
} else if (enc.equals(AudioFormatKeys.ENCODING_ALAW)) {
waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
} else if (enc.equals(AudioFormatKeys.ENCODING_PCM_SIGNED)) {
waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
} else if (enc.equals(AudioFormatKeys.ENCODING_PCM_UNSIGNED)) {
waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
} else if (enc.equals(AudioFormatKeys.ENCODING_ULAW)) {
waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
} else if (enc.equals(AudioFormatKeys.ENCODING_MP3)) {
waveFormatTag = 0x0001; // WAVE_FORMAT_PCM - FIXME
} else {
waveFormatTag = RIFFParser.stringToID(format.get(AudioFormatKeys.EncodingKey)) & 0xffff;
}
return addAudioTrack(waveFormatTag, //
timeScale, sampleRate, //
numberOfChannels, sampleSizeInBits, //
isCompressed, //
frameDuration, frameSize);
}
/** Returns the codec of the specified track. */
public Codec getCodec(int track) {
return tracks.get(track).codec;
}
/** Sets the codec for the specified track. */
public void setCodec(int track, Codec codec) {
tracks.get(track).codec = codec;
}
@Override
public int getTrackCount() {
return tracks.size();
}
/**
* Encodes the provided image and writes its sample data into the specified track.
*
* @param track The track index.
* @param image The image of the video frame.
* @param duration Duration given in media time units.
*
* @throws IndexOutofBoundsException if the track index is out of bounds.
* @throws if the duration is less than 1, or if the dimension of the frame
* does not match the dimension of the video.
* @throws UnsupportedOperationException if the {@code MovieWriter} does not have
* a built-in encoder for this video format.
* @throws IOException if writing the sample data failed.
*/
public void write(int track, BufferedImage image, long duration) throws IOException {
ensureStarted();
VideoTrack vt = (VideoTrack) tracks.get(track);
if (vt.codec == null) {
createCodec(vt);
}
if (vt.codec == null) {
throw new UnsupportedOperationException("No codec for this format: "+vt.format);
}
// The dimension of the image must match the dimension of the video track
Format fmt = vt.format;
if (fmt.get(VideoFormatKeys.WidthKey) != image.getWidth() || fmt.get(VideoFormatKeys.HeightKey) != image.getHeight()) {
throw new IllegalArgumentException("Dimensions of image[" + vt.samples.size()
+ "] (width=" + image.getWidth() + ", height=" + image.getHeight()
+ ") differs from video format of track: " + fmt);
}
// Encode pixel data
{
if (vt.outputBuffer == null) {
vt.outputBuffer = new Buffer();
}
boolean isKeyframe = vt.syncInterval == 0 ? false : vt.samples.size() % vt.syncInterval == 0;
Buffer inputBuffer = new Buffer();
inputBuffer.flags = (isKeyframe) ? EnumSet.of(BufferFlag.KEYFRAME) : EnumSet.noneOf(BufferFlag.class);
inputBuffer.data = image;
vt.codec.process(inputBuffer, vt.outputBuffer);
if (vt.outputBuffer.flags.contains(BufferFlag.DISCARD)) {
return;
}
// Encode palette data
isKeyframe = vt.outputBuffer.flags.contains(BufferFlag.KEYFRAME);
boolean paletteChange = writePalette(track, image, isKeyframe);
writeSample(track, (byte[])vt.outputBuffer.data,vt.outputBuffer.offset,vt.outputBuffer.length, isKeyframe&&!paletteChange);
/*
long offset = getRelativeStreamPosition();
DataChunk videoFrameChunk = new DataChunk(vt.getSampleChunkFourCC(isKeyframe));
moviChunk.add(videoFrameChunk);
videoFrameChunk.getOutputStream().write((byte[]) vt.outputBuffer.data, vt.outputBuffer.offset, vt.outputBuffer.length);
videoFrameChunk.finish();
long length = getRelativeStreamPosition() - offset;
Sample s=new Sample(videoFrameChunk.chunkType, 1, offset, length, isKeyframe&&!paletteChange);
vt.addSample(s);
idx1.add(s);
if (getRelativeStreamPosition() > 1L << 32) {
throw new IOException("AVI file is larger than 4 GB");
}*/
}
}
/** Encodes the data provided in the buffer and then writes it into
* the specified track.
* <p>
* Does nothing if the discard-flag in the buffer is set to true.
*
* @param track The track number.
* @param buf The buffer containing a data sample.
*/
@Override
public void write(int track, Buffer buf) throws IOException {
ensureStarted();
if (buf.flags.contains(BufferFlag.DISCARD)) {
return;
}
Track tr = tracks.get(track);
boolean isKeyframe = buf.flags.contains(BufferFlag.KEYFRAME);
if (buf.data instanceof BufferedImage) {
if (tr.syncInterval != 0) {
isKeyframe = buf.flags.contains(BufferFlag.KEYFRAME) | (tr.samples.size() % tr.syncInterval == 0);
}
}
// Encode palette data
boolean paletteChange = false;
if (buf.data instanceof BufferedImage && tr instanceof VideoTrack) {
paletteChange = writePalette(track, (BufferedImage) buf.data, isKeyframe);
} else if (buf.header instanceof IndexColorModel) {
paletteChange = writePalette(track, (IndexColorModel) buf.header, isKeyframe);
}
// Encode sample data
{
if (buf.format.removeKeys(VideoFormatKeys.FrameRateKey).matches(tr.format) && buf.data instanceof byte[]) {
writeSamples(track, buf.sampleCount, (byte[]) buf.data, buf.offset, buf.length,
buf.isFlag(BufferFlag.KEYFRAME) && !paletteChange);
return;
}
// We got here, because the buffer format does not match the track
// format. Lets see if we can create a codec which can perform the
// encoding for us.
if (tr.codec == null) {
createCodec(tr);
if (tr.codec == null) {
throw new UnsupportedOperationException("No codec for this format " + tr.format + "(" + typeToInt(tr.format.get(FormatKeys.EncodingKey)) + ")");
}
}
if (tr.outputBuffer == null) {
tr.outputBuffer = new Buffer();
}
Buffer outBuf = tr.outputBuffer;
if (tr.codec.process(buf, outBuf) != Codec.CODEC_OK) {
throw new IOException("Codec failed or could not encode the sample in a single step.");
}
if (outBuf.isFlag(BufferFlag.DISCARD)) {
return;
}
writeSamples(track, outBuf.sampleCount, (byte[]) outBuf.data, outBuf.offset, outBuf.length,
isKeyframe&&!paletteChange);
}
}
private boolean writePalette(int track, BufferedImage image, boolean isKeyframe) throws IOException {
if ((image.getColorModel() instanceof IndexColorModel)) {
return writePalette(track, (IndexColorModel) image.getColorModel(), isKeyframe);
}
return false;
}
private boolean writePalette(int track, IndexColorModel imgPalette, boolean isKeyframe) throws IOException {
ensureStarted();
VideoTrack vt = (VideoTrack) tracks.get(track);
int imgDepth = vt.bitCount;
ByteArrayImageOutputStream tmp = null;
boolean paletteChange=false;
switch (imgDepth) {
case 4: {
//IndexColorModel imgPalette = (IndexColorModel) image.getColorModel();
int[] imgRGBs = new int[16];
imgPalette.getRGBs(imgRGBs);
int[] previousRGBs = new int[16];
if (vt.previousPalette == null) {
vt.previousPalette = vt.palette;
}
vt.previousPalette.getRGBs(previousRGBs);
if (isKeyframe || !Arrays.equals(imgRGBs, previousRGBs)) {
paletteChange=true;
vt.previousPalette = imgPalette;
/*
int first = imgPalette.getMapSize();
int last = -1;
for (int i = 0; i < 16; i++) {
if (previousRGBs[i] != imgRGBs[i] && i < first) {
first = i;
}
if (previousRGBs[i] != imgRGBs[i] && i > last) {
last = i;
}
}*/
int first = 0;
int last = imgPalette.getMapSize() - 1;
/*
* typedef struct {
BYTE bFirstEntry;
BYTE bNumEntries;
WORD wFlags;
PALETTEENTRY peNew[];
} AVIPALCHANGE;
*
* typedef struct tagPALETTEENTRY {
BYTE peRed;
BYTE peGreen;
BYTE peBlue;
BYTE peFlags;
} PALETTEENTRY;
*/
tmp = new ByteArrayImageOutputStream(ByteOrder.LITTLE_ENDIAN);
tmp.writeByte(first);//bFirstEntry
tmp.writeByte(last - first + 1);//bNumEntries
tmp.writeShort(0);//wFlags
for (int i = first; i <= last; i++) {
tmp.writeByte((imgRGBs[i] >>> 16) & 0xff); // red
tmp.writeByte((imgRGBs[i] >>> 8) & 0xff); // green
tmp.writeByte(imgRGBs[i] & 0xff); // blue
tmp.writeByte(0); // reserved*/
}
}
break;
}
case 8: {
//IndexColorModel imgPalette = (IndexColorModel) image.getColorModel();
int[] imgRGBs = new int[256];
imgPalette.getRGBs(imgRGBs);
int[] previousRGBs = new int[256];
if (vt.previousPalette != null) {
vt.previousPalette.getRGBs(previousRGBs);
}
if (isKeyframe||!Arrays.equals(imgRGBs, previousRGBs)) {
paletteChange=true;
vt.previousPalette = imgPalette;
/*
int first = imgPalette.getMapSize();
int last = -1;
for (int i = 0; i < 16; i++) {
if (previousRGBs[i] != imgRGBs[i] && i < first) {
first = i;
}
if (previousRGBs[i] != imgRGBs[i] && i > last) {
last = i;
}
}*/
int first = 0;
int last = imgPalette.getMapSize() - 1;
/*
* typedef struct {
BYTE bFirstEntry;
BYTE bNumEntries;
WORD wFlags;
PALETTEENTRY peNew[];
} AVIPALCHANGE;
*
* typedef struct tagPALETTEENTRY {
BYTE peRed;
BYTE peGreen;
BYTE peBlue;
BYTE peFlags;
} PALETTEENTRY;
*/
tmp = new ByteArrayImageOutputStream(ByteOrder.LITTLE_ENDIAN);
tmp.writeByte(first);//bFirstEntry
tmp.writeByte(last - first + 1);//bNumEntries
tmp.writeShort(0);//wFlags
for (int i = first; i <= last; i++) {
tmp.writeByte((imgRGBs[i] >>> 16) & 0xff); // red
tmp.writeByte((imgRGBs[i] >>> 8) & 0xff); // green
tmp.writeByte(imgRGBs[i] & 0xff); // blue
tmp.writeByte(0); // reserved*/
}
}
break;
}
}
if (tmp != null) {
tmp.close();
writePalette(track, tmp.toByteArray(), 0, (int) tmp.length(), isKeyframe);
}
return paletteChange;
}
private Codec createCodec(Format fmt) {
Codec[] codecs = Registry.getInstance().getEncoders(new Format(FormatKeys.MimeTypeKey, FormatKeys.MIME_AVI).append(fmt));
return codecs.length == 0 ? null : codecs[0];
}
private void createCodec(Track tr) {
Format fmt = tr.format;
tr.codec = createCodec(fmt);
String enc = fmt.get(FormatKeys.EncodingKey);
if (tr.codec != null) {
if (fmt.get(FormatKeys.MediaTypeKey) == FormatKeys.MediaType.VIDEO) {
tr.codec.setInputFormat(new Format(FormatKeys.EncodingKey, VideoFormatKeys.ENCODING_BUFFERED_IMAGE, VideoFormatKeys.DataClassKey, BufferedImage.class).append(fmt));
if (null == tr.codec.setOutputFormat(new Format(VideoFormatKeys.FixedFrameRateKey, true).append(fmt))) {
throw new UnsupportedOperationException("Track " + tr + " codec does not support format " + fmt + ". codec=" + tr.codec);
}
} else {
tr.codec.setInputFormat(null);
if (null == tr.codec.setOutputFormat(fmt)) {
throw new UnsupportedOperationException("Track " + tr + " codec " + tr.codec + " does not support format. " + fmt);
}
}
}
}
public boolean isVFRSupported() {
return false;
}
}