1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 package org.opencastproject.videoeditor.ffmpeg;
24
25 import org.opencastproject.util.IoSupport;
26 import org.opencastproject.videoeditor.impl.VideoClip;
27 import org.opencastproject.videoeditor.impl.VideoEditorProperties;
28
29 import org.apache.commons.lang3.StringUtils;
30 import org.osgi.framework.BundleContext;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 import java.io.BufferedReader;
35 import java.io.InputStreamReader;
36 import java.text.DecimalFormat;
37 import java.text.DecimalFormatSymbols;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Properties;
43
44
45
46
47
48
49
50 public class FFmpegEdit {
51
52 private static final Logger logger = LoggerFactory.getLogger(FFmpegEdit.class);
53 private static final String FFMPEG_BINARY_DEFAULT = "ffmpeg";
54 private static final String CONFIG_FFMPEG_PATH = "org.opencastproject.composer.ffmpeg.path";
55
56 private static final String DEFAULT_FFMPEG_PROPERTIES = "-preset faster -crf 18";
57 private static final String DEFAULT_AUDIO_FADE = "0.2";
58 private static final String DEFAULT_VIDEO_FADE = "0.2";
59 private static String binary = FFMPEG_BINARY_DEFAULT;
60
61 protected float vfade;
62 protected float afade;
63 protected String ffmpegProperties = DEFAULT_FFMPEG_PROPERTIES;
64 protected String ffmpegScaleFilter = null;
65 protected String videoCodec = null;
66 protected String audioCodec = null;
67
68 public static void init(BundleContext bundleContext) {
69 String path = bundleContext.getProperty(CONFIG_FFMPEG_PATH);
70
71 if (StringUtils.isNotBlank(path)) {
72 binary = path.trim();
73 }
74 }
75
76 public FFmpegEdit() {
77 this.afade = Float.parseFloat(DEFAULT_AUDIO_FADE);
78 this.vfade = Float.parseFloat(DEFAULT_VIDEO_FADE);
79 this.ffmpegProperties = DEFAULT_FFMPEG_PROPERTIES;
80 }
81
82
83
84
85 public FFmpegEdit(Properties properties) {
86 String fade = properties.getProperty(VideoEditorProperties.AUDIO_FADE, DEFAULT_AUDIO_FADE);
87 this.afade = Float.parseFloat(fade);
88 fade = properties.getProperty(VideoEditorProperties.VIDEO_FADE, DEFAULT_VIDEO_FADE);
89 this.vfade = Float.parseFloat(fade);
90 this.ffmpegProperties = properties.getProperty(VideoEditorProperties.FFMPEG_PROPERTIES, DEFAULT_FFMPEG_PROPERTIES);
91 this.ffmpegScaleFilter = properties.getProperty(VideoEditorProperties.FFMPEG_SCALE_FILTER, null);
92 this.videoCodec = properties.getProperty(VideoEditorProperties.VIDEO_CODEC, null);
93 this.audioCodec = properties.getProperty(VideoEditorProperties.AUDIO_CODEC, null);
94 }
95
96 public String processEdits(List<String> inputfiles, String dest, String outputSize, List<VideoClip> cleanclips)
97 throws Exception {
98 return processEdits(inputfiles, dest, outputSize, cleanclips, true, true);
99 }
100
101 public String processEdits(List<String> inputfiles, String dest, String outputSize, List<VideoClip> cleanclips,
102 boolean hasAudio, boolean hasVideo) throws Exception {
103 List<String> cmd = makeEdits(inputfiles, dest, outputSize, cleanclips, hasAudio, hasVideo);
104 return run(cmd);
105 }
106
107
108
109
110 private String run(List<String> params) {
111 BufferedReader in = null;
112 Process encoderProcess = null;
113 try {
114 params.add(0, "-nostats");
115 params.add(0, "-nostdin");
116 params.add(0, "-hide_banner");
117 params.add(0, binary);
118 logger.info("executing command: " + StringUtils.join(params, " "));
119 ProcessBuilder pbuilder = new ProcessBuilder(params);
120 pbuilder.redirectErrorStream(true);
121 encoderProcess = pbuilder.start();
122 in = new BufferedReader(new InputStreamReader(
123 encoderProcess.getInputStream()));
124 String line;
125 int n = 5;
126 while ((line = in.readLine()) != null) {
127 if (n-- > 0) {
128 logger.info(line);
129 }
130 }
131
132
133 encoderProcess.waitFor();
134 int exitCode = encoderProcess.exitValue();
135 if (exitCode != 0) {
136 throw new Exception("Ffmpeg exited abnormally with status " + exitCode);
137 }
138
139 } catch (Exception ex) {
140 logger.error("VideoEditor ffmpeg failed", ex);
141 return ex.toString();
142 } finally {
143 IoSupport.closeQuietly(in);
144 IoSupport.closeQuietly(encoderProcess);
145 }
146 return null;
147 }
148
149
150
151
152
153
154
155
156
157
158
159 public List<String> makeEdits(List<String> inputfiles, String dest, String outputResolution,
160 List<VideoClip> clips, boolean hasAudio, boolean hasVideo) throws Exception {
161
162 if (!hasAudio && !hasVideo) {
163 throw new IllegalArgumentException("Inputfiles should have at least audio or video stream.");
164 }
165
166 DecimalFormat f = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.US));
167 int n = clips.size();
168 int i;
169 String outmap = "";
170 String scale = "";
171 List<String> command = new ArrayList<String>();
172 List<String> vpads = new ArrayList<String>();
173 List<String> apads = new ArrayList<String>();
174 List<String> clauses = new ArrayList<String>();
175
176 if (n > 1) {
177 for (i = 0; i < n ; i++) {
178 if (hasVideo) {
179 vpads.add("[v" + i + "]");
180 }
181 if (hasAudio) {
182 apads.add("[a" + i + "]");
183 }
184 }
185 }
186 if (hasVideo) {
187 if (outputResolution != null && outputResolution.length() > 3) {
188
189 scale = ",scale=" + outputResolution;
190 }
191 else if (ffmpegScaleFilter != null) {
192
193 scale = ",scale=" + ffmpegScaleFilter;
194 }
195 }
196
197 for (i = 0; i < n; i++) {
198
199 VideoClip vclip = clips.get(i);
200 int fileindx = vclip.getSrc();
201 double inpt = vclip.getStartInSeconds();
202 double duration = vclip.getDurationInSeconds();
203
204 String clip = "";
205 if (hasVideo) {
206 String vfadeFilter = "";
207
208 if (vfade > 0.00001) {
209 double vend = duration - vfade;
210 vfadeFilter = ",fade=t=in:st=0:d=" + vfade + ",fade=t=out:st=" + f.format(vend) + ":d=" + vfade;
211 }
212
213 clip = "[" + fileindx + ":v]trim=" + f.format(inpt) + ":duration=" + f.format(duration)
214 + scale + ",setpts=PTS-STARTPTS" + vfadeFilter + "[v" + i + "]";
215
216 clauses.add(clip);
217 }
218
219 if (hasAudio) {
220 String afadeFilter = "";
221
222 if (afade > 0.00001) {
223 double aend = duration - afade;
224 afadeFilter = ",afade=t=in:st=0:d=" + afade + ",afade=t=out:st=" + f.format(aend) + ":d=" + afade;
225 }
226
227 clip = "[" + fileindx + ":a]atrim=" + f.format(inpt) + ":duration=" + f.format(duration)
228 + ",asetpts=PTS-STARTPTS" + afadeFilter + "[a"
229 + i + "]";
230 clauses.add(clip);
231 }
232 }
233 if (n > 1) {
234
235 if (hasVideo) {
236 clauses.add(StringUtils.join(vpads, "") + "concat=n=" + n + ":unsafe=1[ov0]");
237 }
238 if (hasAudio) {
239 clauses.add(StringUtils.join(apads, "") + "concat=n=" + n
240 + ":v=0:a=1[oa0]");
241 }
242 outmap = "o";
243 }
244 for (String o : inputfiles) {
245 command.add("-i");
246 command.add(o);
247 }
248 command.add("-filter_complex");
249 command.add(StringUtils.join(clauses, ";"));
250 String[] options = ffmpegProperties.split(" ");
251 command.addAll(Arrays.asList(options));
252 if (hasAudio) {
253 command.add("-map");
254 command.add("[" + outmap + "a0]");
255 }
256 if (hasVideo) {
257 command.add("-map");
258 command.add("[" + outmap + "v0]");
259 }
260 if (hasVideo && videoCodec != null) {
261 command.add("-c:v");
262 command.add(videoCodec);
263 }
264 if (hasAudio && audioCodec != null) {
265 command.add("-c:a");
266 command.add(audioCodec);
267 }
268 command.add(dest);
269
270 return command;
271 }
272 }