PageRenderTime 4115ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/com/tivo/kmttg/task/encode.java

http://kmttg.googlecode.com/
Java | 375 lines | 302 code | 37 blank | 36 comment | 78 complexity | 677b063941f206a2322b593196cb0da2 MD5 | raw file
  1. package com.tivo.kmttg.task;
  2. import java.io.Serializable;
  3. import java.util.Date;
  4. import java.util.Stack;
  5. import java.util.regex.Matcher;
  6. import java.util.regex.Pattern;
  7. import com.tivo.kmttg.main.config;
  8. import com.tivo.kmttg.main.encodeConfig;
  9. import com.tivo.kmttg.main.jobData;
  10. import com.tivo.kmttg.main.jobMonitor;
  11. import com.tivo.kmttg.util.*;
  12. // Encoding class for running background ffmpeg, handbrake, etc. encoding jobs
  13. public class encode implements Serializable {
  14. private static final long serialVersionUID = 1L;
  15. private backgroundProcess process;
  16. private jobData job;
  17. // constructor
  18. public encode(jobData job) {
  19. debug.print("job=" + job);
  20. this.job = job;
  21. }
  22. public backgroundProcess getProcess() {
  23. return process;
  24. }
  25. public Boolean launchJob() {
  26. debug.print("");
  27. Boolean schedule = true;
  28. // Don't encode if encodeFile already exists
  29. if ( file.isFile(job.encodeFile) ) {
  30. if (config.OverwriteFiles == 0) {
  31. log.warn("SKIPPING ENCODE, FILE ALREADY EXISTS: " + job.encodeFile);
  32. // Schedule an AtomicParsley job if relevant
  33. scheduleAtomicParsley();
  34. schedule = false;
  35. } else {
  36. log.warn("OVERWRITING EXISTING FILE: " + job.encodeFile);
  37. }
  38. }
  39. // Decide which file needs to be encoded and update args accordingly
  40. String mpeg;
  41. if ( file.isFile(job.mpegFile_cut) ) {
  42. mpeg = job.mpegFile_cut;
  43. } else {
  44. mpeg = job.mpegFile;
  45. if (config.VrdReview_noCuts == 1) {
  46. // Look for VRD default edit file output
  47. String tryit = string.replaceSuffix(mpeg, " (02).mpg");
  48. if (file.isFile(tryit))
  49. mpeg = tryit;
  50. }
  51. }
  52. if ( ! file.isFile(mpeg) ) {
  53. log.error("mpeg file not given or doesn't exist: " + mpeg);
  54. schedule = false;
  55. }
  56. job.inputFile = mpeg;
  57. if (schedule) {
  58. if ( ! encodeConfig.isValidEncodeName(job.encodeName) ) {
  59. log.error("invalid encoding profile for this job: " + job.encodeName);
  60. schedule = false;
  61. }
  62. }
  63. if (schedule) {
  64. // Create sub-folders for output file if needed
  65. if ( ! jobMonitor.createSubFolders(job.encodeFile, job) ) {
  66. schedule = false;
  67. }
  68. }
  69. if (schedule) {
  70. if ( start() ) {
  71. job.process_encode = this;
  72. jobMonitor.updateJobStatus(job, "running");
  73. job.time = new Date().getTime();
  74. }
  75. return true;
  76. } else {
  77. return false;
  78. }
  79. }
  80. // Return false if starting command fails, true otherwise
  81. private Boolean start() {
  82. debug.print("");
  83. if ( ! encodeConfig.isValidEncodeName(job.encodeName) ) {
  84. jobMonitor.removeFromJobList(job);
  85. return false;
  86. }
  87. Stack<String> command = encodeConfig.getFullCommand(
  88. job.encodeName, job.inputFile, job.encodeFile, job.srtFile
  89. );
  90. // This deals with potential ###xHEIGHT or WIDTHx### keywords in encoding profile
  91. command = ffmpeg.getOutputDimensions(job.inputFile, command);
  92. process = new backgroundProcess();
  93. log.print(">> ENCODING WITH PROFILE '" + job.encodeName + "' TO FILE " + job.encodeFile + " ...");
  94. if ( process.run(command) ) {
  95. log.print(process.toString());
  96. } else {
  97. log.error("Failed to start command: " + process.toString());
  98. process.printStderr();
  99. process = null;
  100. jobMonitor.removeFromJobList(job);
  101. return false;
  102. }
  103. return true;
  104. }
  105. public void kill() {
  106. debug.print("");
  107. process.kill();
  108. log.warn("Killing '" + job.type + "' job: " + process.toString());
  109. }
  110. // Check status of a currently running job
  111. // Returns true if still running, false if job finished
  112. // If job is finished then check result
  113. public Boolean check() {
  114. //debug.print("");
  115. int exit_code = process.exitStatus();
  116. if (exit_code == -1) {
  117. // Still running
  118. if (config.GUIMODE) {
  119. String t = jobMonitor.getElapsedTime(job.time);
  120. String size = null;
  121. if ( file.isFile(job.encodeFile) ) {
  122. size = String.format("%.2f MB", (float)file.size(job.encodeFile)/Math.pow(2,20));
  123. if (size.equals("0.00 MB")) size = null;
  124. }
  125. // Try and determine pct complete
  126. int pct = -1;
  127. if (process.toString().contains(config.handbrake)) {
  128. // Get pct complete from handbrake stdout
  129. pct = handbrakeGetPct();
  130. }
  131. else if (process.toString().contains(config.mencoder)) {
  132. // Get pct complete from mencoder stdout
  133. pct = mencoderGetPct();
  134. }
  135. else if (process.toString().contains("ffmpeg")) {
  136. // Get duration from ffmpeg stderr if not yet available
  137. if ( job.duration == null ) {
  138. long duration = ffmpegGetDuration();
  139. if ( duration > 0 ) {
  140. job.duration = duration;
  141. }
  142. }
  143. // Get time from ffmpeg stderr
  144. long time = ffmpegGetTime();
  145. if (job.duration != null && time > 0) {
  146. Long duration = (Long)job.duration;
  147. pct = Integer.parseInt(String.format("%d", time*100/duration));
  148. }
  149. }
  150. else {
  151. // Some other encoder
  152. pct = -2;
  153. }
  154. String status = t;
  155. if ( jobMonitor.isFirstJobInMonitor(job) ) {
  156. // If 1st job then update title with pct complete
  157. if (pct > -1) {
  158. String title = String.format("encode: %d%% %s", pct, config.kmttg);
  159. config.gui.setTitle(title);
  160. config.gui.progressBar_setValue(pct);
  161. }
  162. if (pct == -2) {
  163. String title = String.format("encode: %s %s", t, config.kmttg);
  164. config.gui.setTitle(title);
  165. }
  166. } else {
  167. if (pct > -1)
  168. status = String.format("%d%%",pct);
  169. }
  170. // Update STATUS column
  171. if (size != null) status += "---" + size;
  172. config.gui.jobTab_UpdateJobMonitorRowStatus(job, status);
  173. }
  174. return true;
  175. } else {
  176. // Job finished
  177. if (config.GUIMODE) {
  178. if ( jobMonitor.isFirstJobInMonitor(job) ) {
  179. config.gui.setTitle(config.kmttg);
  180. config.gui.progressBar_setValue(0);
  181. }
  182. }
  183. jobMonitor.removeFromJobList(job);
  184. // Check for problems
  185. int failed = 0;
  186. // No or empty output file means problems
  187. if ( ! file.isFile(job.encodeFile) || file.isEmpty(job.encodeFile) ) {
  188. failed = 1;
  189. }
  190. if (failed == 1) {
  191. log.error("encoding failed (exit code: " + exit_code + " ) - check command: " + process.toString());
  192. process.printStderr();
  193. } else {
  194. log.warn("encoding job completed: " + jobMonitor.getElapsedTime(job.time));
  195. log.print("---DONE--- job=" + job.type + " output=" + job.encodeFile);
  196. // Remove mpegFile.qsfix file if present
  197. String fix;
  198. if (job.mpegFile.matches("^.+\\.qsfix$"))
  199. fix = job.mpegFile;
  200. else
  201. fix = job.mpegFile + ".qsfix";
  202. if (file.isFile(fix)) {
  203. if (file.delete(fix)) log.print("(Deleted file: " + fix + ")");
  204. }
  205. // Remove .mpg file if option enabled
  206. // If there is a second encode job working off the same source file,
  207. // then the source should not be removed.
  208. if (!job.hasMoreEncodingJobs && config.RemoveMpegFile == 1) {
  209. if ( file.delete(job.inputFile) ) {
  210. log.print("(Deleted file: " + job.inputFile + ")");
  211. } else {
  212. log.error("Failed to delete file: "+ job.inputFile);
  213. }
  214. if ( file.delete(job.mpegFile)) {
  215. log.print("(Deleted file: " + job.mpegFile + ")");
  216. }
  217. }
  218. // Schedule an AtomicParsley job if relevant
  219. scheduleAtomicParsley();
  220. }
  221. }
  222. return false;
  223. }
  224. private void scheduleAtomicParsley() {
  225. // Schedule an AtomicParsley job if relevant
  226. if (file.isFile(config.AtomicParsley)) {
  227. job.metaFile = job.encodeFile + ".txt";
  228. if ( ! file.isFile(job.metaFile) ) {
  229. job.metaFile = job.mpegFile_cut + ".txt";
  230. }
  231. if ( ! file.isFile(job.metaFile) ) {
  232. job.metaFile = job.mpegFile + ".txt";
  233. }
  234. if ( ! file.isFile(job.metaFile) ) {
  235. job.metaFile = string.replaceSuffix(job.encodeFile, ".txt.TiVo");
  236. }
  237. if ( file.isFile(job.metaFile) &&
  238. (job.encodeFile.toLowerCase().endsWith(".mp4") ||
  239. job.encodeFile.toLowerCase().endsWith(".m4v")) ) {
  240. jobData new_job = new jobData();
  241. new_job.source = job.source;
  242. new_job.tivoName = job.tivoName;
  243. new_job.type = "atomic";
  244. new_job.name = config.AtomicParsley;
  245. new_job.encodeFile = job.encodeFile;
  246. new_job.metaFile = job.metaFile;
  247. jobMonitor.submitNewJob(new_job);
  248. }
  249. }
  250. }
  251. // Obtain total duration time from ffmpeg stderr
  252. private long ffmpegGetDuration() {
  253. Stack<String> stderr = process.getStderr();
  254. String line;
  255. for (int i=0; i<stderr.size(); ++i) {
  256. line = stderr.get(i);
  257. if (line.contains("Duration:")) {
  258. String[] l = line.split("\\s+");
  259. String d = l[2].replaceFirst(",", "");
  260. String[] ll = d.split(":");
  261. try {
  262. float hour = Float.parseFloat(ll[0]);
  263. float min = Float.parseFloat(ll[1]);
  264. float sec = Float.parseFloat(ll[2]);
  265. long ms = (long)(3600*hour + 60*min + sec)*1000;
  266. return ms;
  267. }
  268. catch (NumberFormatException n) {
  269. }
  270. }
  271. }
  272. return 0;
  273. }
  274. // Obtain current length in ms of encoding file from ffmpeg stderr
  275. private long ffmpegGetTime() {
  276. String last = process.getStderrLast();
  277. if (last.matches("")) return 0;
  278. if (last.contains("time=")) {
  279. String[] l = last.split("time=");
  280. String[] ll = l[l.length-1].split("\\s+");
  281. float sec = (float)0;
  282. try {
  283. if (ll[0].contains(":")) {
  284. // "HH:MM:SS.MS" format
  285. Pattern p = Pattern.compile("(\\d+):(\\d+):(\\d+).(\\d+)");
  286. Matcher m = p.matcher(ll[0]);
  287. if (m.matches()) {
  288. long HH = Long.parseLong(m.group(1));
  289. long MM = Long.parseLong(m.group(2));
  290. long SS = Long.parseLong(m.group(3));
  291. long MS = Long.parseLong(m.group(4));
  292. long ms = MS + 1000*(SS+60*MM+60*60*HH);
  293. return ms;
  294. }
  295. } else {
  296. sec = Float.parseFloat(ll[0]);
  297. }
  298. }
  299. catch (NumberFormatException n) {
  300. }
  301. return (long)sec*1000;
  302. }
  303. return 0;
  304. }
  305. // Obtain pct complete from handbrake stdout
  306. private int handbrakeGetPct() {
  307. String last = process.getStdoutLast();
  308. if (last.matches("")) return 0;
  309. if (last.contains("Encoding")) {
  310. String[] all = last.split("Encoding");
  311. String pct_str = all[all.length-1].replaceFirst("^.+,\\s+(.+)\\s+%.*$", "$1");
  312. try {
  313. return (int)Float.parseFloat(pct_str);
  314. }
  315. catch (NumberFormatException n) {
  316. return 0;
  317. }
  318. }
  319. return 0;
  320. }
  321. // Obtain pct complete from handbrake stdout
  322. private int mencoderGetPct() {
  323. String last = process.getStdoutLast();
  324. if (last.matches("")) return 0;
  325. Pattern pat = Pattern.compile("(\\d+)%");
  326. Matcher match = pat.matcher(last);
  327. if (match.find()) {
  328. String pct_str = match.group(1);
  329. try {
  330. return (int)Float.parseFloat(pct_str);
  331. }
  332. catch (NumberFormatException n) {
  333. return 0;
  334. }
  335. }
  336. return 0;
  337. }
  338. }