com.seleniumtests.util.video.avi.AVIInputStream Maven / Gradle / Ivy
/*
* @(#)AVIInputStream.java
*
* Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package com.seleniumtests.util.video.avi;
import org.monte.media.AbortException;
import org.monte.media.Format;
import org.monte.media.ParseException;
import org.monte.media.io.ByteArrayImageInputStream;
import org.monte.media.math.Rational;
import org.monte.media.riff.RIFFChunk;
import org.monte.media.riff.RIFFParser;
import org.monte.media.riff.RIFFVisitor;
import java.awt.Dimension;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import static org.monte.media.AudioFormatKeys.*;
import static org.monte.media.VideoFormatKeys.*;
/**
* Provides low-level support for reading encoded audio and video samples from
* an AVI 1.0 or an AVI 2.0 file.
*
* The length of an AVI 1.0 file is limited to 1 GB.
* This class supports lengths of up to 4 GB, but such files may not work on
* all players.
*
* Support for AVI 2.0 file is incomplete.
* This class currently ignores the extended index chunks. Instead all chunks
* in the "movi" list are scanned. With scanning, the reader is not able to
* distinguish between keyframes and non-keyframes. As a consequence opening an
* AVI 2.0 file is very slow, and decoding of frames may fail.
*
* For detailed information about the AVI 1.0 file format see:
* msdn.microsoft.com AVI RIFF
* www.microsoft.com FOURCC for Video Compression
* www.saettler.com RIFF
*
* For detailed information about the AVI 2.0 file format see:
* OpenDML AVI File Format Extensions, Version 1.02
*
* @author Werner Randelshofer
* @version $Id: AVIInputStream.java 299 2013-01-03 07:40:18Z werner $
*/
public class AVIInputStream extends AbstractAVIStream {
/**
* The image input stream.
*/
protected final ImageInputStream in;
/**
* This variable is set to true when all meta-data has been read from the
* file.
*/
private boolean isRealized = false;
protected MainHeader mainHeader;
protected ArrayList idx1 = new ArrayList();
private long moviOffset = 0;
/**
* Creates a new instance.
*
* @param file the input file
*/
public AVIInputStream(File file) throws IOException {
this.in = new FileImageInputStream(file);
in.setByteOrder(ByteOrder.LITTLE_ENDIAN);
this.streamOffset = 0;
}
/**
* Creates a new instance.
*
* @param in the input stream.
*/
public AVIInputStream(ImageInputStream in) throws IOException {
this.in = in;
this.streamOffset = in.getStreamPosition();
in.setByteOrder(ByteOrder.LITTLE_ENDIAN);
}
/**
* Ensures that all meta-data has been read from the file.
*/
protected void ensureRealized() throws IOException {
if (!isRealized) {
isRealized = true;
readAllMetadata();
}
if (mainHeader == null) {
throw new IOException("AVI main header missing.");
}
}
/**
* Returns the main header flags. The flags are an or-combination of the
* {@code AVIH_...} values.
*/
public int getHeaderFlags() throws IOException {
ensureRealized();
return mainHeader.flags;
}
public Dimension getVideoDimension() throws IOException {
ensureRealized();
return (Dimension) mainHeader.size.clone();
}
public int getTrackCount() throws IOException {
ensureRealized();
return tracks.size();
}
/**
* Returns the number of microseconds (10^-6 seconds) per frame. This is
* used as a time basis for the start time of tracks within a movie.
*/
public long getMicroSecPerFrame() throws IOException {
ensureRealized();
return mainHeader.microSecPerFrame;
}
/**
* Returns the time scale of the specified track.
*/
public long getTimeScale(int track) throws IOException {
ensureRealized();
return tracks.get(track).scale;
}
/**
* Returns the start time of the track given as the number of frames in
* microSecPerFrame units.
*/
public long getStartTime(int track) throws IOException {
ensureRealized();
return tracks.get(track).startTime;
}
/**
* Returns the number of media data chunks in the track. This includes
* chunks which do not affect the timing of the media, such as palette
* changes.
*
* @param track
* @return the number of chunks
* @throws IOException
*/
public long getChunkCount(int track) throws IOException {
ensureRealized();
return tracks.get(track).samples.size();
}
/**
* Returns the name of the track, or null if the name is not specified.
*/
public String getName(int track) throws IOException {
ensureRealized();
return tracks.get(track).name;
}
/**
* Returns the contents of the extra track header. Returns null if the
* header is not present.
*
* @param track
* @param fourcc
* @return The extra header as a byte array
* @throws IOException
*/
public byte[] getExtraHeader(int track, String fourcc) throws IOException {
ensureRealized();
int id = RIFFParser.stringToID(fourcc);
for (RIFFChunk c : tracks.get(track).extraHeaders) {
if (c.getID() == id) {
return c.getData();
}
}
return null;
}
/**
* Returns the fourcc's of all extra stream headers.
*
* @param track
* @return An array of fourcc's of all extra stream headers.
* @throws IOException
*/
public String[] getExtraHeaderFourCCs(int track) throws IOException {
Track tr = tracks.get(track);
String[] fourccs = new String[tr.extraHeaders.size()];
for (int i = 0; i < fourccs.length; i++) {
fourccs[i] = RIFFParser.idToString(tr.extraHeaders.get(i).getID());
}
return fourccs;
}
/**
* Reads all metadata of the file.
*/
protected void readAllMetadata() throws IOException {
in.seek(streamOffset);
final RIFFParser p = new RIFFParser();
//p.declareStopChunkType(MOVI_ID);
//p.declareStopChunkType(REC_ID);
try {
RIFFVisitor v = new RIFFVisitor() {
private Track currentTrack;
@Override
public boolean enteringGroup(RIFFChunk group) {
//System.out.println("AVIInputStream enteringGroup " + group + " 0x" + Integer.toHexString(group.getType()) + " 0x" + Integer.toHexString(group.getID()));
if (group.getType() == MOVI_ID) {
moviOffset = group.getScan() + 8;
}
if (group.getType() == MOVI_ID && group.getID() == LIST_ID) {
if (mainHeader != null
&& (mainHeader.flags & AVIH_FLAG_HAS_INDEX) != 0
&& p.getStreamOffset() == 0) {
// => skip movi list if an index is available
//System.out.println("AVIInputStream skipping movi list.");
return false;
}
}
return true;
}
@Override
public void enterGroup(RIFFChunk group) throws ParseException, AbortException {
//System.out.println("AVIInputStream enterGroup " + group);
}
@Override
public void leaveGroup(RIFFChunk group) throws ParseException, AbortException {
//System.out.println("AVIInputStream leaveGroup " + group);
if (group.getType() == HDRL_ID) {
currentTrack = null;
}
}
@Override
public void visitChunk(RIFFChunk group, RIFFChunk chunk) throws ParseException, AbortException {
try {
//System.out.print("group " + intToType(group.getType()) + " " + intToType(group.getID()));
//System.out.println(" chunk " + intToType(chunk.getType()) + " " + intToType(chunk.getID()));
switch (chunk.getType()) {
case HDRL_ID:
switch (chunk.getID()) {
case AVIH_ID:
mainHeader = readAVIH(chunk.getData());
break;
default:
break;
}
break;
case STRL_ID:
// FIXME - The code below depends too much on
// the sequence of the chunks in the STRL.
// We should just collect all chunks in the STRL
// and process them when we leave the STRL.
switch (chunk.getID()) {
case STRH_ID:
currentTrack = readSTRH(chunk.getData());
tracks.add(currentTrack);
break;
case STRF_ID:
switch (currentTrack.mediaType) {
case AUDIO:
readAudioSTRF((AudioTrack) currentTrack, chunk.getData());
break;
case VIDEO:
readVideoSTRF((VideoTrack) currentTrack, chunk.getData());
break;
default:
throw new ParseException("Unsupported media type:" + currentTrack.mediaType);
}
break;
case STRN_ID:
readSTRN(currentTrack, chunk.getData());
break;
default:
currentTrack.extraHeaders.add(chunk);
break;
}
break;
case AVI_ID:
switch (chunk.getID()) {
case IDX1_ID:
if (isFlagSet(mainHeader.flags, AVIH_FLAG_HAS_INDEX)) {
readIDX1(tracks, idx1, chunk.getData());
}
break;
default:
break;
}
break;
case MOVI_ID:
// fall through
case REC_ID: {
int chunkIdInt = chunk.getID();
int id = chunkIdInt;
int track = (((chunkIdInt >> 24) & 0xff) - '0') * 10 + (((chunkIdInt >>> 16) & 0xff) - '0');
if (track >= 0 && track < tracks.size()) {
Track tr=tracks.get(track);
Sample s = new Sample(id, (id & 0xffff) == PC_ID ? 0 : 1, chunk.getScan(), chunk.getSize(), false);
// Audio chunks may contain multiple samples
if (tr.format.get(MediaTypeKey)==MediaType.AUDIO) {
s.duration=(int)(s.length/(tr.format.get(FrameSizeKey)*tr.format.get(ChannelsKey)));
}
// The first chunk and all uncompressed chunks are keyframes
s.isKeyframe=tr.samples.isEmpty()||(id & 0xffff) == WB_ID||(id & 0xffff) == DB_ID;
if (tr.samples.size()>0) {
Sample lastSample=tr.samples.get(tr.samples.size()-1);
s.timeStamp = lastSample.timeStamp+lastSample.duration;
}
tr.length=s.timeStamp+s.duration;
idx1.add(s);
tr.samples.add(s);
}
}
break;
default:
break;
}
// System.out.println("AVIInputStream visitChunk " + group + " " + chunk);
} catch (IOException ex) {
throw new ParseException("Error parsing " + RIFFParser.idToString(group.getID()) + "." + RIFFParser.idToString(chunk.getID()), ex);
}
}
};
// Parse all RIFF structures in the file
int count = 0;
while (true) {
long offset = p.parse(in, v);
p.setStreamOffset(offset);
count++;
}
} catch (EOFException ex) {
//ex.printStackTrace();
} catch (ParseException ex) {
throw new IOException("Error Parsing AVI stream", ex);
} catch (AbortException ex) {
throw new IOException("Parsing aborted", ex);
}
}
/**
* Reads the AVI Main Header and returns a MainHeader object.
*/
private MainHeader readAVIH(byte[] data) throws IOException {
try (
ByteArrayImageInputStream inputStream = new ByteArrayImageInputStream(data, ByteOrder.LITTLE_ENDIAN);) {
MainHeader mh = new MainHeader();
mh.microSecPerFrame = inputStream.readUnsignedInt();
mh.maxBytesPerSec = inputStream.readUnsignedInt();
mh.paddingGranularity = inputStream.readUnsignedInt();
mh.flags = inputStream.readInt();
mh.totalFrames = inputStream.readUnsignedInt();
mh.initialFrames = inputStream.readUnsignedInt();
mh.streams = inputStream.readUnsignedInt();
mh.suggestedBufferSize = inputStream.readUnsignedInt();
mh.size = new Dimension(inputStream.readInt(), inputStream.readInt());
return mh;
}
}
/**
* Reads an AVI Stream Header and returns a Track object.
*/
/*typedef struct {
* FOURCC enum aviStrhType type;
* // Contains a FOURCC that specifies the type of the data contained in
* // the stream. The following standard AVI values for video and audio are
* // defined.
* FOURCC handler;
* DWORD set aviStrhFlags flags;
* WORD priority;
* WORD language;
* DWORD initialFrames;
* DWORD scale;
* DWORD rate;
* DWORD startTime;
* DWORD length;
* DWORD suggestedBufferSize;
* DWORD quality;
* DWORD sampleSize;
* aviRectangle frame;
* } AVISTREAMHEADER; */
private Track readSTRH(byte[] data) throws IOException, ParseException {
try (
ByteArrayImageInputStream inputStream = new ByteArrayImageInputStream(data, ByteOrder.LITTLE_ENDIAN);) {
Track tr = null;
inputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
String type = intToType(inputStream.readInt());
inputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
int handler = inputStream.readInt();
if (type.equals(AVIMediaType.AUDIO.fccType)) {
tr = new AudioTrack(tracks.size(), handler);
} else if (type.equals(AVIMediaType.VIDEO.fccType)) {
tr = new VideoTrack(tracks.size(), handler, null);
} else if (type.equals(AVIMediaType.MIDI.fccType)) {
tr = new MidiTrack(tracks.size(), handler);
} else if (type.equals(AVIMediaType.TEXT.fccType)) {
tr = new TextTrack(tracks.size(), handler);
} else {
throw new ParseException("Unknown track type " + type);
}
tr.fccHandler = handler;
tr.flags = inputStream.readInt();
tr.priority = inputStream.readUnsignedShort();
tr.language = inputStream.readUnsignedShort();
tr.initialFrames = inputStream.readUnsignedInt();
tr.scale = inputStream.readUnsignedInt();
tr.rate = inputStream.readUnsignedInt();
tr.startTime = inputStream.readUnsignedInt();
tr.length = inputStream.readUnsignedInt();
/*tr.suggestedBufferSize=*/ inputStream.readUnsignedInt();
tr.quality = inputStream.readInt();
/*tr.sampleSize=*/ inputStream.readUnsignedInt();
tr.frameLeft = inputStream.readShort();
tr.frameTop = inputStream.readShort();
tr.frameRight = inputStream.readShort();
tr.frameBottom = inputStream.readShort();
return tr;
}
}
/**
*
* typedef struct {
* cstring name;
* } STREAMNAME;
*
*
* @param tr
* @param data
* @throws IOException
*/
private void readSTRN(Track tr, byte[] data) throws IOException {
tr.name = new String(data, 0, data.length - 1, "ASCII");
}
/**
*
//---------------------- // AVI Bitmap Info Header //
* ---------------------- typedef struct { BYTE blue; BYTE green; BYTE red;
* BYTE reserved; } RGBQUAD;
*
* // Values for this enum taken from: //
* http://www.fourcc.org/index.php?http%3A//www.fourcc.org/rgb.php enum {
* BI_RGB = 0x00000000, RGB = 0x32424752, // Alias for BI_RGB BI_RLE8 =
* 0x01000000, RLE8 = 0x38454C52, // Alias for BI_RLE8 BI_RLE4 = 0x00000002,
* RLE4 = 0x34454C52, // Alias for BI_RLE4 BI_BITFIELDS = 0x00000003, raw =
* 0x32776173, RGBA = 0x41424752, RGBT = 0x54424752, cvid = "cvid" }
* bitmapCompression;
*
* typedef struct { DWORD structSize; DWORD width; DWORD height; WORD
* planes; WORD bitCount; FOURCC enum bitmapCompression compression; DWORD
* imageSizeInBytes; DWORD xPelsPerMeter; DWORD yPelsPerMeter; DWORD
* numberOfColorsUsed; DWORD numberOfColorsImportant; RGBQUAD colors[]; }
* BITMAPINFOHEADER;
*
*
*
* @param tr
* @param data
* @throws IOException
*/
private void readVideoSTRF(VideoTrack tr, byte[] data) throws IOException {
try (
ByteArrayImageInputStream inputStream = new ByteArrayImageInputStream(data, ByteOrder.LITTLE_ENDIAN);) {
inputStream.readUnsignedInt(); // structSize
tr.width = inputStream.readInt();
tr.height = inputStream.readInt();
tr.planes = inputStream.readUnsignedShort();
tr.bitCount = inputStream.readUnsignedShort();
inputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
tr.compression = intToType(inputStream.readInt());
inputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
long imageSizeInBytes = inputStream.readUnsignedInt();
tr.xPelsPerMeter = inputStream.readUnsignedInt();
tr.yPelsPerMeter = inputStream.readUnsignedInt();
tr.clrUsed = inputStream.readUnsignedInt();
tr.clrImportant = inputStream.readUnsignedInt();
if (tr.bitCount == 0) {
tr.bitCount = (int) (imageSizeInBytes / tr.width / tr.height * 8);
}
tr.format = new Format(MimeTypeKey, MIME_AVI,
MediaTypeKey, MediaType.VIDEO,
EncodingKey, tr.compression,
DataClassKey, byte[].class,
WidthKey, tr.width,
HeightKey, tr.height,
DepthKey, tr.bitCount,
PixelAspectRatioKey, new Rational(1, 1),
FrameRateKey, new Rational(tr.rate, tr.scale),
FixedFrameRateKey, true);
}
}
/**
* /**
* The format of a video track is defined in a "strf" chunk, which * contains a {@code WAVEFORMATEX} struct. *
* ---------------------- * AVI Wave Format Header * ---------------------- * // values for this enum taken from mmreg.h * enum { * WAVE_FORMAT_PCM = 0x0001, * // Microsoft Corporation * ...many more... * } wFormatTagEnum; * * typedef struct { * WORD enum wFormatTagEnum formatTag; * WORD numberOfChannels; * DWORD samplesPerSec; * DWORD avgBytesPerSec; * WORD blockAlignment; * WORD bitsPerSample; * WORD cbSize; * // Size, in bytes, of extra format information appended to the end of the * // WAVEFORMATEX structure. This information can be used by non-PCM formats * // to store extra attributes for the "wFormatTag". If no extra information * // is required by the "wFormatTag", this member must be set to zero. For * // WAVE_FORMAT_PCM formats (and only WAVE_FORMAT_PCM formats), this member * // is ignored. * byte[cbSize] extra; * } WAVEFORMATEX; ** * * @param tr * @param data * @throws IOException */ private void readAudioSTRF(AudioTrack tr, byte[] data) throws IOException { try ( ByteArrayImageInputStream inputStream = new ByteArrayImageInputStream(data, ByteOrder.LITTLE_ENDIAN);) { String formatTag = RIFFParser.idToString(inputStream.readUnsignedShort()); tr.channels = inputStream.readUnsignedShort(); tr.samplesPerSec = inputStream.readUnsignedInt(); tr.avgBytesPerSec = inputStream.readUnsignedInt(); tr.blockAlign = inputStream.readUnsignedShort(); tr.bitsPerSample = inputStream.readUnsignedShort(); if (data.length > 16) { inputStream.readUnsignedShort(); // cbSize // FIXME - Don't ignore extra format information } tr.format = new Format(MimeTypeKey, MIME_AVI, MediaTypeKey, MediaType.AUDIO, EncodingKey, formatTag, SampleRateKey, Rational.valueOf(tr.samplesPerSec), SampleSizeInBitsKey, tr.bitsPerSample, ChannelsKey, tr.channels, FrameSizeKey, tr.blockAlign, FrameRateKey, new Rational(tr.samplesPerSec, 1), SignedKey, tr.bitsPerSample != 8, ByteOrderKey, ByteOrder.LITTLE_ENDIAN); } } /** *
* // The values for this set have been taken from: * // http://graphics.cs.uni-sb.de/NMM/dist-0.4.0/Docs/Doxygen/html/avifmt_8h.html * set { * AVIIF_KEYFRAME = 0x00000010, * // The data chunk is a key frame. * AVIIF_LIST = 0x00000001, * // The data chunk is a 'rec ' list. * AVIIF_NO_TIME = 0x00000100, * // The data chunk does not affect the timing of the stream. For example, * // this flag should be set for palette changes. * AVIIF_COMPUSE = 0x0fff0000 * // These bits are for compressor use * } avioldindex_flags; * * typedef struct { * FOURCC chunkId; * // Specifies a FOURCC that identifies a stream in the AVI file. The * // FOURCC must have the form 'xxyy' where xx is the stream number and yy * // is a two-character code that identifies the contents of the stream: * DWORD set avioldindex_flags flags; * // Specifies a bitwise combination of zero or more of flags. * DWORD offset; * // Specifies the location of the data chunk in the file. The value should * // be specified as an offset, in bytes, from the start of the 'movi' list; * // however, in some AVI files it is given as an offset from the start of * // the file. * DWORD size; * // Specifies the size of the data chunk, in bytes. * } avioldindex_entry; ** * @param tracks * @param data * @return The idx1 list of samples. * @throws IOException */ private void readIDX1(ArrayList