/* * Created on Jan 19, 2011 * Created by Paul Gardner * * Copyright 2011 Vuze, Inc. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ package com.aelitis.azureus.core.util; import java.io.*; import java.util.*; import org.gudy.azureus2.core3.util.Debug; /* * The original code by Mike Melanson (melanson@pcisys.net) was placed in the * public domain and so is this Java port of it. */ public class QTFastStartRAF { private static final Set<String> supported_extensions = new HashSet<String>(); static{ supported_extensions.add( "mov" ); supported_extensions.add( "qt" ); supported_extensions.add( "mp4" ); } private static Set<String> tested = new HashSet<String>(); public static boolean isSupportedExtension( String extension ) { return( supported_extensions.contains( extension.toLowerCase())); } private static final String ATOM_FREE = "free"; private static final String ATOM_JUNK = "junk"; private static final String ATOM_MDAT = "mdat"; private static final String ATOM_MOOV = "moov"; private static final String ATOM_PNOT = "pnot"; private static final String ATOM_SKIP = "skip"; private static final String ATOM_WIDE = "wide"; private static final String ATOM_PICT = "PICT"; private static final String ATOM_FTYP = "ftyp"; private static final String ATOM_CMOV = "cmov"; private static final String ATOM_STCO = "stco"; private static final String ATOM_CO64 = "co64"; private static final String[] VALID_TOPLEVEL_ATOMS = { ATOM_FREE, ATOM_JUNK, ATOM_MDAT, ATOM_MOOV, ATOM_PNOT, ATOM_SKIP, ATOM_WIDE, ATOM_PICT, ATOM_FTYP }; private FileAccessor input; private boolean transparent; private byte[] header; private long body_start; private long body_end; private long seek_position; public QTFastStartRAF( File file, boolean enable ) throws IOException { this( new RAFAccessor( file ), enable ); } public QTFastStartRAF( FileAccessor accessor, boolean enable ) throws IOException { input = accessor; if ( enable ){ String name = accessor.getName(); boolean log; String fail = null; synchronized( tested ){ log = !tested.contains( name ); if ( log ){ tested.add( name ); } } try{ Atom ah = null; Atom ftypAtom = null; boolean gotFtyp = false; boolean gotMdat = false; boolean justCopy = false; while (input.getFilePointer() < input.length()) { ah = new Atom(input); // System.out.println( "got " + ah.type +", size=" + ah.size ); if (!isValidTopLevelAtom(ah)) { throw new IOException("Non top level QT atom found (" + ah.type + "). File invalid?"); } if (gotFtyp && !gotMdat && ah.type.equalsIgnoreCase(ATOM_MOOV)) { justCopy = true; break; } // store ftyp atom to buffer if (ah.type.equalsIgnoreCase(ATOM_FTYP)) { ftypAtom = ah; ftypAtom.fillBuffer(input); gotFtyp = true; } else if (ah.type.equalsIgnoreCase(ATOM_MDAT)) { gotMdat = true; input.skipBytes((int)ah.size); } else { input.skipBytes((int)ah.size); } } if ( justCopy ){ transparent = true; return; } if ( ftypAtom == null ){ throw new IOException("No FTYP atom found"); } if ( ah == null || !ah.type.equalsIgnoreCase(ATOM_MOOV)){ throw new IOException("Last QT atom was not the MOOV atom."); } input.seek(ah.offset); Atom moovAtom = ah; moovAtom.fillBuffer(input); if (isCompressedMoovAtom(moovAtom)){ throw new IOException("Compressed MOOV qt atoms are not supported"); } patchMoovAtom(moovAtom); body_start = ftypAtom.offset+ftypAtom.size; body_end = moovAtom.offset; header = new byte[ftypAtom.buffer.length + moovAtom.buffer.length]; System.arraycopy( ftypAtom.buffer, 0, header, 0, ftypAtom.buffer.length ); System.arraycopy( moovAtom.buffer, 0, header, ftypAtom.buffer.length, moovAtom.buffer.length ); if ( accessor.length() != header.length + ( body_end - body_start )){ throw( new IOException( "Inconsistent: file size has changed" )); } }catch( Throwable e ){ //e.printStackTrace(); fail = Debug.getNestedExceptionMessage( e ); transparent = true; }finally{ input.seek( 0 ); if ( log ){ String message; if ( fail == null ){ message = transparent?"Not required":"Required"; }else{ message = "Failed - " + fail; } Debug.outNoStack( "MOOV relocation for " + accessor.getName() + ": " + message ); } } }else{ transparent = true; } } private boolean isCompressedMoovAtom(Atom moovAtom) { byte[] cmovBuffer = copyOfRange(moovAtom.buffer, 12, 15); if (new String(cmovBuffer).equalsIgnoreCase(ATOM_CMOV)) { return true; } return false; } private boolean isValidTopLevelAtom(Atom ah) { for (String validAtom: VALID_TOPLEVEL_ATOMS) { if (validAtom.equalsIgnoreCase(ah.type)) { return true; } } return false; } private void patchMoovAtom(Atom moovAtom) { int idx = 0; for (idx = 4; idx < moovAtom.size-4; idx++) { byte[] buffer = copyOfRange(moovAtom.buffer, idx, idx+4); if (new String(buffer).equalsIgnoreCase(ATOM_STCO)) { int stcoSize = patchStcoAtom(moovAtom, idx); idx += stcoSize - 4; } else if (new String(buffer).equalsIgnoreCase(ATOM_CO64)) { int co64Size = patchCo64Atom(moovAtom, idx); idx += co64Size - 4; } } } private int patchStcoAtom(Atom ah, int idx) { int stcoSize = (int)bytesToLong(copyOfRange(ah.buffer, idx-4, idx)); int offsetCount = (int)bytesToLong(copyOfRange(ah.buffer, idx + 8, idx+12)); for (int j = 0; j < offsetCount; j++) { int currentOffset = (int)bytesToLong(copyOfRange(ah.buffer, idx + 12 + j * 4, (idx + 12 + j * 4)+4)); currentOffset += ah.size; int offsetIdx = idx + 12 + j * 4; ah.buffer[offsetIdx + 0] = (byte)((currentOffset >> 24) & 0xFF); ah.buffer[offsetIdx + 1] = (byte)((currentOffset >> 16) & 0xFF); ah.buffer[offsetIdx + 2] = (byte)((currentOffset >> 8) & 0xFF); ah.buffer[offsetIdx + 3] = (byte)((currentOffset >> 0) & 0xFF); } return stcoSize; } private int patchCo64Atom(Atom ah, int idx) { int co64Size = (int)bytesToLong(copyOfRange(ah.buffer, idx-4, idx)); int offsetCount = (int)bytesToLong(copyOfRange(ah.buffer, idx + 8, idx+12)); for (int j = 0; j < offsetCount; j++) { long currentOffset = bytesToLong(copyOfRange(ah.buffer, idx + 12 + j * 8, (idx + 12 + j * 8)+8)); currentOffset += ah.size; int offsetIdx = idx + 12 + j * 8; ah.buffer[offsetIdx + 0] = (byte)((currentOffset >> 56) & 0xFF); ah.buffer[offsetIdx + 1] = (byte)((currentOffset >> 48) & 0xFF); ah.buffer[offsetIdx + 2] = (byte)((currentOffset >> 40) & 0xFF); ah.buffer[offsetIdx + 3] = (byte)((currentOffset >> 32) & 0xFF); ah.buffer[offsetIdx + 4] = (byte)((currentOffset >> 24) & 0xFF); ah.buffer[offsetIdx + 5] = (byte)((currentOffset >> 16) & 0xFF); ah.buffer[offsetIdx + 6] = (byte)((currentOffset >> 8) & 0xFF); ah.buffer[offsetIdx + 7] = (byte)((currentOffset >> 0) & 0xFF); } return co64Size; } public static byte[] copyOfRange(byte[] original, int from, int to) { int newLength = to - from; if (newLength < 0) throw new IllegalArgumentException(from + " > " + to); byte[] copy = new byte[newLength]; System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength)); return copy; } private long bytesToLong(byte[] buffer) { long retVal = 0; for ( int i = 0; i < buffer.length; i++ ) { retVal += ((buffer[i] & 0x00000000000000FF) << 8*(buffer.length-i-1)) ; } return retVal; } public void seek( long pos ) throws IOException { if ( transparent ){ input.seek( pos ); }else{ seek_position = pos; } } public int read( byte[] buffer, int pos, int len ) throws IOException { if ( transparent ){ return( input.read( buffer, pos, len )); } // [header] // file [body-start -> body-end] long start_pos = seek_position; int start_len = len; if ( seek_position < header.length ){ int rem = (int)( header.length - seek_position ); if ( rem > len ){ rem = len; } System.arraycopy( header, (int)seek_position, buffer, pos, rem ); pos += rem; len -= rem; seek_position += rem; } if ( len > 0 ){ long file_position = body_start + seek_position - header.length; long rem = body_end - file_position; if ( len < rem ){ rem = len; } input.seek( file_position ); int temp = input.read( buffer, pos, (int)rem ); pos += temp; len -= temp; seek_position += temp; } int read = start_len - len; seek_position = start_pos + read; return( read ); } public long length() throws IOException { return( input.length()); } public void close() throws IOException { input.close(); } private class Atom { public long offset; public long size; public String type; public byte[] buffer = null; public Atom(FileAccessor input) throws IOException { offset = input.getFilePointer(); // get atom size size = input.readInt(); // get atom type byte[] atomTypeFCC = new byte[4]; input.readFully(atomTypeFCC); type = new String(atomTypeFCC); if (size == 1) { // 64 bit size. Read new size from body and store it size = input.readLong(); } // skip back to atom start input.seek(offset); } public void fillBuffer(FileAccessor input) throws IOException { buffer = new byte[(int)size]; input.readFully(buffer); } } private static class RAFAccessor implements FileAccessor { private File file; private RandomAccessFile raf; private RAFAccessor( File _file ) throws IOException { file = _file; raf = new RandomAccessFile( file, "r" ); } public String getName() { return( file.getAbsolutePath()); } public long getFilePointer() throws IOException { return( raf.getFilePointer()); } public void seek( long pos ) throws IOException { raf.seek( pos ); } public void skipBytes( int num ) throws IOException { raf.skipBytes( num ); } public long length() throws IOException { return( raf.length()); } public int read( byte[] buffer, int pos, int len ) throws IOException { return( raf.read(buffer,pos,len)); } public int readInt() throws IOException { return( raf.readInt()); } public long readLong() throws IOException { return( raf.readLong()); } public void readFully( byte[] buffer ) throws IOException { raf.readFully( buffer ); } public void close() throws IOException { raf.close(); } } public interface FileAccessor { public String getName(); public long getFilePointer() throws IOException; public void seek( long pos ) throws IOException; public void skipBytes( int num ) throws IOException; public long length() throws IOException; public int read( byte[] buffer, int pos, int len ) throws IOException; public int readInt() throws IOException; public long readLong() throws IOException; public void readFully( byte[] buffer ) throws IOException; public void close() throws IOException; } /* public static void main( String[] args ) { try{ QTFastStartRAF raf = new QTFastStartRAF( new File( "C:\\temp\\spork.mp4" ), true ); long len = raf.length(); byte[] buffer = new byte[43]; long total = 0; FileOutputStream fos = new FileOutputStream(new File( "C:\\temp\\qtfs_out.mp4" )); while( true ){ int read = raf.read( buffer, 0, buffer.length ); if ( read <= 0 ){ break; } fos.write( buffer, 0, read ); total += read; } if ( total != len ){ System.out.println( "bork" ); } raf.close(); fos.close(); }catch( Throwable e ){ e.printStackTrace(); } } */ }