OSDN Git Service

オプショナルコメントの中間ファイル生成とffmpeg引数での引き渡し
[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         outputPattern.setReplaceFrom(gene.getReplaceFrom());
88         outputPattern.setReplaceFrom(gene.getReplaceTo());
89         final File outputFile = new File(outprof.getDir(),
90                 outputPattern.createFileName() + profile.getFfmpegOption().getExtOption());
91
92         final Map<CommentType, File> tmpComments = new EnumMap<>(CommentType.class);
93         try {
94
95             if (profile.isCommentOverlay()) {
96                 for (CommentType ct : CommentType.values()) {
97                     tmpComments.put(ct, File.createTempFile("vhk", ".tmp", profile.getTempDir()));
98                 }
99
100                 final HideCondition hide = profile.getNgSetting();
101
102                 for (CommentType ct : CommentType.values()) {
103                     publish(new ConvertProgress(PROCESS, -1.0, ct.toString() + "の中間ファイルへの変換中"));
104                     ConvertToVideoHook.convert(EnumSet.of(ct), commentFile, tmpComments.get(ct),
105                             hide.getId(), hide.getWord());
106                 }
107             }
108
109             checkStop();
110             publish(new ConvertProgress(PROCESS, -1.0, "動画の変換を開始"));
111
112             final int code = convert(outputFile, new EnumMap<>(tmpComments));
113             if (code != 0) {
114                 throw new IOException("ffmpeg実行失敗(code " + code + "): " + outputFile.getPath());
115             }
116             publish(new ConvertProgress(PROCESS, 100.0, "変換が正常に終了しました。"));
117             return new ConvertResult(true, outputFile.getName());
118         } finally {
119             for(File f : tmpComments.values()) {
120                 if(f != null && f.exists()) {
121                     f.delete();
122                 }
123             }
124         }
125     }
126
127     private int convert(File outputFile, Map<CommentType,File> tmpComments) throws InterruptedException, IOException {
128         File fwsFile = null;
129         try {
130             final File tmpCws = File.createTempFile("cws", ".swf", profile.getTempDir());
131             fwsFile = Cws2Fws.createFws(videoFile, tmpCws);
132             tmpCws.delete();
133             final File target = (fwsFile != null) ? fwsFile : videoFile;
134
135             final List<String> arguments = createArguments(target, outputFile, tmpComments);
136             final FfmpegUtil util = new FfmpegUtil(profile.getFfmpeg(), target);
137             int duration;
138             try {
139                 duration = util.getDuration();
140             } catch (IOException ex) {
141                 logger.info("動画再生時間を取得できませんでした: {}", target);
142                 duration = Integer.MAX_VALUE;
143             }
144             return executeFfmpeg(arguments, duration);
145         } finally {
146             if (fwsFile != null && fwsFile.exists()) {
147                 fwsFile.delete();
148             }
149         }
150     }
151
152     private List<String> createArguments(final File targetVideoFile, File output, Map<CommentType,File> comments)
153             throws IOException, UnsupportedEncodingException {
154         final ConvertProfile prof = profile;
155         final FfmpegProfile ffop = prof.getFfmpegOption();
156
157         final List<String> cmdList = new ArrayList<>();
158         cmdList.add(prof.getFfmpeg().getPath());
159         cmdList.add("-y");
160         final String[] mainOptions = ffop.getMainOption().split(" +");
161         for (String opt : mainOptions) {
162             if (isNotBlank(opt)) {
163                 cmdList.add(opt);
164             }
165         }
166         final String[] inOptions = ffop.getInOption().split(" +");
167         for (String opt : inOptions) {
168             if (isNotBlank(opt)) {
169                 cmdList.add(opt);
170             }
171         }
172         cmdList.add("-i");
173         cmdList.add(targetVideoFile.getPath());
174         final String[] outOptions = ffop.getOutOption().split(" +");
175         for (String opt : outOptions) {
176             if (isNotBlank(opt)) {
177                 cmdList.add(opt);
178             }
179         }
180         final Info info = MediaInfo.getInfo(profile.getMediaInfo(), targetVideoFile);
181         // 4:3 なら1.33, 16:9 なら1.76
182         final boolean isHD = ((double) info.getWidth() / (double) info.getHeight() > 1.5);
183         if (ffop.isResize()) {
184             final Size scaled = (ffop.isAdjustRatio()) ? MediaInfo.adjustSize(info, ffop.getResizeWidth(), ffop.
185                     getResizeHeight()) : new Size(info.getWidth(), info.getHeight());
186             cmdList.add("-s");
187             cmdList.add(scaled.getWidth() + "x" + scaled.getHeight());
188         }
189         final List<String> avfilterArgs = createAvfilterOptions(ffop.getAvfilterOption());
190         if (!prof.isVhookDisabled()) {
191             final String vhookArg = getVhookArg(prof, comments, isHD);
192             if (isNotBlank(vhookArg)) {
193                 avfilterArgs.add(vhookArg);
194             }
195         }
196         if (!avfilterArgs.isEmpty()) {
197             cmdList.add("-vfilters");
198             final String args = join(avfilterArgs, ", ");
199             cmdList.add(args);
200         }
201         cmdList.add(output.getPath());
202
203         logger.info("arg: {}", cmdList);
204         return cmdList;
205     }
206     private static final Pattern PATTERN_TIME = Pattern.compile("time=(\\d+):(\\d+):(\\d+)");
207
208     private int executeFfmpeg(final List<String> cmdList, int duration) throws InterruptedException, IOException {
209         Process process = null;
210         try {
211             logger.info("Processing FFmpeg...");
212             process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
213             BufferedReader ebr = new BufferedReader(new InputStreamReader(
214                     process.getErrorStream()));
215             String msg;
216             while ((msg = ebr.readLine()) != null) {
217                 if (msg.startsWith("frame=")) {
218                     final Matcher m = PATTERN_TIME.matcher(msg);
219                     double per = -1.0;
220                     if (m.find()) {
221                         final double hour = Integer.parseInt(m.group(1));
222                         final double min = Integer.parseInt(m.group(2));
223                         final double sec = Integer.parseInt(m.group(3));
224                         final double time = ((hour * 60) + min) * 60 + sec;
225                         per = 100.0 * time / duration;
226                         if (logger.isTraceEnabled()) {
227                             logger.trace("time:{}, duration:{}", time, duration);
228                             logger.trace(msg);
229                         }
230                     }
231                     publish(new ConvertProgress(PROCESS, per, msg));
232                 } else if (!msg.endsWith("No accelerated colorspace conversion found")) {
233                     logger.warn(msg);
234                 } else {
235                     logger.info(msg);
236                 }
237
238                 checkStop();
239             }
240
241             process.waitFor();
242             return process.exitValue();
243         } finally {
244             if (process != null) {
245                 process.destroy();
246             }
247         }
248     }
249
250     private static List<String> createAvfilterOptions(String avfilterOption) {
251         final List<String> avfilterArgs = new ArrayList<>();
252         if (isNotBlank(avfilterOption)) {
253             avfilterArgs.add(avfilterOption);
254         }
255         return avfilterArgs;
256     }
257
258     private static String getVhookArg(ConvertProfile prof, Map<CommentType, File> comments, boolean isHD) throws
259             UnsupportedEncodingException {
260         StringBuilder sb = new StringBuilder();
261         sb.append("vhext=");
262         sb.append(prof.getVhook().getPath().replace("\\", "/"));
263         if (prof.isCommentOverlay()) {
264             for(Entry<CommentType, File> e : comments.entrySet()) {
265                 sb.append("|");
266                 sb.append(e.getKey().getVhookOptionPrefix());
267                 sb.append(URLEncoder.encode(e.getValue().getPath().replace("\\", "/"), "Shift_JIS"));
268             }
269         }
270         sb.append("|");
271         sb.append("--font:");
272         sb.append(URLEncoder.encode(
273                 prof.getFont().getPath().replace("\\", "/"), "Shift_JIS"));
274         sb.append("|");
275         sb.append("--font-index:");
276         sb.append(prof.getFontIndex());
277         sb.append("|");
278         sb.append("--show-user:");
279         final int dispNum = (prof.getMaxNumOfComment() < 0) ? 30 : prof.getMaxNumOfComment();
280         sb.append(dispNum);
281         sb.append("|");
282         sb.append("--shadow:");
283         sb.append(prof.getShadowIndex());
284         sb.append("|");
285         if (prof.isShowConverting()) {
286             sb.append("--enable-show-video");
287             sb.append("|");
288         }
289         if (!prof.isDisableFontSizeArrange()) {
290             sb.append("--enable-fix-font-size");
291             sb.append("|");
292         }
293         if (prof.isCommentOpaque()) {
294             sb.append("--enable-opaque-comment");
295             sb.append("|");
296         }
297         if (isHD) {
298             sb.append("--aspect-mode:1");
299             sb.append("|");
300         }
301         return sb.toString();
302     }
303
304     protected void checkStop() throws InterruptedException {
305         if (Thread.interrupted()) {
306             throw new InterruptedException("中止要求を受け付けました");
307         }
308     }
309 }