OSDN Git Service

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