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