package edu.washington.cs.oneswarm.ui.gwt.server.ffmpeg;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.InvalidPropertiesFormatException;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import edu.washington.cs.oneswarm.ui.gwt.rpc.StringTools;
import edu.washington.cs.oneswarm.ui.gwt.server.OneSwarmUIServiceImpl;
import edu.washington.cs.oneswarm.ui.gwt.server.ffmpeg.FFMpegException.ErrorType;
public class MovieStreamInfo {
private final static int CURRENT_VERISON = 1;
private static Logger logger = Logger.getLogger(MovieStreamInfo.class.getName());
private String audioCodec = "";
private long audioRate;
private long audioSampleRate;
private long bitRate;
private Set<String> container = new HashSet<String>();
private long cropBottom;
private long cropLeft;
private long cropRight;
private boolean cropSet = false;
private long cropTop;
private double duration = -1;
private double frameRate;
private long resolutionX;
private long resolutionY;
private String videoCodec = "";
private long videoRate;
private String ffmpegOut = "";
public String getFfmpegOut() {
return ffmpegOut;
}
private static final HashMap<String, SupportedContainer> flashSupportedFormats;
static {
flashSupportedFormats = new HashMap<String, SupportedContainer>();
flashSupportedFormats.put("flv", new FlashSupportedContainerFLV());
flashSupportedFormats.put("mp4", new FlashSupportedContainerMP4());
flashSupportedFormats.put("mp3", new FlashSupportedContainerAudioOnly());
}
/*
* default at 10 Mbit/s for unknown bitrates
*/
private final static int DEFAULT_BIT_RATE = 10 * 1024 * 1024;
public MovieStreamInfo(File file) throws InvalidPropertiesFormatException, IOException {
// for deserialization
FileInputStream in = new FileInputStream(file);
Properties p = new Properties();
p.loadFromXML(in);
in.close();
long version = getLongProperty(p, "version", 0);
if (version < CURRENT_VERISON) {
throw new IOException("version mismatch (" + version + "<" + CURRENT_VERISON);
}
bitRate = getLongProperty(p, "bitRate", 0);
duration = getDoubleProperty(p, "duration", 0);
container = getStringSetProperty(p, "container", new HashSet<String>());
ffmpegOut = p.getProperty("ffmpegOut", "");
videoCodec = p.getProperty("videoCodec", "");
videoRate = getLongProperty(p, "videoRate", 0);
audioCodec = p.getProperty("audioCodec", "");
audioRate = getLongProperty(p, "audioRate", 0);
audioSampleRate = getLongProperty(p, "audioSampleRate", 0);
resolutionX = getLongProperty(p, "resolutionX", 0);
resolutionY = getLongProperty(p, "resolutionY", 0);
frameRate = getDoubleProperty(p, "frameRate", 0);
cropTop = getLongProperty(p, "cropTop", 0);
cropBottom = getLongProperty(p, "cropBottom", 0);
cropLeft = getLongProperty(p, "cropLeft", 0);
cropRight = getLongProperty(p, "cropRight", 0);
cropSet = getLongProperty(p, "cropSet", 0) == 1;
logger.fine("loaded moviesteaminfo from file: " + toString());
}
public MovieStreamInfo(String ffmpegStdErr) throws FFMpegException {
ffmpegOut = ffmpegStdErr;
logger.finest("creating movie stream info, stderr:\n" + ffmpegStdErr);
// find the duration line
String[] lines = ffmpegStdErr.split("\n");
boolean durationDone = false;
boolean audioDone = false;
boolean videoDone = false;
boolean containerDone = false;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
// String[] lineSplit = line.split(" ");
// System.out.println("'" + line + "'");
try {
if (!durationDone) {
durationDone = this.parseDurationLine(line);
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (!containerDone) {
containerDone = this.parseContainerLine(line);
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (!audioDone) {
audioDone = this.parseAudioLine(line);
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (!videoDone) {
videoDone = this.parseVideoLine(line);
}
} catch (Exception e) {
e.printStackTrace();
}
// for (int j = 0; j < lineSplit.length; j++) {
// String field = lineSplit[j];
// System.out.println("'" + field + "'");
// if()
// }
}
if (duration < 0) {
final FFMpegException mpegException = new FFMpegException(ErrorType.OTHER,
"Unable to parse ffmpeg output");
mpegException.setStdErr(ffmpegStdErr);
throw mpegException;
}
logger.fine("created moviestreaminfo: " + toString());
}
public long getBitRate() {
return bitRate;
}
public long getCropBottom() {
return cropBottom;
}
public long getCropLeft() {
return cropLeft;
}
public long getCropRight() {
return cropRight;
}
public long getCropTop() {
return cropTop;
}
public double getDuration() {
return duration;
}
public long getResolutionX() {
return resolutionX;
}
public long getResolutionY() {
return resolutionY;
}
public boolean hasAudio() {
return audioCodec.length() > 0;
}
public boolean hasVideo() {
return videoCodec.length() > 0;
}
public boolean isCropSet() {
return cropSet;
}
/**
* returns true is the video is ready to be viewed in flash without
* convertion
*
* @return
*/
public boolean isFlashReady() {
logger.fine("checking video for flash support");
SupportedContainer fsc = getContainerRules(this.container);
if (fsc == null) {
logger.fine("no flash support for container: " + createCommaSeparated(container));
return false;
}
logger.finer("found flash supported container '" + fsc.getName() + "', checking codecs");
return fsc.isSupported(this);
}
private static SupportedContainer getContainerRules(Set<String> containers) {
for (String container : containers) {
if (flashSupportedFormats.containsKey(container)) {
return flashSupportedFormats.get(container);
}
}
return null;
}
private boolean parseAudioLine(String l) {
String t = l.trim();
if (t.startsWith("Stream")) {
if (t.contains("Audio:")) {
System.out.println("audio line: '" + l + "'");
String[] s = t.split("Audio:");
if (s.length == 2) {
String entries = s[1].trim();
String[] eSplit = entries.split(", ");
if (eSplit.length > 0) {
this.audioCodec = eSplit[0];
System.out.println("acodec: " + this.audioCodec);
}
if (eSplit.length > 1) {
if (eSplit[1].contains("Hz")) {
this.audioSampleRate = Long.parseLong(eSplit[1].replace(" Hz", ""));
}
}
if (eSplit.length > 4) {
this.audioRate = parseBitRate(eSplit[4]);
System.out.println("a rate: " + audioRate);
}
return true;
}
}
}
return false;
}
private long parseBitRate(String line) {
String[] vRate = line.split(" ");
if (vRate.length > 1) {
if (vRate[1].equals("mb/s")) {
return Math.round(Double.parseDouble(vRate[0]) * 1024 * 1024);
} else if (vRate[1].equals("kb/s")) {
return Math.round(Double.parseDouble(vRate[0]) * 1024);
} else if (vRate[1].equals("b/s")) {
return Math.round(Double.parseDouble(vRate[0]));
} else if (vRate[0].equals("N/A")) {
return DEFAULT_BIT_RATE;
}
}
return 0;
}
private boolean parseContainerLine(String l) {
String t = l.trim();
if (t.startsWith("Input")) {
System.out.println("parsing container line: " + t);
String[] split = t.split(" ");
if (split.length > 3) {
this.container = setFromCommaSeparated(split[2]);
System.out.println("got container: " + container);
return true;
}
}
return false;
}
private boolean parseDurationLine(String line) {
String[] lineSplit = line.split(" ");
boolean d = false;
boolean b = false;
/*
* find the duration field
*/
for (int i = 0; i < lineSplit.length; i++) {
if ("Duration:".equals(lineSplit[i])) {
if (lineSplit.length > i + 2) {
d = true;
this.duration = parseLength(lineSplit[i + 1]);
}
}
}
for (int i = 0; i < lineSplit.length; i++) {
if ("bitrate:".equals(lineSplit[i])) {
if (lineSplit.length > i + 2) {
b = true;
this.bitRate = parseBitRate(lineSplit[i + 1] + " " + lineSplit[i + 2]);
} else if (lineSplit.length > i + 1) {
if ("N/A".equals(lineSplit[i + 1])) {
b = true;
this.bitRate = DEFAULT_BIT_RATE;
}
}
}
}
return b && d;
}
public Map<String, String> getAsUiMap() {
Map<String, String> v = new HashMap<String, String>();
v.put("ffmpegOut", getFfmpegOut());
if (isFlashReady()) {
String rules = getContainerRules(container).name;
v.put(" Flash ready", "yes, with '" + rules + "' rules");
} else {
v.put(" Flash ready", "no, convert needed");
}
v.put(" Duration", StringTools.trim(getDuration(), 1) + " s");
v.put(" Container", getContainerString() + "");
v.put(" Bit rate", StringTools.formatRate(getBitRate(), "bit/s"));
if (hasVideo()) {
v.put("Video codec", getVideoCodec());
if (getVideoRate() > 0) {
v.put("Video bit rate", StringTools.formatRate(getVideoRate(), "bit/s"));
}
v.put("Video Resolution", getResolutionX() + "x" + getResolutionY());
v.put("Video frame rate", getFrameRate() + "");
if (isCropSet()) {
v.put("Video crop", "(" + getCropLeft() + "," + getCropRight() + "," + getCropTop()
+ "," + getCropBottom() + ")");
}
}
if (hasAudio()) {
v.put("Audio codec", getAudioCodec());
if (getAudioRate() > 0) {
v.put("Audio bit rate", StringTools.formatRate(getAudioRate(), "bit/s"));
}
v.put("Audio sample rate", getAudioSampleRate() + " Hz");
}
return v;
}
private double parseLength(String str) {
logger.finest("looking at length line: " + str);
// remove the last ,
if (str.endsWith(",")) {
str = str.replaceAll(",", "");
}
String[] split = str.split(":");
if (split.length == 3) {
double seconds = Double.parseDouble(split[2]);
int minutes = Integer.parseInt(split[1]);
int hours = Integer.parseInt(split[0]);
logger.finest("h=" + hours + " m=" + minutes + " s=" + seconds);
double ret = 3600 * hours + 60 * minutes + seconds;
logger.finest("h=" + hours + " m=" + minutes + " s=" + seconds + " ret=" + ret);
return ret;
}
return -1;
}
private boolean parseVideoLine(String l) {
String t = l.trim();
if (t.startsWith("Stream")) {
if (t.contains("Video:")) {
System.out.println("video line: '" + l + "'");
String[] s = t.split("Video:");
if (s.length == 2) {
String entries = s[1].trim();
String[] eSplit = entries.split(", ");
if (eSplit.length > 0) {
/*
* parse the codec field
*/
this.videoCodec = eSplit[0];
System.out.println("vcodec: " + this.videoCodec);
}
if (eSplit.length > 2) {
/*
* and the resolution field
*/
String res[] = eSplit[2].split(" ");
if (res.length > 0) {
String[] rSplit = res[0].split("x");
if (rSplit.length == 2) {
resolutionX = Integer.parseInt(rSplit[0]);
resolutionY = Integer.parseInt(rSplit[1]);
System.out.println("res: " + resolutionX + "x" + resolutionY);
}
}
}
if (eSplit.length > 3) {
long vRate = parseBitRate(eSplit[3]);
if (vRate > 0) {
this.videoRate = vRate;
System.out.println("video bit rate: " + videoRate);
} else {
// this could be frame rate, test
String[] fr = eSplit[3].split(" ");
if (fr.length > 1 && fr[1].contains("tb(r)")) {
frameRate = Double.parseDouble(fr[0]);
System.out.println("frame rate: " + frameRate);
}
}
}
if (eSplit.length > 4) {
/*
* and the frame rate
*/
String[] fr = eSplit[4].split(" ");
if (fr.length > 0) {
try {
frameRate = Double.parseDouble(fr[0]);
} catch (Exception e) {
frameRate = -1.0;
}
System.out.println("frame rate: " + frameRate);
}
}
return true;
}
}
}
return false;
}
public void setCrop(int top, int bottom, int left, int rigth) {
this.cropTop = top;
this.cropBottom = bottom;
this.cropLeft = left;
this.cropRight = rigth;
this.cropSet = true;
}
public void setDuration(double duration) {
this.duration = duration;
}
public String toString() {
return "container=" + createCommaSeparated(container) + " vcodec=" + videoCodec
+ " acodec=" + audioCodec + " duration=" + duration + " bitrate=" + bitRate
+ " crop=(" + cropTop + "," + cropBottom + "," + cropLeft + "," + cropRight + ")";
}
public Properties getAsProperties() {
Properties p = new Properties();
p.setProperty("version", CURRENT_VERISON + "");
p.setProperty("bitRate", "" + bitRate);
p.setProperty("duration", "" + duration);
p.setProperty("videoCodec", videoCodec);
p.setProperty("videoRate", "" + videoRate);
p.setProperty("audioCodec", audioCodec);
p.setProperty("audioRate", "" + audioRate);
p.setProperty("audioSampleRate", "" + audioSampleRate);
p.setProperty("resolutionX", "" + resolutionX);
p.setProperty("resolutionY", "" + resolutionY);
p.setProperty("frameRate", "" + frameRate);
p.setProperty("cropSet", "" + (cropSet ? 1 : 0));
p.setProperty("cropTop", "" + cropTop);
p.setProperty("cropBottom", "" + cropBottom);
p.setProperty("cropLeft", "" + cropLeft);
p.setProperty("cropRight", "" + cropRight);
p.setProperty("container", createCommaSeparated(container));
p.setProperty("ffmpegOut", ffmpegOut);
return p;
}
public void writeToFile(File f) throws IOException {
Properties p = getAsProperties();
FileOutputStream out = new FileOutputStream(f);
p.storeToXML(out, "");
out.close();
}
private static String createCommaSeparated(Set<String> strings) {
StringBuilder b = new StringBuilder();
for (String s : strings) {
b.append(s + ",");
}
return b.toString();
}
private static double getDoubleProperty(Properties p, String key, double defaultVal) {
if (p.containsKey(key)) {
return Double.parseDouble(p.getProperty(key));
} else {
return defaultVal;
}
}
private static long getLongProperty(Properties p, String key, long defaultVal) {
if (p.containsKey(key)) {
return Long.parseLong(p.getProperty(key));
} else {
return defaultVal;
}
}
private static Set<String> getStringSetProperty(Properties p, String key, Set<String> defaultVal) {
if (p.containsKey(key)) {
String commaSeparated = p.getProperty(key);
return setFromCommaSeparated(commaSeparated);
} else {
return defaultVal;
}
}
public static void main(String[] args) {
StringBuilder b = new StringBuilder();
BufferedReader in;
try {
OneSwarmUIServiceImpl.loadLogger();
logger.setLevel(Level.ALL);
in = new BufferedReader(new FileReader(new File("/tmp/ffmpeg.out")));
String line;
while ((line = in.readLine()) != null) {
b.append(line + "\n");
}
MovieStreamInfo m = new MovieStreamInfo(b.toString());
m.writeToFile(new File("/tmp/m"));
System.out.println(m.toString());
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (FFMpegException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static Set<String> setFromCommaSeparated(String commaSeparated) {
Set<String> set = new HashSet<String>();
if (commaSeparated.contains(",")) {
String[] split = commaSeparated.split(",");
for (String s : split) {
if (s.length() > 0) {
set.add(s);
}
}
} else {
set.add(commaSeparated);
}
return set;
}
static abstract class SupportedContainer {
public static enum VideoHandler {
FLASH, SILVERLIGHT, HTML5;
}
private final String name;
private final VideoHandler videoHandler;
private SupportedContainer(String name, VideoHandler videoHandler) {
this.name = name;
this.videoHandler = videoHandler;
}
public String getName() {
return name;
}
public abstract boolean isSupported(MovieStreamInfo m);
public abstract String[] getSupportedAudioCodecs();
public abstract String[] getSupportedVideoCodecs();
protected boolean isVideoCodecSupported(MovieStreamInfo m) {
for (String supportedCodec : getSupportedVideoCodecs()) {
if (m.videoCodec.equals(supportedCodec)) {
return true;
}
}
return false;
}
protected boolean isAudioCodecSupported(MovieStreamInfo m) {
for (String supportedCodec : getSupportedAudioCodecs()) {
if (m.audioCodec.equals(supportedCodec)) {
return true;
}
}
return false;
}
}
static class FlashSupportedContainerFLV extends SupportedContainer {
public final static String[] SUPPORTED_VIDEO_CODECS = new String[] { "flv", "vp6f", "h264" };
public final static String[] SUPPORTED_AUDIO_CODECS = new String[] { "nellymoser", "mp3",
"aac" };
public FlashSupportedContainerFLV() {
super("flv", VideoHandler.FLASH);
}
@Override
public boolean isSupported(MovieStreamInfo m) {
/*
* this is only for flv containers
*/
if (m.container.contains("flv")) {
logger.finer("checking for flash support, flv: " + toString());
/*
* if the file has video it must be supported by flash to be
* flash ready
*/
if (m.hasVideo() && (isVideoCodecSupported(m) == false)) {
logger.finer("format not supported, hasVideo=" + m.hasVideo()
+ " codec_supported=" + isVideoCodecSupported(m));
return false;
}
logger.finest("video is ok, codec=" + m.videoCodec);
/*
* same goes for audio
*/
if (m.hasAudio() && (isAudioCodecSupported(m) == false)) {
logger.finer("format not supported, hasAudio=" + m.hasAudio()
+ " codec_supported=" + isAudioCodecSupported(m));
return false;
}
/*
* special check for 48000 Hz mp3, not supported...
*/
if (m.audioCodec.equals("mp3")) {
if (m.audioSampleRate == 48000) {
logger.fine("mp3 with 48000 Hz sample rate is not allowed");
return false;
} else {
logger.finest("mp3 sample rate ok (" + m.audioSampleRate + " Hz)");
}
}
logger.finest("audio is ok, codec=" + m.audioCodec);
logger.fine("format is flv supported");
return true;
}
logger.finer("container not flv");
return false;
}
@Override
public String[] getSupportedAudioCodecs() {
return SUPPORTED_AUDIO_CODECS;
}
@Override
public String[] getSupportedVideoCodecs() {
return SUPPORTED_VIDEO_CODECS;
}
}
static class FlashSupportedContainerMP4 extends SupportedContainer {
public final static String[] SUPPORTED_VIDEO_CODECS = new String[] { "h264" };
public final static String[] SUPPORTED_AUDIO_CODECS = new String[] { "aac" };
public FlashSupportedContainerMP4() {
super("mp4", VideoHandler.FLASH);
}
@Override
public boolean isSupported(MovieStreamInfo m) {
/*
* this is only for flv containers
*/
if (m.container.contains("mp4")) {
logger.finer("checking for flash support, mp4: " + toString());
/*
* if the file has video it must be supported by flash to be
* flash ready
*/
if (m.hasVideo() && (isVideoCodecSupported(m) == false)) {
logger.finer("format not supported, hasVideo=" + m.hasVideo()
+ " codec_supported=" + isVideoCodecSupported(m));
return false;
}
logger.finest("video is ok, codec=" + m.videoCodec);
/*
* same goes for audio
*/
if (m.hasAudio() && (isAudioCodecSupported(m) == false)) {
logger.finer("format not supported, hasAudio=" + m.hasAudio()
+ " codec_supported=" + isAudioCodecSupported(m));
return false;
}
logger.finest("audio is ok, codec=" + m.audioCodec);
logger.fine("format is flash mp4 supported");
return true;
}
logger.finer("container not mp4");
return false;
}
@Override
public String[] getSupportedAudioCodecs() {
return SUPPORTED_AUDIO_CODECS;
}
@Override
public String[] getSupportedVideoCodecs() {
return SUPPORTED_VIDEO_CODECS;
}
}
static class FlashSupportedContainerAudioOnly extends SupportedContainer {
private final Set<String> containers = new HashSet<String>();
public final static String[] SUPPORTED_VIDEO_CODECS = new String[] { "" };
public final static String[] SUPPORTED_AUDIO_CODECS = new String[] { "mp3" };// ,
// "aac"
// };
public FlashSupportedContainerAudioOnly() {
super("AudioOnly", VideoHandler.FLASH);
for (String audioContainer : SUPPORTED_AUDIO_CODECS) {
containers.add(audioContainer);
}
}
@Override
public boolean isSupported(MovieStreamInfo m) {
/*
* this is only for audio only containers
*/
boolean containerSupported = false;
for (String container : containers) {
if (m.container.contains(container)) {
containerSupported = true;
}
}
if (containerSupported) {
logger.finer("checking for flash support, audio only containers: " + toString());
/*
* if the file has video it won't work with audio only
* containers...
*/
if (m.hasVideo()) {
logger.finer("format not supported, hasVideo=" + m.hasVideo());
return false;
}
/*
*
*/
if (m.hasAudio() && (isAudioCodecSupported(m) == false)) {
logger.finer("format not supported, hasAudio=" + m.hasAudio()
+ " codec_supported=" + isAudioCodecSupported(m));
return false;
}
logger.finest("audio is ok, codec=" + m.audioCodec);
logger.fine("format is flash audio supported");
return true;
}
logger.finer("container not in (" + createCommaSeparated(containers) + ")");
return false;
}
@Override
public String[] getSupportedAudioCodecs() {
return SUPPORTED_AUDIO_CODECS;
}
@Override
public String[] getSupportedVideoCodecs() {
return SUPPORTED_VIDEO_CODECS;
}
}
/**
* if the video is in h246 it is really expensive to reencode to flash, much
* better to just repackage to mp4
*
* @author isdal
*
*/
static class RepackSupportedContainer extends SupportedContainer {
public final static String[] SUPPORTED_VIDEO_CODECS = new String[] { "h264" };
/*
* it could be supported by more containers, might add more
*/
public final static String[] SUPPORTED_CONTAINERS = new String[] { "mkv", "avi" };
public RepackSupportedContainer() {
super("repack", VideoHandler.FLASH);
}
@Override
public boolean isSupported(MovieStreamInfo m) {
/*
* current ffmpeg doesn't support this due to bug 807
*/
if (true)
return false;
boolean supportedContainer = false;
for (String container : SUPPORTED_CONTAINERS) {
if (m.container.contains(container)) {
supportedContainer = true;
}
}
if (supportedContainer) {
logger.finer("checking for repackage support: " + toString());
/*
* if the file has video it must be supported by flash to be
* flash ready
*/
if (m.hasVideo() && (isVideoCodecSupported(m) == false)) {
logger.finer("format not supported, hasVideo=" + m.hasVideo()
+ " codec_supported=" + isVideoCodecSupported(m));
return false;
}
logger.finest("video is ok, codec=" + m.videoCodec);
logger.fine("format is repack supported");
return true;
}
logger.finer("container not repack supported");
return false;
}
@Override
public String[] getSupportedAudioCodecs() {
return new String[0];
}
@Override
public String[] getSupportedVideoCodecs() {
return SUPPORTED_VIDEO_CODECS;
}
}
public String getAudioCodec() {
return audioCodec;
}
public long getAudioRate() {
return audioRate;
}
public long getAudioSampleRate() {
return audioSampleRate;
}
public Set<String> getContainer() {
return container;
}
public String getContainerString() {
return createCommaSeparated(container);
}
public double getFrameRate() {
return frameRate;
}
public String getVideoCodec() {
return videoCodec;
}
public long getVideoRate() {
return videoRate;
}
}