OSDN Git Service

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