package org.jcodec.containers.flv;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.util.Arrays.asList;
import org.jcodec.codecs.h264.H264Utils;
import org.jcodec.codecs.h264.io.model.PictureParameterSet;
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
import org.jcodec.codecs.h264.mp4.AvcCBox;
import org.jcodec.common.AudioFormat;
import org.jcodec.common.Codec;
import org.jcodec.common.StringUtils;
import org.jcodec.common.io.IOUtils;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.logging.Logger;
import org.jcodec.common.tools.MainUtils;
import org.jcodec.common.tools.MainUtils.Cmd;
import org.jcodec.common.tools.ToJSON;
import org.jcodec.containers.flv.FLVTag.AacAudioTagHeader;
import org.jcodec.containers.flv.FLVTag.AudioTagHeader;
import org.jcodec.containers.flv.FLVTag.AvcVideoTagHeader;
import org.jcodec.containers.flv.FLVTag.Type;
import org.jcodec.containers.flv.FLVTag.VideoTagHeader;
import org.jcodec.platform.Platform;
import java.io.File;
import java.io.IOException;
import java.lang.System;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* Makes a clip out of an FLV
*
* @author Stanislav Vitvitskyy
*
*/
public class FLVTool {
private static Map<String, PacketProcessorFactory> processors = new HashMap<String, PacketProcessorFactory>();
static {
processors.put("clip", new ClipPacketProcessor.Factory());
processors.put("fix_pts", new FixPtsProcessor.Factory());
processors.put("info", new InfoPacketProcessor.Factory());
processors.put("shift_pts", new ShiftPtsProcessor.Factory());
}
public static void main1(String[] args) throws IOException {
if (args.length < 1) {
printGenericHelp();
return;
}
String command = args[0];
Cmd cmd = MainUtils.parseArguments(Platform.copyOfRangeO(args, 1, args.length));
if (cmd.args.length < 1) {
MainUtils.printHelpCmd(command, processors.get(command).getFlags(), asList("file _in", "?file out"));
return;
}
int maxPackets = cmd.getIntegerFlagD("max-packets", Integer.MAX_VALUE);
PacketProcessor processor = getProcessor(command, cmd);
if (processor == null) {
System.err.println("Unknown command: " + command);
printGenericHelp();
return;
}
SeekableByteChannel _in = null;
SeekableByteChannel out = null;
try {
_in = NIOUtils.readableChannel(new File(cmd.getArg(0)));
if (processor.hasOutput())
out = NIOUtils.writableChannel(new File(cmd.getArg(1)));
FLVReader demuxer = new FLVReader(_in);
FLVWriter muxer = new FLVWriter(out);
FLVTag pkt = null;
for (int i = 0; i < maxPackets && (pkt = demuxer.readNextPacket()) != null; i++) {
if (!processor.processPacket(pkt, muxer))
break;
}
processor.finish(muxer);
if (processor.hasOutput())
muxer.finish();
} finally {
IOUtils.closeQuietly(_in);
IOUtils.closeQuietly(out);
}
}
private static void printGenericHelp() {
System.err.println("Syntax: <command> [flags] <file in> [file out]\nWhere command is: ["
+ StringUtils.joinS(processors.keySet().toArray(new String[0]), ", ") + "].");
}
private static PacketProcessor getProcessor(String command, Cmd cmd) {
PacketProcessorFactory factory = processors.get(command);
if (factory == null)
return null;
return factory.newPacketProcessor(cmd);
}
public static interface PacketProcessor {
boolean processPacket(FLVTag pkt, FLVWriter writer) throws IOException;
boolean hasOutput();
void finish(FLVWriter muxer) throws IOException;
}
public static interface PacketProcessorFactory {
PacketProcessor newPacketProcessor(Cmd flags);
Map<String, String> getFlags();
}
/**
* A packet processor that clips the flv between the given timestamps
*
*/
public static class ClipPacketProcessor implements PacketProcessor {
private static FLVTag h264Config;
private boolean copying = false;
private Double from;
private Double to;
public static class Factory implements PacketProcessorFactory {
@Override
public PacketProcessor newPacketProcessor(Cmd flags) {
return new ClipPacketProcessor(flags.getDoubleFlag("from"), flags.getDoubleFlag("to"));
}
@Override
public Map<String, String> getFlags() {
HashMap<String, String> map = new HashMap<String, String>();
map.put("from", "From timestamp (in seconds, i.e 67.49)");
map.put("from", "From timestamp (_in seconds, i.e 67.49)");
map.put("to", "To timestamp");
return map;
}
}
public ClipPacketProcessor(Double from, Double to) {
this.from = from;
this.to = to;
}
public boolean processPacket(FLVTag pkt, FLVWriter writer) throws IOException {
if (pkt.getType() == Type.VIDEO && pkt.getTagHeader().getCodec() == Codec.H264) {
if (((AvcVideoTagHeader) pkt.getTagHeader()).getAvcPacketType() == 0) {
h264Config = pkt;
System.out.println("GOT AVCC");
}
}
if (!copying && (from == null || pkt.getPtsD() > from) && pkt.getType() == Type.VIDEO && pkt.isKeyFrame()
&& h264Config != null) {
System.out.println("Starting at packet: " + ToJSON.toJSON(pkt));
copying = true;
h264Config.setPts(pkt.getPts());
writer.addPacket(h264Config);
}
if ((to != null && pkt.getPtsD() >= to)) {
System.out.println("Stopping at packet: " + ToJSON.toJSON(pkt));
return false;
}
if (copying)
writer.addPacket(pkt);
return true;
}
@Override
public void finish(FLVWriter muxer) {
}
@Override
public boolean hasOutput() {
return true;
}
}
/**
* A packet processor that forces a certain FPS
*
*/
public static class FixPtsProcessor implements PacketProcessor {
private double lastPtsAudio = 0;
private double lastPtsVideo = 0;
private List<FLVTag> tags;
private int audioTagsInQueue;
private int videoTagsInQueue;
private static final double CORRECTION_PACE = 0.33;
public static class Factory implements PacketProcessorFactory {
@Override
public PacketProcessor newPacketProcessor(Cmd flags) {
return new FixPtsProcessor();
}
@Override
public Map<String, String> getFlags() {
return new HashMap<String, String>();
}
}
public FixPtsProcessor() {
this.tags = new ArrayList<FLVTag>();
}
public boolean processPacket(FLVTag pkt, FLVWriter writer) throws IOException {
tags.add(pkt);
if (pkt.getType() == Type.AUDIO) {
++audioTagsInQueue;
} else if (pkt.getType() == Type.VIDEO) {
++videoTagsInQueue;
}
if (tags.size() < 600)
return true;
processOneTag(writer);
return true;
}
private void processOneTag(FLVWriter writer) throws IOException {
FLVTag tag = tags.remove(0);
if (tag.getType() == Type.AUDIO) {
tag.setPts((int) Math.round(lastPtsAudio * 1000));
lastPtsAudio += audioFrameDuration(((AudioTagHeader) tag.getTagHeader()));
--audioTagsInQueue;
} else if (tag.getType() == Type.VIDEO) {
double duration = (((double) 1024 * audioTagsInQueue) / (48000 * videoTagsInQueue));
tag.setPts((int) Math.round(lastPtsVideo * 1000));
lastPtsVideo += min(
(1 + CORRECTION_PACE) * duration,
max((1 - CORRECTION_PACE) * duration, duration + min(1, abs(lastPtsAudio - lastPtsVideo))
* (lastPtsAudio - lastPtsVideo)));
--videoTagsInQueue;
System.out.println(lastPtsVideo + " - " + lastPtsAudio);
} else {
tag.setPts((int) Math.round(lastPtsVideo * 1000));
}
writer.addPacket(tag);
}
private double audioFrameDuration(AudioTagHeader audioTagHeader) {
switch (audioTagHeader.getCodec()) {
case AAC:
return ((double) 1024) / audioTagHeader.getAudioFormat().getSampleRate();
case MP3:
return ((double) 1152) / audioTagHeader.getAudioFormat().getSampleRate();
default:
throw new RuntimeException("Audio codec:" + audioTagHeader.getCodec() + " is not supported.");
}
}
@Override
public void finish(FLVWriter muxer) throws IOException {
while (tags.size() > 0) {
processOneTag(muxer);
}
}
@Override
public boolean hasOutput() {
return true;
}
}
/**
* A packet processor that just dumps info
*
*/
public static class InfoPacketProcessor implements PacketProcessor {
private FLVTag prevVideoTag;
private FLVTag prevAudioTag;
public static class Factory implements PacketProcessorFactory {
private static final String FLAG_CHECK = "check";
private static final String FLAG_STREAM = "stream";
@Override
public PacketProcessor newPacketProcessor(Cmd flags) {
return new InfoPacketProcessor(flags.getBooleanFlagD(FLAG_CHECK, false), flags.getEnumFlagD(FLAG_STREAM,
null, Type.class));
}
@Override
public Map<String, String> getFlags() {
HashMap<String, String> map = new HashMap<String, String>() ;
map.put(FLAG_CHECK, "Check sanity and report errors only, no packet dump will be generated.");
map.put(FLAG_STREAM, "Stream selector, can be one of: ['video', 'audio', 'script'].");
return map;
}
}
private boolean checkOnly;
private Type streamType;
public InfoPacketProcessor(boolean checkOnly, Type streamType) {
this.checkOnly = checkOnly;
this.streamType = streamType;
}
@Override
public boolean processPacket(FLVTag pkt, FLVWriter writer) throws IOException {
if (checkOnly)
return true;
if (pkt.getType() == Type.VIDEO) {
if (streamType == Type.VIDEO || streamType == null) {
if (prevVideoTag != null)
dumpOnePacket(prevVideoTag, pkt.getPts() - prevVideoTag.getPts());
prevVideoTag = pkt;
}
} else if (pkt.getType() == Type.AUDIO) {
if (streamType == Type.AUDIO || streamType == null) {
if (prevAudioTag != null)
dumpOnePacket(prevAudioTag, pkt.getPts() - prevAudioTag.getPts());
prevAudioTag = pkt;
}
} else {
dumpOnePacket(pkt, 0);
}
return true;
}
private void dumpOnePacket(FLVTag pkt, int duration) {
System.out.print("T=" + typeString(pkt.getType()) + "|PTS=" + pkt.getPts() + "|DUR=" + duration + "|"
+ (pkt.isKeyFrame() ? "K" : " ") + "|POS=" + pkt.getPosition());
if (pkt.getTagHeader() instanceof VideoTagHeader) {
VideoTagHeader vt = (VideoTagHeader) pkt.getTagHeader();
System.out.print("|C=" + vt.getCodec() + "|FT=" + vt.getFrameType());
if (vt instanceof AvcVideoTagHeader) {
AvcVideoTagHeader avct = (AvcVideoTagHeader) vt;
System.out.print("|PKT_TYPE=" + avct.getAvcPacketType() + "|COMP_OFF=" + avct.getCompOffset());
if (avct.getAvcPacketType() == 0) {
ByteBuffer frameData = pkt.getData().duplicate();
FLVReader.parseVideoTagHeader(frameData);
AvcCBox avcc = H264Utils.parseAVCCFromBuffer(frameData);
for (SeqParameterSet sps : H264Utils.readSPSFromBufferList(avcc.getSpsList())) {
System.out.println();
System.out.print(" SPS[" + sps.getSeq_parameter_set_id() + "]:" + ToJSON.toJSON(sps));
}
for (PictureParameterSet pps : H264Utils.readPPSFromBufferList(avcc.getPpsList())) {
System.out.println();
System.out.print(" PPS[" + pps.getPic_parameter_set_id() + "]:" + ToJSON.toJSON(pps));
}
}
}
} else if (pkt.getTagHeader() instanceof AudioTagHeader) {
AudioTagHeader at = (AudioTagHeader) pkt.getTagHeader();
AudioFormat format = at.getAudioFormat();
System.out.print("|C=" + at.getCodec() + "|SR=" + format.getSampleRate() + "|SS="
+ (format.getSampleSizeInBits() >> 3) + "|CH=" + format.getChannels());
} else if (pkt.getType() == Type.SCRIPT) {
FLVMetadata metadata = FLVReader.parseMetadata(pkt.getData().duplicate());
if (metadata != null) {
System.out.println();
System.out.print(" Metadata:" + ToJSON.toJSON(metadata));
}
}
System.out.println();
}
private String typeString(Type type) {
return type.toString().substring(0, 1);
}
@Override
public void finish(FLVWriter muxer) throws IOException {
if (prevVideoTag != null)
dumpOnePacket(prevVideoTag, 0);
if (prevAudioTag != null)
dumpOnePacket(prevAudioTag, 0);
}
@Override
public boolean hasOutput() {
return false;
}
}
/**
* A packet processor shifts pts
*
*/
public static class ShiftPtsProcessor implements PacketProcessor {
private static final long WRAP_AROUND_VALUE = 0x80000000L;
private static final int HALF_WRAP_AROUND_VALUE = 0x40000000;
public static class Factory implements PacketProcessorFactory {
@Override
public PacketProcessor newPacketProcessor(Cmd flags) {
return new ShiftPtsProcessor(flags.getIntegerFlagD("to", 0), flags.getIntegerFlag("by"),
flags.getBooleanFlagD("wrap-around", false));
}
@Override
public Map<String, String> getFlags() {
HashMap<String, String> map = new HashMap<String, String>();
map.put("to", "Shift first pts to this value, and all subsequent pts accordingly.");
map.put("by", "Shift all pts by this value.");
map.put("wrap-around", "Expect wrap around of timestamps.");
return map;
}
}
private int shiftTo;
private Integer shiftBy;
private long ptsDelta;
private boolean firstPtsSeen;
private List<FLVTag> savedTags;
private boolean expectWrapAround;
private int prevPts;
public ShiftPtsProcessor(int shiftTo, Integer shiftBy, boolean expectWrapAround) {
this.savedTags = new LinkedList<FLVTag>();
this.shiftTo = shiftTo;
this.shiftBy = shiftBy;
this.expectWrapAround = true;
}
public boolean processPacket(FLVTag pkt, FLVWriter writer) throws IOException {
boolean avcPrivatePacket = pkt.getType() == Type.VIDEO
&& ((VideoTagHeader) pkt.getTagHeader()).getCodec() == Codec.H264
&& ((AvcVideoTagHeader) pkt.getTagHeader()).getAvcPacketType() == 0;
boolean aacPrivatePacket = pkt.getType() == Type.AUDIO
&& ((AudioTagHeader) pkt.getTagHeader()).getCodec() == Codec.AAC
&& ((AacAudioTagHeader) pkt.getTagHeader()).getPacketType() == 0;
boolean validPkt = pkt.getType() != Type.SCRIPT && !avcPrivatePacket && !aacPrivatePacket;
if (expectWrapAround && validPkt && pkt.getPts() < prevPts
&& ((long) prevPts - pkt.getPts() > HALF_WRAP_AROUND_VALUE)) {
Logger.warn("Wrap around detected: " + prevPts + " -> " + pkt.getPts());
if (pkt.getPts() < -HALF_WRAP_AROUND_VALUE) {
ptsDelta += (WRAP_AROUND_VALUE << 1);
} else if (pkt.getPts() >= 0) {
ptsDelta += WRAP_AROUND_VALUE;
}
}
if (validPkt)
prevPts = pkt.getPts();
if (firstPtsSeen) {
writePacket(pkt, writer);
} else {
if (!validPkt) {
savedTags.add(pkt);
} else {
if (shiftBy != null) {
ptsDelta = shiftBy;
if (ptsDelta + pkt.getPts() < 0)
ptsDelta = -pkt.getPts();
} else {
ptsDelta = shiftTo - pkt.getPts();
}
firstPtsSeen = true;
emptySavedTags(writer);
writePacket(pkt, writer);
}
}
return true;
}
private void writePacket(FLVTag pkt, FLVWriter writer) throws IOException {
long newPts = pkt.getPts() + ptsDelta;
if (newPts < 0) {
Logger.warn("Preventing negative pts for tag @" + pkt.getPosition());
if (shiftBy != null)
newPts = 0;
else
newPts = shiftTo;
} else if (newPts >= WRAP_AROUND_VALUE) {
Logger.warn("PTS wrap around @" + pkt.getPosition());
newPts -= WRAP_AROUND_VALUE;
ptsDelta = newPts - pkt.getPts();
}
pkt.setPts((int) newPts);
writer.addPacket(pkt);
}
private void emptySavedTags(FLVWriter muxer) throws IOException {
while (savedTags.size() > 0) {
writePacket(savedTags.remove(0), muxer);
}
}
@Override
public void finish(FLVWriter muxer) throws IOException {
emptySavedTags(muxer);
}
@Override
public boolean hasOutput() {
return true;
}
}
}