OSDN Git Service

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