2 package saccubus.worker.impl.convert;
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.*;
8 import java.io.BufferedReader;
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.List;
16 import java.util.logging.Level;
17 import java.util.logging.Logger;
18 import java.util.regex.Matcher;
19 import java.util.regex.Pattern;
20 import saccubus.conv.ConvertToVideoHook;
21 import saccubus.util.FfmpegUtil;
22 import saccubus.worker.Worker;
23 import saccubus.worker.WorkerListener;
24 import saccubus.worker.profile.ConvertProfile;
25 import saccubus.worker.profile.ConvertProfile.HideCondition;
26 import saccubus.worker.profile.FfmpegProfile;
27 import saccubus.worker.profile.GeneralProfile;
28 import saccubus.worker.profile.OutputProfile;
29 import yukihane.inqubus.util.OutputNamePattern;
30 import yukihane.mediainfowrapper.Info;
31 import yukihane.mediainfowrapper.MediaInfo;
32 import yukihane.mediainfowrapper.Size;
33 import yukihane.swf.Cws2Fws;
36 * 動画を(コメント付きに)変換するワーカクラス.
39 public class Convert extends Worker<ConvertResult, ConvertProgress> {
41 private static final Logger logger = Logger.getLogger(Convert.class.getName());
42 private final ConvertProfile profile;
43 private final File videoFile;
44 private final File commentFile;
46 public Convert(ConvertProfile profile, File video, File comment) {
47 this(profile, video, comment, null);
52 * @param profile 変換用プロファイル.
54 * @param comment 変換元コメント. コメントを付与しない場合はnull.
55 * @param output 変換後出力動画.
56 * @throws IOException 変換失敗.
58 public Convert(ConvertProfile profile, File video, File comment,
59 WorkerListener<ConvertResult, ConvertProgress> listener) {
61 this.profile = profile;
62 this.videoFile = video;
63 this.commentFile = comment;
64 logger.log(Level.INFO, "convert video:{0}, comment:{1}", new Object[]{videoFile, commentFile});
68 protected ConvertResult work() throws Exception {
69 if (!profile.isConvert()) {
70 return new ConvertResult(true, "");
73 final GeneralProfile gene = profile.getGeneralProfile();
74 final OutputProfile outprof = profile.getOutputProfile();
75 final OutputNamePattern pattern = new OutputNamePattern(outprof.getFileName());
76 final String id = outprof.getVideoId();
77 pattern.setId(isNotEmpty(id) ? id : "");
78 final String title = outprof.getTitile();
79 pattern.setTitle(isNotEmpty(title) ? title : "");
80 final String fileName = getBaseName(videoFile.getPath());
81 pattern.setFileName(fileName);
82 pattern.setReplaceFrom(gene.getReplaceFrom());
83 pattern.setReplaceFrom(gene.getReplaceTo());
84 final File outputFile = new File(outprof.getDir(), pattern.createFileName());
86 File transformedComment = null;
89 if (profile.isCommentOverlay()) {
90 transformedComment = File.createTempFile("vhk", ".tmp", profile.getTempDir());
91 final HideCondition hide = profile.getNgSetting();
92 publish(new ConvertProgress(PROCESS, -1.0, "コメントの中間ファイルへの変換中"));
93 ConvertToVideoHook.convert(commentFile, transformedComment, hide.getId(), hide.getWord());
97 publish(new ConvertProgress(PROCESS, -1.0, "動画の変換を開始"));
99 final int code = convert(transformedComment, outputFile);
101 throw new IOException("ffmpeg実行失敗: " + outputFile.getPath());
103 publish(new ConvertProgress(PROCESS, 100.0, "変換が正常に終了しました。"));
104 return new ConvertResult(true, outputFile.getName());
106 if (transformedComment != null && transformedComment.exists()) {
107 transformedComment.delete();
112 private int convert(File transformedComment, File outputFile) throws InterruptedException, IOException {
115 final File tmpCws = File.createTempFile("cws", ".swf", profile.getTempDir());
116 fwsFile = Cws2Fws.createFws(videoFile, tmpCws);
118 final File target = (fwsFile != null) ? fwsFile : videoFile;
120 final List<String> arguments = createArguments(target, transformedComment, outputFile);
121 final int duration = new FfmpegUtil(profile.getFfmpeg(), target).getDuration();
122 return executeFfmpeg(arguments, duration);
124 if (fwsFile != null && fwsFile.exists()) {
130 private List<String> createArguments(final File targetVideoFile, File transformedComment, File output)
131 throws IOException, UnsupportedEncodingException {
132 final ConvertProfile prof = profile;
133 final FfmpegProfile ffop = prof.getFfmpegOption();
135 final List<String> cmdList = new ArrayList<String>();
136 cmdList.add(prof.getFfmpeg().getPath());
138 final String[] mainOptions = ffop.getMainOption().split(" +");
139 for (String opt : mainOptions) {
140 if (isNotBlank(opt)) {
144 final String[] inOptions = ffop.getInOption().split(" +");
145 for (String opt : inOptions) {
146 if (isNotBlank(opt)) {
151 cmdList.add(targetVideoFile.getPath());
152 final String[] outOptions = ffop.getOutOption().split(" +");
153 for (String opt : outOptions) {
154 if (isNotBlank(opt)) {
158 final Info info = MediaInfo.getInfo(new File("bin", "MediaInfo"), targetVideoFile);
159 // 4:3 なら1.33, 16:9 なら1.76
160 final boolean isHD = ((double) info.getWidth() / (double) info.getHeight() > 1.5);
161 if (ffop.isResize()) {
162 final Size scaled = (ffop.isAdjustRatio()) ? MediaInfo.adjustSize(info, ffop.getResizeWidth(), ffop.
163 getResizeHeight()) : new Size(info.getWidth(), info.getHeight());
165 cmdList.add(scaled.getWidth() + "x" + scaled.getHeight());
167 final List<String> avfilterArgs = createAvfilterOptions(ffop.getAvfilterOption());
168 if (!prof.isVhookDisabled()) {
170 final String vhookArg = getVhookArg(prof, prof.isCommentOverlay(), transformedComment.getPath(), isHD);
171 if (isNotBlank(vhookArg)) {
172 avfilterArgs.add(vhookArg);
175 if (!avfilterArgs.isEmpty()) {
176 cmdList.add("-vfilters");
177 final String args = "\"" + join(avfilterArgs, ", ") + "\"";
180 cmdList.add(output.getPath());
181 final StringBuilder argMsg = new StringBuilder();
182 argMsg.append("arg:");
183 for (String s : cmdList) {
184 argMsg.append(" ").append(s);
186 logger.log(Level.INFO, argMsg.toString());
190 private static final Pattern PATTERN_TIME = Pattern.compile("time=(\\d+)");
192 private int executeFfmpeg(final List<String> cmdList, int duration) throws InterruptedException, IOException {
193 Process process = null;
195 logger.log(Level.INFO, "Processing FFmpeg...");
196 process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
197 BufferedReader ebr = new BufferedReader(new InputStreamReader(
198 process.getErrorStream()));
200 while ((msg = ebr.readLine()) != null) {
201 if (msg.startsWith("frame=")) {
202 final Matcher m = PATTERN_TIME.matcher(msg);
205 final String strTime = m.group(1);
206 final double time = Double.parseDouble(strTime);
207 per = 100.0 * time / duration;
208 logger.log(Level.FINEST, "time:{0}, duration:{1}", new Object[]{time, duration});
210 publish(new ConvertProgress(PROCESS, per, msg));
211 } else if (!msg.endsWith("No accelerated colorspace conversion found")) {
212 logger.log(Level.INFO, msg);
219 return process.exitValue();
221 if (process != null) {
227 private static List<String> createAvfilterOptions(String avfilterOption) {
228 final List<String> avfilterArgs = new ArrayList<String>();
229 if (isNotBlank(avfilterOption)) {
230 avfilterArgs.add(avfilterOption);
235 private static String getVhookArg(ConvertProfile prof, boolean addComment, String commPath, boolean isHD) throws
236 UnsupportedEncodingException {
237 StringBuilder sb = new StringBuilder();
239 sb.append(prof.getVhook().getPath().replace("\\", "/"));
242 sb.append("--data-user:");
243 sb.append(URLEncoder.encode(commPath.replace("\\", "/"), "Shift_JIS"));
246 sb.append("--font:");
247 sb.append(URLEncoder.encode(
248 prof.getFont().getPath().replace("\\", "/"), "Shift_JIS"));
250 sb.append("--font-index:");
251 sb.append(prof.getFontIndex());
253 sb.append("--show-user:");
254 final int dispNum = (prof.getMaxNumOfComment() < 0) ? 30 : prof.getMaxNumOfComment();
257 sb.append("--shadow:");
258 sb.append(prof.getShadowIndex());
260 if (prof.isShowConverting()) {
261 sb.append("--enable-show-video");
264 if (!prof.isDisableFontSizeArrange()) {
265 sb.append("--enable-fix-font-size");
268 if (prof.isCommentOpaque()) {
269 sb.append("--enable-opaque-comment");
273 sb.append("--aspect-mode:1");
276 return sb.toString();
279 protected void checkStop() throws InterruptedException {
280 if (Thread.interrupted()) {
281 throw new InterruptedException("中止要求を受け付けました");