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.EnumMap;
16 import java.util.EnumSet;
17 import java.util.List;
19 import java.util.Map.Entry;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24 import saccubus.conv.ConvertToVideoHook;
25 import saccubus.conv.CommentType;
26 import saccubus.util.FfmpegUtil;
27 import saccubus.worker.Worker;
28 import saccubus.worker.WorkerListener;
29 import saccubus.worker.profile.ConvertProfile;
30 import saccubus.worker.profile.ConvertProfile.HideCondition;
31 import saccubus.worker.profile.FfmpegProfile;
32 import saccubus.worker.profile.GeneralProfile;
33 import saccubus.worker.profile.OutputProfile;
34 import yukihane.inqubus.util.OutputNamePattern;
35 import yukihane.mediainfowrapper.Info;
36 import yukihane.mediainfowrapper.MediaInfo;
37 import yukihane.mediainfowrapper.Size;
38 import yukihane.swf.Cws2Fws;
41 * 動画を(コメント付きに)変換するワーカクラス.
44 public class Convert extends Worker<ConvertResult, ConvertProgress> {
46 private static final Logger logger = LoggerFactory.getLogger(Convert.class);
47 private final ConvertProfile profile;
48 private final File videoFile;
49 private final File commentFile;
51 public Convert(ConvertProfile profile, File video, File comment) {
52 this(profile, video, comment, null);
57 * @param profile 変換用プロファイル.
59 * @param comment 変換元コメント. コメントを付与しない場合はnull.
60 * @param output 変換後出力動画.
61 * @throws IOException 変換失敗.
63 public Convert(ConvertProfile profile, File video, File comment,
64 WorkerListener<ConvertResult, ConvertProgress> listener) {
66 this.profile = profile;
67 this.videoFile = video;
68 this.commentFile = comment;
69 logger.info("convert video:{}, comment:{}", videoFile, commentFile);
73 protected ConvertResult work() throws Exception {
74 if (!profile.isConvert()) {
75 return new ConvertResult(true, "");
78 final GeneralProfile gene = profile.getGeneralProfile();
79 final OutputProfile outprof = profile.getOutputProfile();
80 final OutputNamePattern outputPattern = new OutputNamePattern(outprof.getFileName());
81 final String id = outprof.getVideoId();
82 outputPattern.setId(isNotEmpty(id) ? id : "");
83 final String title = outprof.getTitile();
84 outputPattern.setTitle(isNotEmpty(title) ? title : "");
85 final String fileName = getBaseName(videoFile.getPath());
86 outputPattern.setFileName(fileName);
87 outputPattern.setReplaceFrom(gene.getReplaceFrom());
88 outputPattern.setReplaceFrom(gene.getReplaceTo());
89 final File outputFile = new File(outprof.getDir(),
90 outputPattern.createFileName() + profile.getFfmpegOption().getExtOption());
92 final Map<CommentType, File> tmpComments = new EnumMap<>(CommentType.class);
95 if (profile.isCommentOverlay()) {
96 for (CommentType ct : CommentType.values()) {
97 tmpComments.put(ct, File.createTempFile("vhk", ".tmp", profile.getTempDir()));
100 final HideCondition hide = profile.getNgSetting();
102 for (CommentType ct : CommentType.values()) {
103 publish(new ConvertProgress(PROCESS, -1.0, ct.toString() + "の中間ファイルへの変換中"));
104 ConvertToVideoHook.convert(EnumSet.of(ct), commentFile, tmpComments.get(ct),
105 hide.getId(), hide.getWord());
110 publish(new ConvertProgress(PROCESS, -1.0, "動画の変換を開始"));
112 final int code = convert(outputFile, new EnumMap<>(tmpComments));
114 throw new IOException("ffmpeg実行失敗(code " + code + "): " + outputFile.getPath());
116 publish(new ConvertProgress(PROCESS, 100.0, "変換が正常に終了しました。"));
117 return new ConvertResult(true, outputFile.getName());
119 for(File f : tmpComments.values()) {
120 if(f != null && f.exists()) {
127 private int convert(File outputFile, Map<CommentType,File> tmpComments) throws InterruptedException, IOException {
130 final File tmpCws = File.createTempFile("cws", ".swf", profile.getTempDir());
131 fwsFile = Cws2Fws.createFws(videoFile, tmpCws);
133 final File target = (fwsFile != null) ? fwsFile : videoFile;
135 final List<String> arguments = createArguments(target, outputFile, tmpComments);
136 final FfmpegUtil util = new FfmpegUtil(profile.getFfmpeg(), target);
139 duration = util.getDuration();
140 } catch (IOException ex) {
141 logger.info("動画再生時間を取得できませんでした: {}", target);
142 duration = Integer.MAX_VALUE;
144 return executeFfmpeg(arguments, duration);
146 if (fwsFile != null && fwsFile.exists()) {
152 private List<String> createArguments(final File targetVideoFile, File output, Map<CommentType,File> comments)
153 throws IOException, UnsupportedEncodingException {
154 final ConvertProfile prof = profile;
155 final FfmpegProfile ffop = prof.getFfmpegOption();
157 final List<String> cmdList = new ArrayList<>();
158 cmdList.add(prof.getFfmpeg().getPath());
160 final String[] mainOptions = ffop.getMainOption().split(" +");
161 for (String opt : mainOptions) {
162 if (isNotBlank(opt)) {
166 final String[] inOptions = ffop.getInOption().split(" +");
167 for (String opt : inOptions) {
168 if (isNotBlank(opt)) {
173 cmdList.add(targetVideoFile.getPath());
174 final String[] outOptions = ffop.getOutOption().split(" +");
175 for (String opt : outOptions) {
176 if (isNotBlank(opt)) {
180 final Info info = MediaInfo.getInfo(profile.getMediaInfo(), targetVideoFile);
181 // 4:3 なら1.33, 16:9 なら1.76
182 final boolean isHD = ((double) info.getWidth() / (double) info.getHeight() > 1.5);
183 if (ffop.isResize()) {
184 final Size scaled = (ffop.isAdjustRatio()) ? MediaInfo.adjustSize(info, ffop.getResizeWidth(), ffop.
185 getResizeHeight()) : new Size(info.getWidth(), info.getHeight());
187 cmdList.add(scaled.getWidth() + "x" + scaled.getHeight());
189 final List<String> avfilterArgs = createAvfilterOptions(ffop.getAvfilterOption());
190 if (!prof.isVhookDisabled()) {
191 final String vhookArg = getVhookArg(prof, comments, isHD);
192 if (isNotBlank(vhookArg)) {
193 avfilterArgs.add(vhookArg);
196 if (!avfilterArgs.isEmpty()) {
197 cmdList.add("-vfilters");
198 final String args = join(avfilterArgs, ", ");
201 cmdList.add(output.getPath());
203 logger.info("arg: {}", cmdList);
206 private static final Pattern PATTERN_TIME = Pattern.compile("time=(\\d+):(\\d+):(\\d+)");
208 private int executeFfmpeg(final List<String> cmdList, int duration) throws InterruptedException, IOException {
209 Process process = null;
211 logger.info("Processing FFmpeg...");
212 process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
213 BufferedReader ebr = new BufferedReader(new InputStreamReader(
214 process.getErrorStream()));
216 while ((msg = ebr.readLine()) != null) {
217 if (msg.startsWith("frame=")) {
218 final Matcher m = PATTERN_TIME.matcher(msg);
221 final double hour = Integer.parseInt(m.group(1));
222 final double min = Integer.parseInt(m.group(2));
223 final double sec = Integer.parseInt(m.group(3));
224 final double time = ((hour * 60) + min) * 60 + sec;
225 per = 100.0 * time / duration;
226 if (logger.isTraceEnabled()) {
227 logger.trace("time:{}, duration:{}", time, duration);
231 publish(new ConvertProgress(PROCESS, per, msg));
232 } else if (!msg.endsWith("No accelerated colorspace conversion found")) {
242 return process.exitValue();
244 if (process != null) {
250 private static List<String> createAvfilterOptions(String avfilterOption) {
251 final List<String> avfilterArgs = new ArrayList<>();
252 if (isNotBlank(avfilterOption)) {
253 avfilterArgs.add(avfilterOption);
258 private static String getVhookArg(ConvertProfile prof, Map<CommentType, File> comments, boolean isHD) throws
259 UnsupportedEncodingException {
260 StringBuilder sb = new StringBuilder();
262 sb.append(prof.getVhook().getPath().replace("\\", "/"));
263 if (prof.isCommentOverlay()) {
264 for(Entry<CommentType, File> e : comments.entrySet()) {
266 sb.append(e.getKey().getVhookOptionPrefix());
267 sb.append(URLEncoder.encode(e.getValue().getPath().replace("\\", "/"), "Shift_JIS"));
271 sb.append("--font:");
272 sb.append(URLEncoder.encode(
273 prof.getFont().getPath().replace("\\", "/"), "Shift_JIS"));
275 sb.append("--font-index:");
276 sb.append(prof.getFontIndex());
278 sb.append("--show-user:");
279 final int dispNum = (prof.getMaxNumOfComment() < 0) ? 30 : prof.getMaxNumOfComment();
282 sb.append("--shadow:");
283 sb.append(prof.getShadowIndex());
285 if (prof.isShowConverting()) {
286 sb.append("--enable-show-video");
289 if (!prof.isDisableFontSizeArrange()) {
290 sb.append("--enable-fix-font-size");
293 if (prof.isCommentOpaque()) {
294 sb.append("--enable-opaque-comment");
298 sb.append("--aspect-mode:1");
301 return sb.toString();
304 protected void checkStop() throws InterruptedException {
305 if (Thread.interrupted()) {
306 throw new InterruptedException("中止要求を受け付けました");