OSDN Git Service

5299af32f4958cf0b90d16df709dda1a35a8dc30
[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 org.slf4j.Logger;
18 import org.slf4j.LoggerFactory;
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 = LoggerFactory.getLogger(Convert.class);
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.info("convert video:{}, comment:{}", 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.info("動画再生時間を取得できませんでした: {}", 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
202         logger.info("arg: {}", cmdList);
203         return cmdList;
204     }
205     private static final Pattern PATTERN_TIME = Pattern.compile("time=(\\d+)");
206
207     private int executeFfmpeg(final List<String> cmdList, int duration) throws InterruptedException, IOException {
208         Process process = null;
209         try {
210             logger.info("Processing FFmpeg...");
211             process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
212             BufferedReader ebr = new BufferedReader(new InputStreamReader(
213                     process.getErrorStream()));
214             String msg;
215             while ((msg = ebr.readLine()) != null) {
216                 if (msg.startsWith("frame=")) {
217                     final Matcher m = PATTERN_TIME.matcher(msg);
218                     double per = -1.0;
219                     if (m.find()) {
220                         final String strTime = m.group(1);
221                         final double time = Double.parseDouble(strTime);
222                         per = 100.0 * time / duration;
223                         logger.trace("time:{}, duration:{}", time, duration);
224                     }
225                     publish(new ConvertProgress(PROCESS, per, msg));
226                 } else if (!msg.endsWith("No accelerated colorspace conversion found")) {
227                     logger.warn(msg);
228                 }
229
230                 checkStop();
231             }
232
233             process.waitFor();
234             return process.exitValue();
235         } finally {
236             if (process != null) {
237                 process.destroy();
238             }
239         }
240     }
241
242     private static List<String> createAvfilterOptions(String avfilterOption) {
243         final List<String> avfilterArgs = new ArrayList<>();
244         if (isNotBlank(avfilterOption)) {
245             avfilterArgs.add(avfilterOption);
246         }
247         return avfilterArgs;
248     }
249
250     private static String getVhookArg(ConvertProfile prof, String commPath, String commOwnerPath, boolean isHD) throws
251             UnsupportedEncodingException {
252         StringBuilder sb = new StringBuilder();
253         sb.append("vhext=");
254         sb.append(prof.getVhook().getPath().replace("\\", "/"));
255         if (prof.isCommentOverlay()) {
256             sb.append("|");
257             sb.append("--data-user:");
258             sb.append(URLEncoder.encode(commPath.replace("\\", "/"), "Shift_JIS"));
259             sb.append("|");
260             sb.append("--data-owner:");
261             sb.append(URLEncoder.encode(commOwnerPath.replace("\\", "/"), "Shift_JIS"));
262         }
263         sb.append("|");
264         sb.append("--font:");
265         sb.append(URLEncoder.encode(
266                 prof.getFont().getPath().replace("\\", "/"), "Shift_JIS"));
267         sb.append("|");
268         sb.append("--font-index:");
269         sb.append(prof.getFontIndex());
270         sb.append("|");
271         sb.append("--show-user:");
272         final int dispNum = (prof.getMaxNumOfComment() < 0) ? 30 : prof.getMaxNumOfComment();
273         sb.append(dispNum);
274         sb.append("|");
275         sb.append("--shadow:");
276         sb.append(prof.getShadowIndex());
277         sb.append("|");
278         if (prof.isShowConverting()) {
279             sb.append("--enable-show-video");
280             sb.append("|");
281         }
282         if (!prof.isDisableFontSizeArrange()) {
283             sb.append("--enable-fix-font-size");
284             sb.append("|");
285         }
286         if (prof.isCommentOpaque()) {
287             sb.append("--enable-opaque-comment");
288             sb.append("|");
289         }
290         if (isHD) {
291             sb.append("--aspect-mode:1");
292             sb.append("|");
293         }
294         return sb.toString();
295     }
296
297     protected void checkStop() throws InterruptedException {
298         if (Thread.interrupted()) {
299             throw new InterruptedException("中止要求を受け付けました");
300         }
301     }
302 }