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.workflow.handler.subtitletimeshift;
23
24 import static java.lang.String.format;
25
26 import org.opencastproject.job.api.JobContext;
27 import org.opencastproject.mediapackage.MediaPackage;
28 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
29 import org.opencastproject.mediapackage.Track;
30 import org.opencastproject.mediapackage.selector.TrackSelector;
31 import org.opencastproject.subtitleparser.webvttparser.WebVTTParser;
32 import org.opencastproject.subtitleparser.webvttparser.WebVTTSubtitle;
33 import org.opencastproject.subtitleparser.webvttparser.WebVTTSubtitleCue;
34 import org.opencastproject.subtitleparser.webvttparser.WebVTTWriter;
35 import org.opencastproject.util.Checksum;
36 import org.opencastproject.util.ChecksumType;
37 import org.opencastproject.util.NotFoundException;
38 import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
39 import org.opencastproject.workflow.api.WorkflowInstance;
40 import org.opencastproject.workflow.api.WorkflowOperationException;
41 import org.opencastproject.workflow.api.WorkflowOperationHandler;
42 import org.opencastproject.workflow.api.WorkflowOperationInstance;
43 import org.opencastproject.workflow.api.WorkflowOperationResult;
44 import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
45 import org.opencastproject.workspace.api.Workspace;
46
47 import org.apache.commons.io.FilenameUtils;
48 import org.apache.commons.lang3.StringUtils;
49 import org.osgi.service.component.annotations.Component;
50 import org.osgi.service.component.annotations.Reference;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import java.io.ByteArrayInputStream;
55 import java.io.ByteArrayOutputStream;
56 import java.io.File;
57 import java.io.FileInputStream;
58 import java.io.IOException;
59 import java.net.URI;
60 import java.util.Collection;
61 import java.util.Objects;
62
63
64
65
66
67
68
69 @Component(
70 property = {
71 "service.description=subtitle-timeshift Workflow Operation Handler",
72 "workflow.operation=subtitle-timeshift"
73 },
74 immediate = true,
75 service = WorkflowOperationHandler.class
76 )
77 public class SubtitleTimeshiftWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
78
79 private static final String SUBTITLE_SOURCE_FLAVOR_CFG_KEY = "subtitle-source-flavor";
80 private static final String VIDEO_SOURCE_FLAVOR_CFG_KEY = "video-source-flavors";
81 private static final String TARGET_FLAVOR_CFG_KEY = "target-flavor";
82
83
84 private static final String COLLECTION = "subtitles";
85
86
87 private static final Logger logger = LoggerFactory.getLogger(SubtitleTimeshiftWorkflowOperationHandler.class);
88
89
90
91
92 private Workspace workspace = null;
93
94
95
96
97
98
99
100 @Reference
101 public void setWorkspace(Workspace workspace) {
102 this.workspace = workspace;
103 }
104
105
106 @Override
107 public WorkflowOperationResult start(WorkflowInstance workflowInstance, JobContext context)
108 throws WorkflowOperationException {
109
110 MediaPackage mediaPackage = workflowInstance.getMediaPackage();
111 logger.info("Starting subtitle timeshift workflow for mediapackage: {}", mediaPackage.getIdentifier().toString());
112
113
114 final WorkflowOperationInstance operation = workflowInstance.getCurrentOperation();
115 MediaPackageElementFlavor configuredSubtitleFlavor;
116 String configuredVideoFlavors;
117 MediaPackageElementFlavor configuredTargetFlavor;
118 try {
119 configuredSubtitleFlavor = MediaPackageElementFlavor.parseFlavor(
120 Objects.toString(operation.getConfiguration(SUBTITLE_SOURCE_FLAVOR_CFG_KEY)));
121 configuredTargetFlavor = MediaPackageElementFlavor.parseFlavor(
122 operation.getConfiguration(TARGET_FLAVOR_CFG_KEY));
123 configuredVideoFlavors = StringUtils
124 .trimToNull(operation.getConfiguration(VIDEO_SOURCE_FLAVOR_CFG_KEY));
125 if (configuredVideoFlavors == null) {
126 throw new WorkflowOperationException(format("Configuration property %s not set", VIDEO_SOURCE_FLAVOR_CFG_KEY));
127 }
128 } catch (Exception e) {
129 throw new WorkflowOperationException("Couldn't parse subtitle-timeshift workflow configurations.", e);
130 }
131
132
133 Track[] originalSubtitleTracks;
134 Track videoTrack;
135 long totalVideoDuration = 0;
136 try {
137
138 Track[] subtitleTracks = mediaPackage.getTracks(configuredSubtitleFlavor);
139
140 TrackSelector trackSelector = new TrackSelector();
141 for (String flavor : asList(configuredVideoFlavors)) {
142 trackSelector.addFlavor(flavor);
143 }
144 Collection<Track> videoTracks = trackSelector.select(mediaPackage, false);
145 if (videoTracks.isEmpty()) {
146 throw new WorkflowOperationException(format("No video tracks found in mediapackage %s with flavor %s",
147 mediaPackage.getIdentifier().toString(), configuredVideoFlavors));
148 }
149
150
151
152 if (subtitleTracks.length == 0) {
153
154 logger.info("No subtitle track found with flavor {}. Skipping subtitle-timeshift workflow operation "
155 + "for mediapackage {}", configuredSubtitleFlavor, mediaPackage.getIdentifier());
156 return createResult(mediaPackage, Action.SKIP);
157 }
158
159
160 originalSubtitleTracks = subtitleTracks;
161
162
163 for (Track track : videoTracks) {
164 Long duration = track.getDuration();
165 if (duration != null) {
166 totalVideoDuration += duration;
167 } else {
168 logger.debug("Videotrack {} did not have a duration.", track);
169 }
170 }
171
172 logger.info("Valid tracks found. Start shifting subtitle tracks by duration '{}'", totalVideoDuration);
173
174 } catch (Exception e) {
175 logger.error("Error in subtitle-timeshift workflow while getting tracks for mediapackage {}",
176 mediaPackage.getIdentifier(), e);
177 throw new WorkflowOperationException(e);
178 }
179
180
181 try {
182 for (Track originalSubtitleTrack : originalSubtitleTracks) {
183
184
185 WebVTTSubtitle newSubtitleFile = loadAndParseSubtitleFile(originalSubtitleTrack);
186
187
188 shiftTime(newSubtitleFile, totalVideoDuration);
189
190
191 String originalFileName = FilenameUtils.getBaseName(originalSubtitleTrack.getLogicalName());
192 String newFileName = "timeshifted-" + originalFileName + ".vtt";
193 URI newSubtitleFileUri = saveSubtitleFileToWorkspace(newSubtitleFile, newFileName);
194
195
196 Track newSubtitleTrack = createNewTrackFromSubtitleUri(newSubtitleFileUri, configuredTargetFlavor,
197 originalSubtitleTrack);
198
199
200 mediaPackage.add(newSubtitleTrack);
201 logger.info("Added subtitle track with URI {} to mediapackage {}", newSubtitleFileUri,
202 mediaPackage.getIdentifier());
203 }
204
205 } catch (Exception e) {
206 logger.error("Error while shifting time of subtitle tracks for mediapackage {}", mediaPackage.getIdentifier(), e);
207 throw new WorkflowOperationException(e);
208 }
209
210 logger.info("Subtitle-Timeshift workflow operation for media package {} completed", mediaPackage);
211 return createResult(mediaPackage, Action.CONTINUE);
212 }
213
214
215
216
217
218
219
220
221
222 private Track createNewTrackFromSubtitleUri(URI subtitleFile, MediaPackageElementFlavor targetFlavor,
223 Track originalSubtitleTrack) throws IOException, NotFoundException {
224
225 Track newSubtitleTrack = (Track) originalSubtitleTrack.clone();
226 newSubtitleTrack.generateIdentifier();
227 newSubtitleTrack.setFlavor(targetFlavor);
228 newSubtitleTrack.setURI(subtitleFile);
229 newSubtitleTrack.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, workspace.get(subtitleFile, true)));
230 return newSubtitleTrack;
231 }
232
233
234
235
236
237
238
239
240
241 private URI saveSubtitleFileToWorkspace(WebVTTSubtitle webVTTSubtitle, String fileName)
242 throws WorkflowOperationException {
243 try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
244 WebVTTWriter writer = new WebVTTWriter();
245 writer.write(webVTTSubtitle, outputStream);
246 try (ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray())) {
247 return workspace.putInCollection(COLLECTION, fileName, inputStream);
248 }
249 } catch (IOException e) {
250 logger.error("An exception occurred while parsing and saving a subtitle file to the workspace", e);
251 throw new WorkflowOperationException(e);
252 }
253 }
254
255
256
257
258
259
260
261
262 private WebVTTSubtitle loadAndParseSubtitleFile(Track subtitleTrack) throws WorkflowOperationException {
263
264 File subtitleFile;
265 try {
266 subtitleFile = workspace.get(subtitleTrack.getURI());
267 } catch (IOException ex) {
268 throw new WorkflowOperationException("Can't read " + subtitleTrack.getURI());
269 } catch (NotFoundException ex) {
270 throw new WorkflowOperationException("Workspace does not contain a track " + subtitleTrack.getURI());
271 }
272
273
274 WebVTTSubtitle subtitle;
275 try (FileInputStream fin = new FileInputStream(subtitleFile)) {
276 subtitle = new WebVTTParser().parse(fin);
277 } catch (Exception e) {
278 throw new WorkflowOperationException("Couldn't parse subtitle file " + subtitleTrack.getURI(), e);
279 }
280
281 return subtitle;
282 }
283
284
285
286
287
288
289 public void shiftTime(WebVTTSubtitle subtitleFile, long time) {
290 for (WebVTTSubtitleCue cue : subtitleFile.getCues()) {
291 long start = cue.getStartTime();
292 long end = cue.getEndTime();
293 cue.setStartTime(start + time);
294 cue.setEndTime(end + time);
295 }
296 }
297
298 }