package com.limegroup.gnutella.metadata.video.reader; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.io.IOUtils; import com.limegroup.gnutella.metadata.MetaReader; import com.limegroup.gnutella.metadata.video.VideoMetaData; /** * Constructs metadata for an MPEG 1 & 2 video file. * * This is based off the work of XNap, at: * http://xnap.sourceforge.net/xref/org/xnap/plugin/viewer/videoinfo/VideoFile.html */ public class MPEGMetaData implements MetaReader { private static final Log LOG = LogFactory.getLog(MPEGMetaData.class); private static final int PACK_START_CODE = 0x000001BA; private static final int SEQ_START_CODE = 0x000001B3; private static final int MAX_FORWARD_READ_LENGTH = 50000; private static final int MAX_BACKWARD_READ_LENGTH = 3000000; @Override public VideoMetaData parse(File f) throws IOException { RandomAccessFile raf = null; try { raf = new RandomAccessFile(f, "r"); VideoMetaData videoData = new VideoMetaData(); parseMPEG(videoData, raf); return videoData; } finally { IOUtils.close(raf); } } private void parseMPEG(VideoMetaData videoData, RandomAccessFile raf) throws IOException { boolean firstGOP = false; boolean firstSEQ = false; boolean lastGOP = false; long initialHMS = -1; // MPEG is structured a series of codes. // GOP (group of picture) contains hour/minute/second of each frame // SEQ (sequence) contains height/width of the frame. // The height & width of the first frame are assumed to be the // height/width of every frame. // The duration is calculated by subtracting the HMS of the first frame // from the HMS of the last frame. while(true) { LOG.debug("Advancing to next code..."); nextStartCode(raf); int code = raf.readInt(); if(code == PACK_START_CODE && !firstGOP) { LOG.debug("Found GOP code"); firstGOP = true; byte[] b = new byte[6]; raf.readFully(b); if ((b[0] & 0xF0) == 0x20) { initialHMS = getMPEGHMS(b); } else if ((b[0] & 0xC0) == 0x40) { initialHMS = getMPEG2HMS(b); } } else if(code == SEQ_START_CODE && !firstSEQ) { LOG.debug("Found SEQ code"); firstSEQ = true; byte[] b = new byte[3]; raf.readFully(b); videoData.setWidth(((b[0] & 0xff) << 4) | (b[1] & 0xf0)); videoData.setHeight(((b[1] & 0x0f) << 8) | (b[2] & 0xff)); } if(firstSEQ && firstGOP) break; } // If we couldn't get the initial HMS, we don't need get the last. if(initialHMS != -1) { raf.seek(raf.length()); while(true) { LOG.debug("Rewinding to prior code..."); previousStartCode(raf); if(raf.readInt() == PACK_START_CODE) { LOG.debug("Found GOP code"); lastGOP = true; break; } // pretend we didn't read that int. raf.seek(raf.getFilePointer() - 4); } if (lastGOP) { byte[] b = new byte[6]; long lastHMS = -1; raf.readFully(b); if ((b[0] & 0xF0) == 0x20) { lastHMS = getMPEGHMS(b); } else if ((b[0] & 0xC0) == 0x40) { lastHMS = getMPEG2HMS(b); } if(lastHMS != -1) videoData.setLength((int)(lastHMS - initialHMS)); } } } /** Advances the RAF to the next code. */ private void nextStartCode(RandomAccessFile raf) throws IOException { byte[] b = new byte[1024]; int available; for (int i = 0; i < MAX_FORWARD_READ_LENGTH; i += available) { available = raf.read(b); if (available > 0) { i += available; for (int offset = 0; offset < available - 2; offset++) { if (b[offset] == 0 && b[offset + 1] == 0 && b[offset + 2] == 1) { raf.seek(raf.getFilePointer() - (available - offset)); return; } } } else { throw new IOException("no start code"); } } throw new IOException("no start code"); } /** Rewinds the RAF to the prior code. */ private void previousStartCode(RandomAccessFile raf) throws IOException { byte[] b = new byte[8024]; for (int i = 0; i < MAX_BACKWARD_READ_LENGTH; i += b.length) { long fp = raf.getFilePointer() - b.length; if (fp < 0) { if (fp <= b.length) { break; } fp = 0; } raf.seek(fp); raf.readFully(b); for (int offset = b.length - 1; offset > 1; offset--) { if (b[offset - 2] == 0 && b[offset - 1] == 0 && b[offset] == 1) { raf.seek(raf.getFilePointer() - (b.length - offset) - 2); return; } } raf.seek(raf.getFilePointer() - b.length); } throw new IOException("no prior start code"); } /** Gets the hour/minute/second in seconds of MPEG-1. */ protected long getMPEGHMS(byte[] b) { long low4Bytes = (((b[0] & 0xff) >> 1) & 0x03) << 30 | (b[1] & 0xff) << 22 | ((b[2] & 0xff) >> 1) << 15 | (b[3] & 0xff) << 7 | (b[4] & 0xff) >> 1; return low4Bytes / 90000; } /** Gets the hour/minute/second in seconds of MPEG-2. */ protected long getMPEG2HMS(byte[] b) { long low4Bytes = ((b[0] & 0x18) >> 3) << 30 | (b[0] & 0x03) << 28 | (b[1] & 0xff) << 20 | ((b[2] & 0xF8) >> 1) << 15 | (b[2] & 0x03) << 13 | (b[3] & 0xff) << 5 | (b[4] & 0xff) >> 3; int sys_clock_extension = (b[4] & 0x3) << 7 | ((b[5] & 0xff) >> 1); if (sys_clock_extension == 0) { return low4Bytes / 90000; } else { return -1; } } @Override public String[] getSupportedExtensions() { return new String[] { "mpg", "mpeg" }; } }