OSDN Git Service

workerのコンストラクタでリスナを受け取る
[coroid/inqubus.git] / frontend / src / saccubus / worker / Convert.java
1 /* $Id$ */
2 package saccubus.worker;
3
4 import static org.apache.commons.io.FilenameUtils.getBaseName;
5 import static org.apache.commons.lang.StringUtils.*;
6
7 import java.io.BufferedReader;
8 import java.io.File;
9 import java.io.IOException;
10 import java.io.InputStreamReader;
11 import java.io.UnsupportedEncodingException;
12 import java.net.URLEncoder;
13 import java.util.ArrayList;
14 import java.util.List;
15 import java.util.logging.Level;
16 import java.util.logging.Logger;
17 import javax.swing.SwingWorker;
18 import saccubus.conv.ConvertToVideoHook;
19 import saccubus.worker.profile.ConvertProfile;
20 import saccubus.worker.profile.ConvertProfile.HideCondition;
21 import saccubus.worker.profile.FfmpegProfile;
22 import saccubus.worker.profile.GeneralProfile;
23 import saccubus.worker.profile.OutputProfile;
24 import yukihane.inqubus.util.OutputNamePattern;
25 import yukihane.mediainfowrapper.Info;
26 import yukihane.mediainfowrapper.MediaInfo;
27 import yukihane.mediainfowrapper.Size;
28 import yukihane.swf.Cws2Fws;
29
30 /**
31  * 動画を(コメント付きに)変換するワーカクラス.
32  * @author yuki
33  */
34 public class Convert extends Worker<ConvertResult, ConvertProgress> {
35
36     private static final Logger logger = Logger.getLogger(Convert.class.getName());
37     private final ConvertProfile profile;
38     private final File videoFile;
39     private final File commentFile;
40
41     public Convert(ConvertProfile profile, File video, File comment) {
42         this(profile, video, comment, null);
43     }
44
45     /**
46      * 変換ワーカコンストラクタ.
47      * @param profile 変換用プロファイル.
48      * @param video 変換元動画.
49      * @param comment 変換元コメント. コメントを付与しない場合はnull.
50      * @param output 変換後出力動画.
51      * @throws IOException 変換失敗.
52      */
53     public Convert(ConvertProfile profile, File video, File comment, SaccubusListener<ConvertProgress> listener) {
54         super(listener);
55         this.profile = profile;
56         this.videoFile = video;
57         this.commentFile = comment;
58     }
59
60     @Override
61     public ConvertResult call() throws Exception {
62         if (!profile.isConvert()) {
63             return new ConvertResult(true, "");
64         }
65
66         final GeneralProfile gene = profile.getGeneralProfile();
67         final OutputProfile outprof = profile.getOutputProfile();
68         final OutputNamePattern pattern = new OutputNamePattern(outprof.getFileName());
69         final String id = outprof.getVideoId();
70         pattern.setId(isNotEmpty(id) ? id : "");
71         final String title = outprof.getTitile();
72         pattern.setTitle(isNotEmpty(title) ? title : "");
73         final String fileName = getBaseName(videoFile.getPath());
74         pattern.setFileName(fileName);
75         pattern.setReplaceFrom(gene.getReplaceFrom());
76         pattern.setReplaceFrom(gene.getReplaceTo());
77         final File outputFile = new File(outprof.getDir(), pattern.createFileName());
78
79         File transformedComment = null;
80         try {
81
82             if (profile.isCommentOverlay()) {
83                 transformedComment = File.createTempFile("vhk", ".tmp", profile.getTempDir());
84                 final HideCondition hide = profile.getNgSetting();
85                 publish(new ConvertProgress("コメントの中間ファイルへの変換中"));
86                 ConvertToVideoHook.convert(commentFile, transformedComment, hide.getId(), hide.getWord());
87             }
88
89             checkStop();
90             publish(new ConvertProgress("動画の変換を開始"));
91
92             final int code = convert(transformedComment, outputFile);
93             if (code != 0) {
94                 throw new IOException("ffmpeg実行失敗: " + outputFile.getPath());
95             }
96             publish(new ConvertProgress("変換が正常に終了しました。"));
97             return new ConvertResult(true, outputFile.getName());
98         } finally {
99             if (transformedComment != null && transformedComment.exists()) {
100                 transformedComment.delete();
101             }
102         }
103     }
104
105     private int convert(File transformedComment, File outputFile) throws InterruptedException, IOException {
106         File fwsFile = null;
107         try {
108             final File tmpCws = File.createTempFile("cws", ".swf", profile.getTempDir());
109             fwsFile = Cws2Fws.createFws(videoFile, tmpCws);
110             tmpCws.delete();
111             final File target = (fwsFile != null) ? fwsFile : videoFile;
112
113             final List<String> arguments = createArguments(target, transformedComment, outputFile);
114             return executeFfmpeg(arguments);
115         } finally {
116             if (fwsFile != null && fwsFile.exists()) {
117                 fwsFile.delete();
118             }
119         }
120     }
121
122     private List<String> createArguments(final File targetVideoFile, File transformedComment, File output)
123             throws IOException, UnsupportedEncodingException {
124         final ConvertProfile prof = profile;
125         final FfmpegProfile ffop = prof.getFfmpegOption();
126
127         final List<String> cmdList = new ArrayList<String>();
128         cmdList.add(prof.getFfmpeg().getPath());
129         cmdList.add("-y");
130         final String[] mainOptions = ffop.getMainOption().split(" +");
131         for (String opt : mainOptions) {
132             if (isNotBlank(opt)) {
133                 cmdList.add(opt);
134             }
135         }
136         final String[] inOptions = ffop.getInOption().split(" +");
137         for (String opt : inOptions) {
138             if (isNotBlank(opt)) {
139                 cmdList.add(opt);
140             }
141         }
142         cmdList.add("-i");
143         cmdList.add(targetVideoFile.getPath());
144         final String[] outOptions = ffop.getOutOption().split(" +");
145         for (String opt : outOptions) {
146             if (isNotBlank(opt)) {
147                 cmdList.add(opt);
148             }
149         }
150         final Info info = MediaInfo.getInfo(new File("bin", "MediaInfo"), targetVideoFile);
151         // 4:3 なら1.33, 16:9 なら1.76
152         final boolean isHD = ((double) info.getWidth() / (double) info.getHeight() > 1.5);
153         if (ffop.isResize()) {
154             final Size scaled = (ffop.isAdjustRatio()) ? MediaInfo.adjustSize(info, ffop.getResizeWidth(), ffop.
155                     getResizeHeight()) : new Size(info.getWidth(), info.getHeight());
156             cmdList.add("-s");
157             cmdList.add(scaled.getWidth() + "x" + scaled.getHeight());
158         }
159         final List<String> avfilterArgs = createAvfilterOptions(ffop.getAvfilterOption());
160         if (!prof.isVhookDisabled()) {
161             // TODO 引数冗長
162             final String vhookArg = getVhookArg(prof, prof.isCommentOverlay(), transformedComment.getPath(), isHD);
163             if (isNotBlank(vhookArg)) {
164                 avfilterArgs.add(vhookArg);
165             }
166         }
167         if (!avfilterArgs.isEmpty()) {
168             cmdList.add("-vfilters");
169             final String args = "\"" + join(avfilterArgs, ", ") + "\"";
170             cmdList.add(args);
171         }
172         cmdList.add(output.getPath());
173         final StringBuilder argMsg = new StringBuilder();
174         argMsg.append("arg:");
175         for (String s : cmdList) {
176             argMsg.append(" ").append(s);
177         }
178         logger.log(Level.INFO, argMsg.toString());
179         return cmdList;
180     }
181
182     private int executeFfmpeg(final List<String> cmdList) throws InterruptedException, IOException {
183         Process process = null;
184         try {
185             logger.log(Level.INFO, "Processing FFmpeg...");
186             process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
187             BufferedReader ebr = new BufferedReader(new InputStreamReader(
188                     process.getErrorStream()));
189             String msg;
190             while ((msg = ebr.readLine()) != null) {
191                 if (msg.startsWith("frame=")) {
192                     publish(new ConvertProgress(msg));
193                 } else if (!msg.endsWith("No accelerated colorspace conversion found")) {
194                     logger.log(Level.INFO, msg);
195                 }
196
197                 checkStop();
198             }
199
200             process.waitFor();
201             return process.exitValue();
202         } finally {
203             // TODO 正常終了した場合もdestroyしていいのか?
204             if (process != null) {
205                 process.destroy();
206             }
207         }
208     }
209
210     private static List<String> createAvfilterOptions(String avfilterOption) {
211         final List<String> avfilterArgs = new ArrayList<String>();
212         if (isNotBlank(avfilterOption)) {
213             avfilterArgs.add(avfilterOption);
214         }
215         return avfilterArgs;
216     }
217
218     private static String getVhookArg(ConvertProfile prof, boolean addComment, String commPath, boolean isHD) throws
219             UnsupportedEncodingException {
220         StringBuilder sb = new StringBuilder();
221         sb.append("vhext=");
222         sb.append(prof.getVhook().getPath().replace("\\", "/"));
223         if (addComment) {
224             sb.append("|");
225             sb.append("--data-user:");
226             sb.append(URLEncoder.encode(commPath.replace("\\", "/"), "Shift_JIS"));
227         }
228         sb.append("|");
229         sb.append("--font:");
230         sb.append(URLEncoder.encode(
231                 prof.getFont().getPath().replace("\\", "/"), "Shift_JIS"));
232         sb.append("|");
233         sb.append("--font-index:");
234         sb.append(prof.getFontIndex());
235         sb.append("|");
236         sb.append("--show-user:");
237         sb.append(prof.getMaxNumOfComment());
238         sb.append("|");
239         sb.append("--shadow:");
240         sb.append(prof.getShadowIndex());
241         sb.append("|");
242         if (prof.isShowConverting()) {
243             sb.append("--enable-show-video");
244             sb.append("|");
245         }
246         if (!prof.isDisableFontSizeArrange()) {
247             sb.append("--enable-fix-font-size");
248             sb.append("|");
249         }
250         if (prof.isCommentOpaque()) {
251             sb.append("--enable-opaque-comment");
252             sb.append("|");
253         }
254         if (isHD) {
255             sb.append("--aspect-mode:1");
256             sb.append("|");
257         }
258         return sb.toString();
259     }
260
261     protected void checkStop() throws InterruptedException {
262         if (Thread.interrupted()) {
263             throw new InterruptedException("中止要求を受け付けました");
264         }
265     }
266 }