OSDN Git Service

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