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 pattern = new OutputNamePattern(outprof.getFileName());
78         final String id = outprof.getVideoId();
79         pattern.setId(isNotEmpty(id) ? id : "");
80         final String title = outprof.getTitile();
81         pattern.setTitle(isNotEmpty(title) ? title : "");
82         final String fileName = getBaseName(videoFile.getPath());
83         pattern.setFileName(fileName);
84         pattern.setReplaceFrom(gene.getReplaceFrom());
85         pattern.setReplaceFrom(gene.getReplaceTo());
86         final File outputFile = new File(outprof.getDir(), pattern.createFileName());
87
88         File transformedComment = null;
89         File transformedOwner = null;
90         try {
91
92             if (profile.isCommentOverlay()) {
93                 transformedComment = File.createTempFile("vhk", ".tmp", profile.getTempDir());
94                 transformedOwner = File.createTempFile("vown", ".tmp", profile.getTempDir());
95                 final HideCondition hide = profile.getNgSetting();
96
97                 publish(new ConvertProgress(PROCESS, -1.0, "コメントの中間ファイルへの変換中"));
98                 ConvertToVideoHook.convert(EnumSet.of(ProcessType.NORMAL), commentFile, transformedComment, hide.getId(),
99                         hide.getWord());
100
101                 publish(new ConvertProgress(PROCESS, -1.0, "投稿者コメントの中間ファイルへの変換中"));
102                 ConvertToVideoHook.convert(EnumSet.of(ProcessType.OWNER), commentFile, transformedOwner, hide.getId(),
103                         hide.getWord());
104             }
105
106             checkStop();
107             publish(new ConvertProgress(PROCESS, -1.0, "動画の変換を開始"));
108
109             final int code = convert(outputFile, transformedComment, transformedOwner);
110             if (code != 0) {
111                 throw new IOException("ffmpeg実行失敗: " + outputFile.getPath());
112             }
113             publish(new ConvertProgress(PROCESS, 100.0, "変換が正常に終了しました。"));
114             return new ConvertResult(true, outputFile.getName());
115         } finally {
116             if (transformedComment != null && transformedComment.exists()) {
117                 transformedComment.delete();
118             }
119             if (transformedOwner != null && transformedOwner.exists()) {
120                 transformedOwner.delete();
121             }
122         }
123     }
124
125     private int convert(File outputFile, File commentNormal, File commentOwner) 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, commentNormal, commentOwner);
134             final FfmpegUtil util = new FfmpegUtil(profile.getFfmpeg(), target);
135             int duration;
136             try {
137                 duration = util.getDuration();
138             } catch (IOException ex) {
139                 logger.log(Level.FINE, "動画再生時間を取得できませんでした: {0}", 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, File comment, File commentOwner)
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(new File("bin", "MediaInfo"), 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, comment.getPath(), commentOwner.getPath(), 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         final StringBuilder argMsg = new StringBuilder();
201         argMsg.append("arg:");
202         for (String s : cmdList) {
203             argMsg.append(" ").append(s);
204         }
205         logger.log(Level.INFO, argMsg.toString());
206         return cmdList;
207     }
208     private static final Pattern PATTERN_TIME = Pattern.compile("time=(\\d+)");
209
210     private int executeFfmpeg(final List<String> cmdList, int duration) throws InterruptedException, IOException {
211         Process process = null;
212         try {
213             logger.log(Level.INFO, "Processing FFmpeg...");
214             process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
215             BufferedReader ebr = new BufferedReader(new InputStreamReader(
216                     process.getErrorStream()));
217             String msg;
218             while ((msg = ebr.readLine()) != null) {
219                 if (msg.startsWith("frame=")) {
220                     final Matcher m = PATTERN_TIME.matcher(msg);
221                     double per = -1.0;
222                     if (m.find()) {
223                         final String strTime = m.group(1);
224                         final double time = Double.parseDouble(strTime);
225                         per = 100.0 * time / duration;
226                         logger.log(Level.FINEST, "time:{0}, duration:{1}", new Object[]{time, duration});
227                     }
228                     publish(new ConvertProgress(PROCESS, per, msg));
229                 } else if (!msg.endsWith("No accelerated colorspace conversion found")) {
230                     logger.log(Level.INFO, msg);
231                 }
232
233                 checkStop();
234             }
235
236             process.waitFor();
237             return process.exitValue();
238         } finally {
239             if (process != null) {
240                 process.destroy();
241             }
242         }
243     }
244
245     private static List<String> createAvfilterOptions(String avfilterOption) {
246         final List<String> avfilterArgs = new ArrayList<>();
247         if (isNotBlank(avfilterOption)) {
248             avfilterArgs.add(avfilterOption);
249         }
250         return avfilterArgs;
251     }
252
253     private static String getVhookArg(ConvertProfile prof, String commPath, String commOwnerPath, boolean isHD) throws
254             UnsupportedEncodingException {
255         StringBuilder sb = new StringBuilder();
256         sb.append("vhext=");
257         sb.append(prof.getVhook().getPath().replace("\\", "/"));
258         if (prof.isCommentOverlay()) {
259             sb.append("|");
260             sb.append("--data-user:");
261             sb.append(URLEncoder.encode(commPath.replace("\\", "/"), "Shift_JIS"));
262             sb.append("|");
263             sb.append("--data-owner:");
264             sb.append(URLEncoder.encode(commOwnerPath.replace("\\", "/"), "Shift_JIS"));
265         }
266         sb.append("|");
267         sb.append("--font:");
268         sb.append(URLEncoder.encode(
269                 prof.getFont().getPath().replace("\\", "/"), "Shift_JIS"));
270         sb.append("|");
271         sb.append("--font-index:");
272         sb.append(prof.getFontIndex());
273         sb.append("|");
274         sb.append("--show-user:");
275         final int dispNum = (prof.getMaxNumOfComment() < 0) ? 30 : prof.getMaxNumOfComment();
276         sb.append(dispNum);
277         sb.append("|");
278         sb.append("--shadow:");
279         sb.append(prof.getShadowIndex());
280         sb.append("|");
281         if (prof.isShowConverting()) {
282             sb.append("--enable-show-video");
283             sb.append("|");
284         }
285         if (!prof.isDisableFontSizeArrange()) {
286             sb.append("--enable-fix-font-size");
287             sb.append("|");
288         }
289         if (prof.isCommentOpaque()) {
290             sb.append("--enable-opaque-comment");
291             sb.append("|");
292         }
293         if (isHD) {
294             sb.append("--aspect-mode:1");
295             sb.append("|");
296         }
297         return sb.toString();
298     }
299
300     protected void checkStop() throws InterruptedException {
301         if (Thread.interrupted()) {
302             throw new InterruptedException("中止要求を受け付けました");
303         }
304     }
305 }