/** * Copyright (c) 2006 - 2008 Smaxe Ltd (www.smaxe.com). * All rights reserved. */ package com.smaxe.uv.fformat; import com.smaxe.io.ByteArray; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; /** * <code>FlvTool</code> - FLV tool. * * @author Andrei Sochirca */ public final class FlvTool extends Object { /** * Returns video frame type by <code>tag</code>. * * @param tag * @return frame encoded in the <code>tag</code> */ public static int getVideoFrame(final int tag) { return (tag >> 4) & 0x0F; } /** * Returns video codec by <code>tag</code>. * * @param tag * @return codec encoded in the <code>tag</code> */ public static int getVideoCodec(final int tag) { return tag & 0x0F; } /** * <code>FileInfo</code> - FLV file info. */ public final static class FileInfo extends Object { /** * Number of audio frames. */ public int audioFrames = 0; /** * Size of audio frames (in bytes). */ public int audioFramesSize = 0; /** * Audio stream duration (in milliseconds). */ public int audioStreamDuration = 0; /** * Number of video frames. */ public int videoFrames = 0; /** * Size of video frames (in bytes). */ public int videoFramesSize = 0; /** * Video stream duration (in milliseconds). */ public int videoStreamDuration = 0; /** * Number of data frames. */ public int dataFrames = 0; /** * Size of data frames (in bytes). */ public int dataFramesSize = 0; /** * Constructor. */ public FileInfo() { } @Override public String toString() { return "FileInfo [" + "audio (frames/size/duration): " + audioFrames + "/" + audioFramesSize + "/" + audioStreamDuration + "; " + "video (frames/size/duration): " + videoFrames + "/" + videoFramesSize + "/" + videoStreamDuration + "; " + "data (frames/size): " + dataFrames + "/" + dataFramesSize + "]"; } } /** * <code>GetDataFramesTagProcessor</code> */ private final static class GetDataFramesTagProcessor extends Flv.TagProcessorAdapter { // fields private List<Flv.ScriptData[]> frames = null; /** * Constructor. * * @param frames data frames */ public GetDataFramesTagProcessor(List<Flv.ScriptData[]> frames) { this.frames = frames; } @Override public void onTag(final int type, final int dataSize, final long timestamp, final InputStream is) throws IOException { switch (type) { case Flv.Tag.TYPE_DATA: { frames.add(Flv.readScriptData(is, dataSize)); } break; } } } /** * <code>GetInfoTagProcessor</code> */ private final static class GetInfoTagProcessor extends Flv.TagProcessorAdapter { // fields private FileInfo info = null; private long lastAudioTimestamp = -1; private long lastVideoTimestamp = -1; /** * Constructor. * * @param info file info */ public GetInfoTagProcessor(FileInfo info) { this.info = info; } @Override public void onTag(final int type, final int dataSize, final long timestamp, InputStream is) throws IOException { switch (type) { case Flv.Tag.TYPE_AUDIO: { if (lastAudioTimestamp < 0) lastAudioTimestamp = timestamp; info.audioFrames++; info.audioFramesSize += dataSize; info.audioStreamDuration += timestamp - lastAudioTimestamp; lastAudioTimestamp = timestamp; } break; case Flv.Tag.TYPE_VIDEO: { if (lastVideoTimestamp < 0) lastVideoTimestamp = timestamp; info.videoFrames++; info.videoFramesSize += dataSize; info.videoStreamDuration += timestamp - lastVideoTimestamp; lastVideoTimestamp = timestamp; } break; case Flv.Tag.TYPE_DATA: { info.dataFrames++; info.dataFramesSize += dataSize; } break; } } } /** * <code>ConvertFlvToMp3TagProcessor</code> */ private final static class ConvertFlvToMp3TagProcessor extends Flv.TagProcessorAdapter { /** * <code>FLV_AUDIO_TAG_SIZE</code> - audio tag size. */ private final static int FLV_AUDIO_TAG_SIZE = 1; // fields private OutputStream aos = null; private byte[] buf = null; /** * Constructor. * * @param aos audio output stream */ public ConvertFlvToMp3TagProcessor(final OutputStream aos) { this.aos = aos; this.buf = new byte[1024]; } @Override public void onTag(final int type, final int dataSize, final long timestamp, InputStream is) throws IOException { switch (type) { case Flv.Tag.TYPE_AUDIO: { if (dataSize > 0) { is.read(buf, 0, dataSize); aos.write(buf, FLV_AUDIO_TAG_SIZE, dataSize - FLV_AUDIO_TAG_SIZE); } } break; } } } /** * <code>CheckCorrectnessTagProcessor</code> - check file correctness, i.e. * if audio/video tags are valid (known codec id). */ private final static class CheckCorrectnessTagProcessor extends Flv.TagProcessorAdapter { // fields /** * Constructor. * * @param info file info */ public CheckCorrectnessTagProcessor() { } @Override public void onTag(final int type, final int dataSize, final long timestamp, InputStream is) throws IOException { switch (type) { case Flv.Tag.TYPE_AUDIO: { final int tag = is.read(); } break; case Flv.Tag.TYPE_VIDEO: { final int tag = is.read(); } break; case Flv.Tag.TYPE_DATA: { is.read(); } break; } } } /** * <code>SplitTagProcessor</code> */ private final static class SplitTagProcessor extends Flv.TagProcessorAdapter { // fields private OutputStream aos = null; private OutputStream vos = null; private long audioTimestamp = -1; private long videoTimestamp = -1; private byte[] buf = null; /** * Constructor. * * @param aos audio output stream * @param vos video output stream */ public SplitTagProcessor(final OutputStream aos, final OutputStream vos) { this.aos = aos; this.vos = vos; this.buf = new byte[1024]; } @Override public void onTag(final int type, final int dataSize, final long timestamp, InputStream is) throws IOException { switch (type) { case Flv.Tag.TYPE_AUDIO: { if (audioTimestamp < 0) { audioTimestamp = timestamp; Flv.writeHeader(aos, new Flv.Header(true /*audio*/, false /*video*/)); } is.read(buf, 0, dataSize); Flv.writeTag(aos, type, timestamp - audioTimestamp, buf, 0, dataSize); } break; case Flv.Tag.TYPE_VIDEO: { if (videoTimestamp < 0) { videoTimestamp = timestamp; Flv.writeHeader(vos, new Flv.Header(false /*audio*/, true /*video*/)); } final int vtag = is.read(); if (getVideoFrame(vtag) < 0x05 /*command*/) { Flv.writeTagHeader(vos, type, timestamp - videoTimestamp, dataSize); // copy video frame data vos.write(vtag); int size = dataSize - 1; do { final int len = Math.min(buf.length, size); is.read(buf, 0, len); vos.write(buf, 0, len); size -= len; } while (size > 0); Flv.writeTagTrailer(vos, dataSize); } } break; } } } /** * <code>TagKeeper</code> - tag keeper. */ private final static class TagKeeper extends Flv.TagProcessorAdapter { // fields private final int type; private final byte[] buf; private long timestamp = -1; private ByteArray data = null; /** * Constructor. * * @param type frame type to keep */ public TagKeeper(final int type) { this.type = type; this.buf = new byte[8 * 1024]; } /** * Checks if keeper is valid, i.e. has data. * * @return <code>true</code> if valid; otherwise <code>false</code> */ public final boolean isValid() { return timestamp >= 0; } /** * Returns kept frame timestamp. * * @return frame timestamp */ public final long getTimestamp() { return timestamp; } /** * Writes the tag to the {@link OutputStream <tt>os</tt>} and invalidates the keeper. * * @param os * @throws IOException if an I/O exception occurred */ public void write(OutputStream os) throws IOException { Flv.writeTag(os, type, timestamp, data.array, data.offset, data.length); timestamp = -1; } @Override public void onTag(final int type, final int dataSize, final long timestamp, InputStream is) throws IOException { if (this.type != type) return; this.timestamp = timestamp; if (dataSize > buf.length) { byte[] buf = new byte[dataSize]; is.read(buf, 0, dataSize); data = new ByteArray(buf, 0, dataSize); } else { is.read(buf, 0, dataSize); data = new ByteArray(buf, 0, dataSize); } // post-process... a bit of hack if (type == Flv.Tag.TYPE_VIDEO) { if (getVideoFrame(data.array[data.offset]) >= 0x05 /*command*/) { this.timestamp = -1; } } } } /** * Returns <code>file</code> data frames: available data frames. * * @param file flv file * @return data frames: list of <code>Flv.ScriptData</code> * @throws Exception if an exception occurred */ public static List<Flv.ScriptData[]> getDataFrames(final String file) throws Exception { List<Flv.ScriptData[]> frames = new ArrayList<Flv.ScriptData[]>(); processFile(file, new GetDataFramesTagProcessor(frames)); return frames; } /** * Returns <code>file</code> info: number of audio/video/data frames/bytes. * * @param file flv file * @return file info * @throws Exception if an exception occurred */ public static FileInfo getInfo(final String file) throws Exception { FileInfo info = new FileInfo(); processFile(file, new GetInfoTagProcessor(info)); return info; } /** * @param file * @throws Exception if an exception occurred */ public static void check(final String file) throws Exception { processFile(file, new CheckCorrectnessTagProcessor()); } /** * Merges audio stream from the <code>audioFile</code> and video stream from * the <code>videoFile</code> and writes the result to the <code>file</code>. * * @param audioFile audio flv file * @param videoFile video flv file * @param file result flv file * @throws Exception if an exception occurred */ public static void merge(final String audioFile, final String videoFile, final String file) throws Exception { if (!new File(audioFile).exists()) throw new IllegalArgumentException("Audio file doesn't exists!"); if (!new File(videoFile).exists()) throw new IllegalArgumentException("Video file doesn't exists!"); InputStream ais = null; InputStream vis = null; OutputStream os = null; try { ais = new FileInputStream(audioFile); vis = new FileInputStream(videoFile); os = new BufferedOutputStream(new FileOutputStream(file), 32 * 1024); Flv.readHeader(ais); Flv.readHeader(vis); Flv.writeHeader(os, new Flv.Header(true, true)); TagKeeper audioTag = new TagKeeper(Flv.Tag.TYPE_AUDIO); TagKeeper videoTag = new TagKeeper(Flv.Tag.TYPE_VIDEO); while (true) { while (!videoTag.isValid() && Flv.readTag(vis, videoTag)) {} while (!audioTag.isValid() && Flv.readTag(ais, audioTag)) {} if (videoTag.isValid()) { if (audioTag.isValid()) { if (videoTag.getTimestamp() <= audioTag.getTimestamp()) { videoTag.write(os); } else { audioTag.write(os); } } else { videoTag.write(os); } } else { if (audioTag.isValid()) { audioTag.write(os); } else { break; } } } } catch (Exception e) { throw new Exception(e); } finally { if (ais != null) { try { ais.close(); } catch (Exception e) {/*ignore*/} } if (vis != null) { try { vis.close(); } catch (Exception e) {/*ignore*/} } if (os != null) { try { os.close(); } catch (Exception e) {/*ignore*/} } } } /** * Converts mp3 audio stream from the <code>flvFile</code> to a new <code>mp3File</code>. * * @param flvFile flv file with mp3 audio stream * @param mp3File mp3 audio file * @throws Exception if an exception occurred */ public static void convertFlvToMp3(final String flvFile, final String mp3File) throws Exception { if (new File(mp3File).exists()) throw new IllegalArgumentException("Audio file exists!"); OutputStream aos = null; try { ConvertFlvToMp3TagProcessor processor = new ConvertFlvToMp3TagProcessor( aos = new BufferedOutputStream(new FileOutputStream(mp3File), 32 * 1024)); processFile(flvFile, processor); } catch (Exception e) { throw new Exception(e); } finally { if (aos != null) { try { aos.close(); } catch (Exception e) {/*ignore*/} } } } /** * Splits flv <code>file</code> into <code>audioFile</code> and <code>videoFile</code> * flv files. * * @param file flv file to split * @param audioFile flv file with audio stream * @param videoFile flv file with video stream * @throws Exception if an exception occurred */ public static void split(final String file, final String audioFile, final String videoFile) throws Exception { if (new File(audioFile).exists()) throw new IllegalArgumentException("Audio file exists!"); if (new File(videoFile).exists()) throw new IllegalArgumentException("Video file exists!"); OutputStream aos = null; OutputStream vos = null; try { SplitTagProcessor processor = new SplitTagProcessor( aos = new BufferedOutputStream(new FileOutputStream(audioFile), 32 * 1024), vos = new BufferedOutputStream(new FileOutputStream(videoFile), 32 * 1024)); processFile(file, processor); } catch (Exception e) { throw new Exception(e); } finally { if (aos != null) { try { aos.close(); } catch (Exception e) {/*ignore*/} } if (vos != null) { try { vos.close(); } catch (Exception e) {/*ignore*/} } } } /** * Processes the <code>file</code> using {@link Flv.ITagProcessor <tt>processor</tt>}. * * @param file flv file * @param processor tag processor * @throws Exception if an exception occurred */ public static void processFile(final String file, final Flv.ITagProcessor processor) throws Exception { FileInputStream fis = null; try { fis = new FileInputStream(new File(file)); Flv.readHeader(fis); while (Flv.readTag(fis, processor)) {} } catch (Exception e) { throw new Exception(e); } finally { if (fis != null) { try { fis.close(); } catch (Exception e) {/*ignore*/} } } } /** * Constructor. */ private FlvTool() { } }