/* * JaamSim Discrete Event Simulation * Copyright (C) 2012 Ausenco Engineering Canada Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.video; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.util.ArrayList; import com.jaamsim.ui.LogBox; public class AviWriter { private static class FrameEntry { int size; int pos; boolean keyFrame; } private FileChannel fc; private RandomAccessFile raf; private ArrayList<FrameEntry> index; private int width, height; private int numFrames; private int moviSizePos; private int chunkPos; public AviWriter(String filename, int width, int height, int numFrames) { try { this.width = width; this.height = height; this.numFrames = numFrames; raf = new RandomAccessFile(filename, "rw"); raf.setLength(0); fc = raf.getChannel(); writeHeader(); index = new ArrayList<>(numFrames); } catch (IOException ex) { fc = null; // TODO log this error LogBox.renderLogException(ex); } } public boolean isOpen() { return fc != null; } public void close() { try { // The size of the file before the index is written int dataSize = (int)fc.position(); writeIndex(); // The size of the file with the index int fileSize = (int)fc.position(); ByteBuffer sizeBuff = ByteBuffer.allocate(4); sizeBuff.order(ByteOrder.LITTLE_ENDIAN); // Fixup some of the missing sizes fc.position(4); sizeBuff.putInt(fileSize - 8); sizeBuff.flip(); fc.write(sizeBuff); sizeBuff.clear(); fc.position(moviSizePos); sizeBuff.putInt(dataSize - moviSizePos - 4); sizeBuff.flip(); fc.write(sizeBuff); fc.close(); fc = null; } catch (IOException ex) { // Ignore for now... LogBox.renderLogException(ex); } } private void writeIndex() { ByteBuffer buff = ByteBuffer.allocate(8 + 16 * index.size()); buff.order(ByteOrder.LITTLE_ENDIAN); writeFourCC(buff, "idx1"); buff.putInt(16*index.size()); // struct size for (FrameEntry f : index) { writeFourCC(buff, "00dc"); buff.putInt(f.keyFrame ? 0x10 : 0); // Key frame flag buff.putInt(f.pos); buff.putInt(f.size); } buff.flip(); try { fc.write(buff); } catch (IOException ex) { LogBox.renderLogException(ex); throw new RuntimeException(ex); } } private static void writeFourCC(ByteBuffer buff, String fourCC) { byte[] b = new byte[4]; for (int i = 0; i < 4; ++i) b[i] = (byte)fourCC.charAt(i); buff.put(b); } private void writeHeader() throws IOException { ByteBuffer header = ByteBuffer.allocate(256); header.order(ByteOrder.LITTLE_ENDIAN); writeFourCC(header, "RIFF"); header.putInt(0); // We will fill this size in when we are done writeFourCC(header, "AVI "); writeFourCC(header, "LIST"); header.putInt(192); // Total header size int hdrlPos = header.position(); writeFourCC(header, "hdrl"); writeFourCC(header, "avih"); header.putInt(0x38); // Size of the MainAVIHeader header.putInt(33000000); // 33 ms per frame header.putInt(0); // Max bytes per second header.putInt(0); // reserved header.putInt(0x10); // flags (has index) header.putInt(numFrames); header.putInt(0); // Initial frames header.putInt(1); // num streams header.putInt(width * height * 3); // buffer size header.putInt(width); header.putInt(height); header.putInt(0); // reserved header.putInt(0); header.putInt(0); header.putInt(0); writeFourCC(header, "LIST"); header.putInt(116); // Size of the rest of the header int strlPos = header.position(); writeFourCC(header, "strl"); writeFourCC(header, "strh"); header.putInt(0x38); // Size of the AVIStreamHeader writeFourCC(header, "vids"); writeFourCC(header, "VP80"); header.putInt(0); // flags header.putInt(0); // priority header.putInt(0); // initial frames header.putInt(1); // scale header.putInt(30); // rate header.putInt(0); // start header.putInt(numFrames/33); header.putInt(width * height * 3); header.putInt(-1); // default quality header.putInt(0); // sample size // These 3 make up the RECT struct header.putInt(0); header.putShort((short)width); header.putShort((short)height); writeFourCC(header, "strf"); header.putInt(0x28); // chunk size header.putInt(0x28); // struct size header.putInt(width); header.putInt(height); header.putShort((short)1); // planes header.putShort((short)24); // bpp writeFourCC(header, "VP80"); header.putInt(width * height * 3); // buffer size header.putInt(0); // other info header.putInt(0); header.putInt(0); header.putInt(0); assert(header.position() - hdrlPos == 192); assert(header.position() - strlPos == 116); writeFourCC(header, "LIST"); moviSizePos = header.position(); header.putInt(0); // TODO, work out this size writeFourCC(header, "movi"); chunkPos = header.position(); header.flip(); fc.write(header); // Header is written and we are ready for data } public void addFrame(ByteBuffer frameData, boolean keyFrame) { ByteBuffer header = ByteBuffer.allocate(8); header.order(ByteOrder.LITTLE_ENDIAN); int extraBytes = 0; if ((frameData.limit() & 3) != 0) { // We will align to 4 byte boundaries extraBytes = 4 - (frameData.limit() & 3); } FrameEntry f = new FrameEntry(); f.pos = chunkPos; f.size = frameData.limit() + 8 + extraBytes; f.keyFrame = keyFrame; chunkPos += f.size; index.add(f); writeFourCC(header, "00dc"); header.putInt(frameData.limit() + extraBytes); header.flip(); try { fc.write(header); fc.write(frameData); header.clear(); for (int i = 0; i < extraBytes; ++i) { header.put((byte)0); } header.flip(); if (extraBytes != 0) { fc.write(header); } } catch (IOException ex) { LogBox.renderLogException(ex); throw new RuntimeException(ex); } } }