package org.jcodec.player.filters.http; import gnu.trove.list.array.TIntArrayList; import gnu.trove.map.TLongIntMap; import gnu.trove.map.hash.TIntObjectHashMap; import gnu.trove.map.hash.TLongIntHashMap; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.List; import org.jcodec.common.NIOUtils; import org.jcodec.common.model.Packet; import org.jcodec.common.model.TapeTimecode; import org.jcodec.player.filters.MediaInfo; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * Frame cache, improved version * * @author The JCodec project * */ public class FrameCache { private static int DATA_SEGMENT = 87; private static int INDEX_SEGMENT = 113; private static int DATASEG_SIZE = 1024 * 1024 * 10; private FileChannel fd; private TIntObjectHashMap<IndexRecord> index; private List<Long> dataSegments = new ArrayList<Long>(); private MediaInfo mediaInfo; private TLongIntMap pts2frame; private int dsFill; private List<IndexRecord> dsFrames = new ArrayList<IndexRecord>(); private static class IndexRecord { public int frameNo; public long pos; public int dataLen; public long pts; public int duration; public boolean key; public TapeTimecode tapeTimecode; public IndexRecord(int frameNo, long pos, int dataLen, long pts, int duration, boolean key, TapeTimecode tapeTimecode) { this.frameNo = frameNo; this.pos = pos; this.dataLen = dataLen; this.pts = pts; this.duration = duration; this.key = key; this.tapeTimecode = tapeTimecode; } } public FrameCache(File cacheWhere) throws IOException { fd = new FileInputStream(cacheWhere).getChannel(); loadData(fd); addDataSegment(); } public void close() throws IOException { synchronized (fd) { fd.close(); fd = null; } } public void loadData(FileChannel f) throws IOException { index = new TIntObjectHashMap<IndexRecord>(); pts2frame = new TLongIntHashMap(1, 0.5f, -1, -1); while (f.position() + 5 <= f.size()) { int segmentType = NIOUtils.readByte(f); int segmentSize = NIOUtils.readInt(f); if (f.position() + segmentSize > f.size()) { // invelid segment, file corruption starts here f.position(f.position() - 5); break; } if (segmentType == DATA_SEGMENT) { dataSegments.add(f.position()); f.position(f.position() + segmentSize); } else if (segmentType == INDEX_SEGMENT) { ByteBuffer buffer = NIOUtils.fetchFrom(f, segmentSize); while (buffer.remaining() >= 29) { int frameNo = buffer.getInt(); IndexRecord rec = new IndexRecord(frameNo, buffer.getLong(), buffer.getInt(), buffer.getLong(), buffer.getInt(), buffer.get() == 1, null); index.put(frameNo, rec); pts2frame.put(rec.pts, frameNo); } } else { // invalid segment, file corruption starts here f.position(f.position() - 5); break; } } } public Packet getFrame(int frameNo, ByteBuffer buffer) throws IOException { IndexRecord record = index.get(frameNo); if (record == null) return null; ByteBuffer out = buffer.duplicate(); out.limit(out.position() + record.dataLen); if (fd == null) return null; synchronized (fd) { if (fd == null) return null; fd.position(record.pos); int dsOff = (int) (record.pos - getDataSegmentOff(record)); while (out.remaining() > 0) { int toRead = Math.min(out.remaining(), DATASEG_SIZE - dsOff); NIOUtils.read(fd, out, toRead); if (out.remaining() > 0) { skipToDataseg(); dsOff = 0; } } out.flip(); return new Packet(out, record.pts, 0, record.duration, frameNo, record.key, record.tapeTimecode); } } private void skipToDataseg() throws IOException { while (fd.position() + 5 <= fd.size()) { int segType = NIOUtils.readByte(fd); int segSize = NIOUtils.readInt(fd); if (segType != DATA_SEGMENT && segType != INDEX_SEGMENT) throw new IOException("Invalid segment: " + segType); if (segType == DATA_SEGMENT) return; fd.position(fd.position() + segSize); } } private long getDataSegmentOff(IndexRecord record) { long found = dataSegments.get(0); for (long offset : dataSegments) { if (offset > record.pos) break; found = offset; } return found; } public int pts2frame(long pts) { synchronized (pts2frame) { long[] keys = pts2frame.keys(); int nearestBefore = -1, nearestAfter = -1; for (int i = 0; i < keys.length; i++) { if (keys[i] <= pts && (nearestBefore == -1 || keys[i] > keys[nearestBefore])) nearestBefore = i; if (keys[i] >= pts && (nearestAfter == -1 || keys[i] < keys[nearestAfter])) nearestAfter = i; } int[] values = pts2frame.values(); if (nearestBefore != -1 && nearestAfter != -1 && values[nearestAfter] - values[nearestBefore] <= 1) return values[nearestBefore]; return -1; } } public void addFrame(Packet packet) throws IOException { if (fd == null) return; synchronized (fd) { if (fd == null) return; fd.position(dataSegments.get(dataSegments.size() - 1) + dsFill); long pos = fd.position(); ByteBuffer data = packet.getData().duplicate(); IndexRecord record = new IndexRecord((int) packet.getFrameNo(), pos, packet.getData().remaining(), packet.getPts(), (int) packet.getDuration(), packet.isKeyFrame(), packet.getTapeTimecode()); index.put((int) packet.getFrameNo(), record); while (data.remaining() > 0) { ByteBuffer piece = NIOUtils.read(data, Math.min(data.remaining(), DATASEG_SIZE - dsFill)); fd.write(piece); dsFill += piece.remaining(); if (dsFill == DATASEG_SIZE) { if (dsFrames.size() > 0) { writeIndex(fd, dsFrames); dsFrames.clear(); } dsFill = 0; addDataSegment(); } } dsFrames.add(record); } synchronized (pts2frame) { pts2frame.put(packet.getPts(), (int) packet.getFrameNo()); } synchronized (starts) { updateCached((int) packet.getFrameNo()); } } private void addDataSegment() throws IOException { NIOUtils.writeByte(fd, (byte) DATA_SEGMENT); NIOUtils.writeInt(fd, DATASEG_SIZE); dataSegments.add(fd.position()); } private void writeIndex(FileChannel fd, List<IndexRecord> dsFrames) throws IOException { NIOUtils.writeByte(fd, (byte) INDEX_SEGMENT); NIOUtils.writeInt(fd, dsFrames.size() * 29); ByteBuffer buf = ByteBuffer.allocate(dsFrames.size() * 29); for (IndexRecord indexRecord : dsFrames) { buf.putInt(indexRecord.frameNo); buf.putLong(indexRecord.pos); buf.putInt(indexRecord.dataLen); buf.putLong(indexRecord.pts); buf.putInt(indexRecord.duration); buf.put((byte) (indexRecord.key ? 1 : 0)); } buf.flip(); fd.write(buf); } public boolean hasFrame(int i) { return index.containsKey(i); } public MediaInfo getMediaInfo() { return mediaInfo; } TIntArrayList starts = new TIntArrayList();; TIntArrayList ends = new TIntArrayList(); private void updateCached(int frame) { for (int i = 0; i < starts.size(); i++) { if (frame >= starts.get(i) && frame <= ends.get(i)) return; if (frame == starts.get(i) - 1) { starts.set(i, frame); return; } if (frame == ends.get(i) + 1) { ends.set(i, frame); return; } } starts.add(frame); ends.add(frame); } public int[][] getCached() { return new int[][] { starts.toArray(), ends.toArray() }; } }