OSDN Git Service

b8449f308d375f184afdecbe8278a8a6624f59c5
[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.CommentType;
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(CommentType.NORMAL), commentFile, transformedComment, hide.getId(),
100                         hide.getWord());
101
102                 publish(new ConvertProgress(PROCESS, -1.0, "投稿者コメントの中間ファイルへの変換中"));
103                 ConvertToVideoHook.convert(EnumSet.of(CommentType.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実行失敗(code " + code + "): " + 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(profile.getMediaInfo(), 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+):(\\d+):(\\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 double hour = Integer.parseInt(m.group(1));
221                         final double min = Integer.parseInt(m.group(2));
222                         final double sec = Integer.parseInt(m.group(3));
223                         final double time = ((hour * 60) + min) * 60 + sec;
224                         per = 100.0 * time / duration;
225                         if (logger.isTraceEnabled()) {
226                             logger.trace("time:{}, duration:{}", time, duration);
227                             logger.trace(msg);
228                         }
229                     }
230                     publish(new ConvertProgress(PROCESS, per, msg));
231                 } else if (!msg.endsWith("No accelerated colorspace conversion found")) {
232                     logger.warn(msg);
233                 } else {
234                     logger.info(msg);
235                 }
236
237                 checkStop();
238             }
239
240             process.waitFor();
241             return process.exitValue();
242         } finally {
243             if (process != null) {
244                 process.destroy();
245             }
246         }
247     }
248
249     private static List<String> createAvfilterOptions(String avfilterOption) {
250         final List<String> avfilterArgs = new ArrayList<>();
251         if (isNotBlank(avfilterOption)) {
252             avfilterArgs.add(avfilterOption);
253         }
254         return avfilterArgs;
255     }
256
257     private static String getVhookArg(ConvertProfile prof, String commPath, String commOwnerPath, boolean isHD) throws
258             UnsupportedEncodingException {
259         StringBuilder sb = new StringBuilder();
260         sb.append("vhext=");
261         sb.append(prof.getVhook().getPath().replace("\\", "/"));
262         if (prof.isCommentOverlay()) {
263             sb.append("|");
264             sb.append("--data-user:");
265             sb.append(URLEncoder.encode(commPath.replace("\\", "/"), "Shift_JIS"));
266             sb.append("|");
267             sb.append("--data-owner:");
268             sb.append(URLEncoder.encode(commOwnerPath.replace("\\", "/"), "Shift_JIS"));
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 }