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.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;
38 * 動画を(コメント付きに)変換するワーカクラス.
41 public class Convert extends Worker<ConvertResult, ConvertProgress> {
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;
48 public Convert(ConvertProfile profile, File video, File comment) {
49 this(profile, video, comment, null);
54 * @param profile 変換用プロファイル.
56 * @param comment 変換元コメント. コメントを付与しない場合はnull.
57 * @param output 変換後出力動画.
58 * @throws IOException 変換失敗.
60 public Convert(ConvertProfile profile, File video, File comment,
61 WorkerListener<ConvertResult, ConvertProgress> listener) {
63 this.profile = profile;
64 this.videoFile = video;
65 this.commentFile = comment;
66 logger.info("convert video:{}, comment:{}", videoFile, commentFile);
70 protected ConvertResult work() throws Exception {
71 if (!profile.isConvert()) {
72 return new ConvertResult(true, "");
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());
89 File transformedComment = null;
90 File transformedOwner = null;
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();
98 publish(new ConvertProgress(PROCESS, -1.0, "コメントの中間ファイルへの変換中"));
99 ConvertToVideoHook.convert(EnumSet.of(ProcessType.NORMAL), commentFile, transformedComment, hide.getId(),
102 publish(new ConvertProgress(PROCESS, -1.0, "投稿者コメントの中間ファイルへの変換中"));
103 ConvertToVideoHook.convert(EnumSet.of(ProcessType.OWNER), commentFile, transformedOwner, hide.getId(),
108 publish(new ConvertProgress(PROCESS, -1.0, "動画の変換を開始"));
110 final int code = convert(outputFile, transformedComment, transformedOwner);
112 throw new IOException("ffmpeg実行失敗: " + outputFile.getPath());
114 publish(new ConvertProgress(PROCESS, 100.0, "変換が正常に終了しました。"));
115 return new ConvertResult(true, outputFile.getName());
117 if (transformedComment != null && transformedComment.exists()) {
118 transformedComment.delete();
120 if (transformedOwner != null && transformedOwner.exists()) {
121 transformedOwner.delete();
126 private int convert(File outputFile, File commentNormal, File commentOwner) throws InterruptedException, IOException {
129 final File tmpCws = File.createTempFile("cws", ".swf", profile.getTempDir());
130 fwsFile = Cws2Fws.createFws(videoFile, tmpCws);
132 final File target = (fwsFile != null) ? fwsFile : videoFile;
134 final List<String> arguments = createArguments(target, outputFile, commentNormal, commentOwner);
135 final FfmpegUtil util = new FfmpegUtil(profile.getFfmpeg(), target);
138 duration = util.getDuration();
139 } catch (IOException ex) {
140 logger.info("動画再生時間を取得できませんでした: {}", target);
141 duration = Integer.MAX_VALUE;
143 return executeFfmpeg(arguments, duration);
145 if (fwsFile != null && fwsFile.exists()) {
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();
156 final List<String> cmdList = new ArrayList<>();
157 cmdList.add(prof.getFfmpeg().getPath());
159 final String[] mainOptions = ffop.getMainOption().split(" +");
160 for (String opt : mainOptions) {
161 if (isNotBlank(opt)) {
165 final String[] inOptions = ffop.getInOption().split(" +");
166 for (String opt : inOptions) {
167 if (isNotBlank(opt)) {
172 cmdList.add(targetVideoFile.getPath());
173 final String[] outOptions = ffop.getOutOption().split(" +");
174 for (String opt : outOptions) {
175 if (isNotBlank(opt)) {
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());
186 cmdList.add(scaled.getWidth() + "x" + scaled.getHeight());
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);
195 if (!avfilterArgs.isEmpty()) {
196 cmdList.add("-vfilters");
197 final String args = join(avfilterArgs, ", ");
200 cmdList.add(output.getPath());
201 final StringBuilder argMsg = new StringBuilder();
202 argMsg.append("arg:");
203 for (String s : cmdList) {
204 argMsg.append(" ").append(s);
206 logger.info(argMsg.toString());
209 private static final Pattern PATTERN_TIME = Pattern.compile("time=(\\d+)");
211 private int executeFfmpeg(final List<String> cmdList, int duration) throws InterruptedException, IOException {
212 Process process = null;
214 logger.info("Processing FFmpeg...");
215 process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
216 BufferedReader ebr = new BufferedReader(new InputStreamReader(
217 process.getErrorStream()));
219 while ((msg = ebr.readLine()) != null) {
220 if (msg.startsWith("frame=")) {
221 final Matcher m = PATTERN_TIME.matcher(msg);
224 final String strTime = m.group(1);
225 final double time = Double.parseDouble(strTime);
226 per = 100.0 * time / duration;
227 logger.trace("time:{}, duration:{}", time, duration);
229 publish(new ConvertProgress(PROCESS, per, msg));
230 } else if (!msg.endsWith("No accelerated colorspace conversion found")) {
238 return process.exitValue();
240 if (process != null) {
246 private static List<String> createAvfilterOptions(String avfilterOption) {
247 final List<String> avfilterArgs = new ArrayList<>();
248 if (isNotBlank(avfilterOption)) {
249 avfilterArgs.add(avfilterOption);
254 private static String getVhookArg(ConvertProfile prof, String commPath, String commOwnerPath, boolean isHD) throws
255 UnsupportedEncodingException {
256 StringBuilder sb = new StringBuilder();
258 sb.append(prof.getVhook().getPath().replace("\\", "/"));
259 if (prof.isCommentOverlay()) {
261 sb.append("--data-user:");
262 sb.append(URLEncoder.encode(commPath.replace("\\", "/"), "Shift_JIS"));
264 sb.append("--data-owner:");
265 sb.append(URLEncoder.encode(commOwnerPath.replace("\\", "/"), "Shift_JIS"));
268 sb.append("--font:");
269 sb.append(URLEncoder.encode(
270 prof.getFont().getPath().replace("\\", "/"), "Shift_JIS"));
272 sb.append("--font-index:");
273 sb.append(prof.getFontIndex());
275 sb.append("--show-user:");
276 final int dispNum = (prof.getMaxNumOfComment() < 0) ? 30 : prof.getMaxNumOfComment();
279 sb.append("--shadow:");
280 sb.append(prof.getShadowIndex());
282 if (prof.isShowConverting()) {
283 sb.append("--enable-show-video");
286 if (!prof.isDisableFontSizeArrange()) {
287 sb.append("--enable-fix-font-size");
290 if (prof.isCommentOpaque()) {
291 sb.append("--enable-opaque-comment");
295 sb.append("--aspect-mode:1");
298 return sb.toString();
301 protected void checkStop() throws InterruptedException {
302 if (Thread.interrupted()) {
303 throw new InterruptedException("中止要求を受け付けました");