/* * Flazr <http://flazr.com> Copyright (C) 2009 Peter Thomas. * * This file is part of Flazr. * * Flazr is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Flazr 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Flazr. If not, see <http://www.gnu.org/licenses/>. */ package com.flazr.io.flv; import com.flazr.io.BufferReader; import com.flazr.io.FileChannelReader; import com.flazr.rtmp.RtmpMessage; import com.flazr.rtmp.RtmpReader; import com.flazr.rtmp.message.Aggregate; import com.flazr.rtmp.message.MessageType; import com.flazr.rtmp.message.Metadata; import com.flazr.rtmp.message.MetadataAmf0; import com.flazr.rtmp.message.Video; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FlvReader implements RtmpReader { private static final Logger logger = LoggerFactory.getLogger(FlvReader.class); private final BufferReader in; private final long mediaStartPosition; private final Metadata metadata; private int aggregateDuration; private int width; private int height; public FlvReader(final String path) { in = new FileChannelReader(path); in.position(13); // skip flv header final RtmpMessage metadataAtom = next(); /* TODO: block added to ignore an exception caused probably due to a new message in flv/rtmp that is not treated by flazr */ /*final*/ RtmpMessage metadataTemp = null; try { metadataTemp = MessageType.decode(metadataAtom.getHeader(), metadataAtom.encode()); } catch (Exception e) { if (e.getMessage().equals("bad value / byte: 101 (hex: 65), java.lang.ArrayIndexOutOfBoundsException: 101")) { logger.debug("Ignoring malformed metadata (bad value / byte: 101 (hex: 65))"); } } if(metadataTemp != null && metadataTemp.getHeader().isMetadata()) { metadata = (Metadata) metadataTemp; mediaStartPosition = in.position(); } else { logger.warn("flv file does not start with 'onMetaData', using empty one"); metadata = new MetadataAmf0("onMetaData"); in.position(13); mediaStartPosition = 13; } logger.debug("flv file metadata: {}", metadata); RtmpMessage firstFrame; do { firstFrame = next(); } while (!firstFrame.getHeader().isVideo() && hasNext()); if (firstFrame != null) { Video video = new Video(firstFrame.getHeader(), firstFrame.encode()); width = video.getWidth(); height = video.getHeight(); metadata.setValue("width", width); metadata.setValue("height", height); // rewind seek(0); } } @Override public Metadata getMetadata() { return metadata; } @Override public RtmpMessage[] getStartMessages() { return new RtmpMessage[] { metadata }; } @Override public void setAggregateDuration(int targetDuration) { this.aggregateDuration = targetDuration; } @Override public long getTimePosition() { final int time; if(hasNext()) { time = next().getHeader().getTime(); prev(); } else if(hasPrev()) { time = prev().getHeader().getTime(); next(); } else { throw new RuntimeException("not seekable"); } return time; } private static boolean isSyncFrame(final RtmpMessage message) { final byte firstByte = message.encode().getByte(0); if((firstByte & 0xF0) == 0x10) { return true; } return false; } @Override public long seek(final long time) { logger.debug("trying to seek to: {}", time); if(time == 0) { // special case try { in.position(mediaStartPosition); return 0; } catch(Exception e) { throw new RuntimeException(e); } } final long start = getTimePosition(); if(time > start) { while(hasNext()) { final RtmpMessage cursor = next(); if(cursor.getHeader().getTime() >= time) { break; } } } else { while(hasPrev()) { final RtmpMessage cursor = prev(); if(cursor.getHeader().getTime() <= time) { next(); break; } } } // find the closest sync frame prior try { final long checkPoint = in.position(); while(hasPrev()) { final RtmpMessage cursor = prev(); if(cursor.getHeader().isVideo() && isSyncFrame(cursor)) { logger.debug("returned seek frame / position: {}", cursor); return cursor.getHeader().getTime(); } } // could not find a sync frame ! // TODO better handling, what if file is audio only in.position(checkPoint); return getTimePosition(); } catch(Exception e) { throw new RuntimeException(e); } } @Override public boolean hasNext() { return in.position() < in.size(); } protected boolean hasPrev() { return in.position() > mediaStartPosition; } protected RtmpMessage prev() { final long oldPos = in.position(); in.position(oldPos - 4); final long newPos = oldPos - 4 - in.readInt(); in.position(newPos); final FlvAtom flvAtom = new FlvAtom(in); in.position(newPos); return flvAtom; } private static final int AGGREGATE_SIZE_LIMIT = 65536; @Override public RtmpMessage next() { if(aggregateDuration <= 0) { return new FlvAtom(in); } final ChannelBuffer out = ChannelBuffers.dynamicBuffer(); int firstAtomTime = -1; while(hasNext()) { final FlvAtom flvAtom = new FlvAtom(in); final int currentAtomTime = flvAtom.getHeader().getTime(); if(firstAtomTime == -1) { firstAtomTime = currentAtomTime; } final ChannelBuffer temp = flvAtom.write(); if(out.readableBytes() + temp.readableBytes() > AGGREGATE_SIZE_LIMIT) { prev(); break; } out.writeBytes(temp); if(currentAtomTime - firstAtomTime > aggregateDuration) { break; } } return new Aggregate(firstAtomTime, out); } @Override public void close() { in.close(); } @Override public int getWidth() { return width; } @Override public int getHeight() { return height; } public static void main(String[] args) { FlvReader reader = new FlvReader("/home/felipe/codes/mconf/bbbot/bot/etc/sample.flv"); while(reader.hasNext()) { RtmpMessage message = reader.next(); logger.debug("{} {}", message, ChannelBuffers.hexDump(message.encode())); } reader.close(); } }