1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.silencedetection.ffmpeg;
23
24 import org.opencastproject.mediapackage.MediaPackageException;
25 import org.opencastproject.mediapackage.Track;
26 import org.opencastproject.silencedetection.api.MediaSegment;
27 import org.opencastproject.silencedetection.api.MediaSegments;
28 import org.opencastproject.silencedetection.api.SilenceDetectionFailedException;
29 import org.opencastproject.silencedetection.impl.SilenceDetectionProperties;
30 import org.opencastproject.util.NotFoundException;
31 import org.opencastproject.workspace.api.Workspace;
32
33 import org.apache.commons.lang3.StringUtils;
34 import org.osgi.framework.BundleContext;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import java.io.BufferedReader;
39 import java.io.File;
40 import java.io.IOException;
41 import java.io.InputStreamReader;
42 import java.text.DecimalFormat;
43 import java.text.DecimalFormatSymbols;
44 import java.util.LinkedList;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.Properties;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50
51
52
53
54 public class FFmpegSilenceDetector {
55
56 private static final Logger logger = LoggerFactory.getLogger(FFmpegSilenceDetector.class);
57
58 public static final String FFMPEG_BINARY_CONFIG = "org.opencastproject.composer.ffmpeg.path";
59 public static final String FFMPEG_BINARY_DEFAULT = "ffmpeg";
60
61 private static final Long DEFAULT_SILENCE_MIN_LENGTH = 5000L;
62 private static final Long DEFAULT_SILENCE_PRE_LENGTH = 2000L;
63 private static final String DEFAULT_THRESHOLD_DB = "-40dB";
64 private static final Long DEFAULT_VOICE_MIN_LENGTH = 60000L;
65
66 private static String binary = FFMPEG_BINARY_DEFAULT;
67 private String filePath;
68 private String trackId;
69
70 private List<MediaSegment> segments = null;
71
72
73
74
75
76
77 public static void init(BundleContext bundleContext) {
78 String binaryPath = bundleContext.getProperty(FFMPEG_BINARY_CONFIG);
79 try {
80 if (StringUtils.isNotBlank(binaryPath)) {
81 File binaryFile = new File(StringUtils.trim(binaryPath));
82 if (binaryFile.exists()) {
83 binary = binaryFile.getAbsolutePath();
84 } else {
85 logger.warn("FFmpeg binary file {} does not exist", StringUtils.trim(binaryPath));
86 }
87 }
88 } catch (Exception ex) {
89 logger.error("Failed to set ffmpeg binary path", ex);
90 }
91 }
92
93
94
95
96
97
98
99
100
101 public FFmpegSilenceDetector(Properties properties, Track track, Workspace workspace)
102 throws SilenceDetectionFailedException, MediaPackageException, IOException {
103
104 long minSilenceLength;
105 long minVoiceLength;
106 long preSilenceLength;
107 String thresholdDB;
108
109
110 if (null == properties) {
111 properties = new Properties();
112 }
113
114 minSilenceLength = parseLong(properties, SilenceDetectionProperties.SILENCE_MIN_LENGTH, DEFAULT_SILENCE_MIN_LENGTH);
115 minVoiceLength = parseLong(properties, SilenceDetectionProperties.VOICE_MIN_LENGTH, DEFAULT_VOICE_MIN_LENGTH);
116 preSilenceLength = parseLong(properties, SilenceDetectionProperties.SILENCE_PRE_LENGTH, DEFAULT_SILENCE_PRE_LENGTH);
117 thresholdDB = properties.getProperty(SilenceDetectionProperties.SILENCE_THRESHOLD_DB, DEFAULT_THRESHOLD_DB);
118
119 trackId = track.getIdentifier();
120
121
122 if (!track.hasAudio()) {
123 logger.warn("Track {} has no audio stream to run a silece detection on", trackId);
124 throw new SilenceDetectionFailedException("Element has no audio stream");
125 }
126
127
128 if (preSilenceLength > minSilenceLength) {
129 logger.error("Pre silence length ({}) is configured to be greater than minimun silence length ({})",
130 preSilenceLength, minSilenceLength);
131 throw new SilenceDetectionFailedException("preSilenceLength > minSilenceLength");
132 }
133
134 try {
135 File mediaFile = workspace.get(track.getURI());
136 filePath = mediaFile.getAbsolutePath();
137 } catch (NotFoundException e) {
138 throw new SilenceDetectionFailedException("Error finding the media file in workspace", e);
139 } catch (IOException e) {
140 throw new SilenceDetectionFailedException("Error reading media file in workspace", e);
141 }
142
143 if (track.getDuration() == null) {
144 throw new MediaPackageException("Track " + trackId + " does not have a duration");
145 }
146 logger.debug("Track {} loaded, duration is {} s", filePath, track.getDuration() / 1000);
147 logger.info("Starting silence detection of {}", filePath);
148 DecimalFormat decimalFmt = new DecimalFormat("0.000", new DecimalFormatSymbols(Locale.US));
149 String minSilenceLengthInSeconds = decimalFmt.format((double) minSilenceLength / 1000.0);
150 String filter = "silencedetect=noise=" + thresholdDB + ":duration=" + minSilenceLengthInSeconds;
151 String[] command = new String[] {
152 binary, "-nostats", "-nostdin", "-i", filePath, "-vn", "-filter:a", filter, "-f", "null", "-"};
153
154 logger.info("Running {}", (Object) command);
155
156 ProcessBuilder pbuilder = new ProcessBuilder(command);
157 List<String> segmentsStrings = new LinkedList<String>();
158 Process process = pbuilder.start();
159 try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
160 String line = reader.readLine();
161 while (null != line) {
162
163 logger.debug("FFmpeg output: {}", line);
164 if (line.startsWith("[silencedetect ")) {
165 segmentsStrings.add(line);
166 }
167 line = reader.readLine();
168 }
169 } catch (IOException e) {
170 logger.error("Error executing ffmpeg", e);
171 }
172
173
174
175
176
177
178
179 LinkedList<MediaSegment> segmentsTmp = new LinkedList<>();
180 if (!segmentsStrings.isEmpty()) {
181 long lastSilenceEnd = 0;
182 long lastSilenceStart = 0;
183 Pattern patternStart = Pattern.compile("silence_start\\:\\ \\d+\\.\\d+");
184 Pattern patternEnd = Pattern.compile("silence_end\\:\\ \\d+\\.\\d+");
185 for (String seginfo : segmentsStrings) {
186
187 Matcher matcher = patternEnd.matcher(seginfo);
188 String time = "";
189 while (matcher.find()) {
190 time = matcher.group().substring(13);
191 }
192 if (!"".equals(time)) {
193 long silenceEnd = (long) (Double.parseDouble(time) * 1000);
194 if (silenceEnd > lastSilenceEnd) {
195 logger.debug("Found silence end at {}", silenceEnd);
196 lastSilenceEnd = silenceEnd;
197 }
198 continue;
199 }
200
201
202 matcher = patternStart.matcher(seginfo);
203 time = "";
204 while (matcher.find()) {
205 time = matcher.group().substring(15);
206 }
207 if (!"".equals(time)) {
208 lastSilenceStart = (long) (Double.parseDouble(time) * 1000);
209 logger.debug("Found silence start at {}", lastSilenceStart);
210 if (lastSilenceStart - lastSilenceEnd > minVoiceLength) {
211
212 long segmentStart = java.lang.Math.max(0, lastSilenceEnd - preSilenceLength);
213 logger.info("Adding segment from {} to {}", segmentStart, lastSilenceStart);
214 segmentsTmp.add(new MediaSegment(segmentStart, lastSilenceStart));
215 }
216 }
217 }
218
219 if (lastSilenceStart < lastSilenceEnd && track.getDuration() - lastSilenceEnd > minVoiceLength) {
220 long segmentStart = java.lang.Math.max(0, lastSilenceEnd - preSilenceLength);
221 logger.info("Adding final segment from {} to {}", segmentStart, track.getDuration());
222 segmentsTmp.add(new MediaSegment(segmentStart, track.getDuration()));
223 }
224 }
225
226 logger.info("Segmentation of track {} yielded {} segments", trackId, segmentsTmp.size());
227 segments = segmentsTmp;
228
229 }
230
231 private Long parseLong(Properties properties, String key, Long defaultValue) {
232 try {
233 return Long.parseLong(properties.getProperty(key, defaultValue.toString()));
234 } catch (NumberFormatException e) {
235 logger.warn("Configuration value for {} is invalid, using default value of {} instead", key, defaultValue);
236 return defaultValue;
237 }
238 }
239
240
241
242
243
244 public MediaSegments getMediaSegments() {
245 if (segments == null) {
246 return null;
247 }
248
249 return new MediaSegments(trackId, filePath, segments);
250 }
251 }