OSDN Git Service

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