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.composer;
23
24 import org.opencastproject.composer.api.ComposerService;
25 import org.opencastproject.composer.api.EncoderException;
26 import org.opencastproject.composer.api.EncodingProfile;
27 import org.opencastproject.job.api.Job;
28 import org.opencastproject.job.api.JobContext;
29 import org.opencastproject.mediapackage.MediaPackage;
30 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
31 import org.opencastproject.mediapackage.MediaPackageElementParser;
32 import org.opencastproject.mediapackage.MediaPackageException;
33 import org.opencastproject.mediapackage.Track;
34 import org.opencastproject.mediapackage.selector.TrackSelector;
35 import org.opencastproject.serviceregistry.api.ServiceRegistry;
36 import org.opencastproject.util.NotFoundException;
37 import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
38 import org.opencastproject.workflow.api.ConfiguredTagsAndFlavors;
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.lang3.StringUtils;
48 import org.osgi.service.component.annotations.Component;
49 import org.osgi.service.component.annotations.Reference;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import java.io.IOException;
54 import java.util.Collection;
55
56
57
58
59
60 @Component(
61 immediate = true,
62 service = WorkflowOperationHandler.class,
63 property = {
64 "service.description=Prepare Media Workflow Operation Handler",
65 "workflow.operation=prepare-av"
66 }
67 )
68 public class PrepareAVWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
69
70
71 private static final Logger logger = LoggerFactory.getLogger(PrepareAVWorkflowOperationHandler.class);
72 private static final String QUESTION_MARK = "?";
73
74
75 public static final String PREPARE_AV_PROFILE = "av.copy";
76
77
78 public static final String MUX_AV_PROFILE = "mux-av.copy";
79
80
81 public static final String PREPARE_AONLY_PROFILE = "audio-only.copy";
82
83
84 public static final String PREPARE_VONLY_PROFILE = "video-only.copy";
85
86
87 public static final String OPT_REWRITE = "rewrite";
88
89
90 public static final String OPT_AUDIO_MUXING_SOURCE_FLAVORS = "audio-muxing-source-flavors";
91
92
93 private ComposerService composerService = null;
94
95
96 private Workspace workspace = null;
97
98
99
100
101
102
103
104 @Reference
105 protected void setComposerService(ComposerService composerService) {
106 this.composerService = composerService;
107 }
108
109
110
111
112
113
114
115
116 @Reference
117 public void setWorkspace(Workspace workspace) {
118 this.workspace = workspace;
119 }
120
121
122
123
124
125
126
127 public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
128 throws WorkflowOperationException {
129 try {
130 logger.debug("Running a/v muxing workflow operation on workflow {}", workflowInstance.getId());
131 return mux(workflowInstance);
132 } catch (Exception e) {
133 throw new WorkflowOperationException(e);
134 }
135 }
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151 private WorkflowOperationResult mux(WorkflowInstance wi) throws EncoderException,
152 WorkflowOperationException, NotFoundException, MediaPackageException, IOException {
153 MediaPackage src = wi.getMediaPackage();
154 MediaPackage mediaPackage = (MediaPackage) src.clone();
155 WorkflowOperationInstance operation = wi.getCurrentOperation();
156
157
158 ConfiguredTagsAndFlavors tagsAndFlavors = getTagsAndFlavors(wi,
159 Configuration.none, Configuration.one, Configuration.many, Configuration.one);
160
161
162 MediaPackageElementFlavor sourceFlavor = tagsAndFlavors.getSingleSrcFlavor();
163 ConfiguredTagsAndFlavors.TargetTags targetTrackTags = tagsAndFlavors.getTargetTags();
164 MediaPackageElementFlavor targetFlavor = tagsAndFlavors.getSingleTargetFlavor();
165 String muxEncodingProfileName = StringUtils.trimToNull(operation.getConfiguration("mux-encoding-profile"));
166 String audioVideoEncodingProfileName = StringUtils.trimToNull(operation.getConfiguration(
167 "audio-video-encoding-profile"));
168 String videoOnlyEncodingProfileName = StringUtils.trimToNull(operation.getConfiguration("video-encoding-profile"));
169 String audioOnlyEncodingProfileName = StringUtils.trimToNull(operation.getConfiguration("audio-encoding-profile"));
170
171
172 boolean rewrite = true;
173 if (StringUtils.trimToNull(operation.getConfiguration(OPT_REWRITE)) != null) {
174 rewrite = Boolean.parseBoolean(operation.getConfiguration(OPT_REWRITE));
175 }
176
177 String audioMuxingSourceFlavors = StringUtils.trimToNull(operation.getConfiguration(
178 OPT_AUDIO_MUXING_SOURCE_FLAVORS));
179
180
181 TrackSelector trackSelector = new TrackSelector();
182 trackSelector.addFlavor(sourceFlavor);
183 Collection<Track> tracks = trackSelector.select(mediaPackage, false);
184
185 Track audioTrack = null;
186 Track videoTrack = null;
187
188 switch (tracks.size()) {
189 case 0:
190 logger.info("No audio/video tracks with flavor '{}' found to prepare", sourceFlavor);
191 return createResult(mediaPackage, Action.CONTINUE);
192 case 1:
193 videoTrack = tracks.iterator().next();
194 if (!videoTrack.hasAudio() && videoTrack.hasVideo() && (audioMuxingSourceFlavors != null)) {
195 audioTrack = findAudioTrack(videoTrack, mediaPackage, audioMuxingSourceFlavors);
196 } else {
197 audioTrack = videoTrack;
198 }
199 break;
200 case 2:
201 for (Track track : tracks) {
202 if (track.hasAudio() && !track.hasVideo()) {
203 audioTrack = track;
204 } else if (!track.hasAudio() && track.hasVideo()) {
205 videoTrack = track;
206 } else {
207 throw new WorkflowOperationException("Multiple tracks with competing audio/video streams and flavor '"
208 + sourceFlavor + "' found");
209 }
210 }
211 break;
212 default:
213 logger.error("More than two tracks with flavor {} found. No idea what we should be doing", sourceFlavor);
214 throw new WorkflowOperationException("More than two tracks with flavor '" + sourceFlavor + "' found");
215 }
216
217 Job job = null;
218 Track composedTrack = null;
219
220
221 if (audioTrack == null && videoTrack != null) {
222 if (rewrite) {
223 logger.info("Encoding video only track {} to prepared version", videoTrack);
224 if (videoOnlyEncodingProfileName == null) {
225 videoOnlyEncodingProfileName = PREPARE_VONLY_PROFILE;
226 }
227
228 EncodingProfile profile = composerService.getProfile(videoOnlyEncodingProfileName);
229 if (profile == null) {
230 throw new IllegalStateException("Encoding profile '" + videoOnlyEncodingProfileName + "' was not found");
231 }
232 composedTrack = prepare(videoTrack, mediaPackage, videoOnlyEncodingProfileName);
233 } else {
234 composedTrack = (Track) videoTrack.clone();
235 composedTrack.setIdentifier(null);
236 mediaPackage.add(composedTrack);
237 }
238 } else if (videoTrack == null && audioTrack != null) {
239 if (rewrite) {
240 logger.info("Encoding audio only track {} to prepared version", audioTrack);
241 if (audioOnlyEncodingProfileName == null) {
242 audioOnlyEncodingProfileName = PREPARE_AONLY_PROFILE;
243 }
244
245 EncodingProfile profile = composerService.getProfile(audioOnlyEncodingProfileName);
246 if (profile == null) {
247 throw new IllegalStateException("Encoding profile '" + audioOnlyEncodingProfileName + "' was not found");
248 }
249 composedTrack = prepare(audioTrack, mediaPackage, audioOnlyEncodingProfileName);
250 } else {
251 composedTrack = (Track) audioTrack.clone();
252 composedTrack.setIdentifier(null);
253 mediaPackage.add(composedTrack);
254 }
255 } else if (audioTrack == videoTrack) {
256 if (rewrite) {
257 logger.info("Encoding audiovisual track {} to prepared version", videoTrack);
258 if (audioVideoEncodingProfileName == null) {
259 audioVideoEncodingProfileName = PREPARE_AV_PROFILE;
260 }
261
262 EncodingProfile profile = composerService.getProfile(audioVideoEncodingProfileName);
263 if (profile == null) {
264 throw new IllegalStateException("Encoding profile '" + audioVideoEncodingProfileName + "' was not found");
265 }
266 composedTrack = prepare(videoTrack, mediaPackage, audioVideoEncodingProfileName);
267 } else {
268 composedTrack = (Track) videoTrack.clone();
269 composedTrack.setIdentifier(null);
270 mediaPackage.add(composedTrack);
271 }
272 } else {
273 logger.info("Muxing audio and video only track {} to prepared version", videoTrack);
274
275 if (audioTrack.hasVideo()) {
276 logger.info("Stripping video from track {}", audioTrack);
277 audioTrack = prepare(audioTrack, null, PREPARE_AONLY_PROFILE);
278 }
279
280 if (muxEncodingProfileName == null) {
281 muxEncodingProfileName = MUX_AV_PROFILE;
282 }
283
284
285 EncodingProfile profile = composerService.getProfile(muxEncodingProfileName);
286 if (profile == null) {
287 throw new IllegalStateException("Encoding profile '" + muxEncodingProfileName + "' was not found");
288 }
289
290 job = composerService.mux(videoTrack, audioTrack, profile.getIdentifier());
291 if (!waitForStatus(job).isSuccess()) {
292 throw new WorkflowOperationException("Muxing video track " + videoTrack + " and audio track " + audioTrack
293 + " failed");
294 }
295 composedTrack = (Track) MediaPackageElementParser.getFromXml(job.getPayload());
296 mediaPackage.add(composedTrack);
297 String fileName = getFileNameFromElements(videoTrack, composedTrack);
298 composedTrack.setURI(workspace.moveTo(composedTrack.getURI(), mediaPackage.getIdentifier().toString(),
299 composedTrack.getIdentifier(), fileName));
300 }
301
302 long timeInQueue = 0;
303 if (job != null) {
304
305 timeInQueue = job.getQueueTime();
306 }
307
308
309 composedTrack.setFlavor(targetFlavor);
310 logger.debug("Composed track has flavor '{}'", composedTrack.getFlavor());
311
312
313 applyTargetTagsToElement(targetTrackTags, composedTrack);
314
315 return createResult(mediaPackage, Action.CONTINUE, timeInQueue);
316 }
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332 private Track prepare(Track videoTrack, MediaPackage mediaPackage, String encodingProfile)
333 throws WorkflowOperationException, NotFoundException, IOException, EncoderException, MediaPackageException {
334 Track composedTrack = null;
335 logger.info("Encoding video only track {} to prepared version", videoTrack);
336 Job job = composerService.encode(videoTrack, encodingProfile);
337 if (!waitForStatus(job).isSuccess()) {
338 throw new WorkflowOperationException("Rewriting container for video track " + videoTrack + " failed");
339 }
340 composedTrack = (Track) MediaPackageElementParser.getFromXml(job.getPayload());
341 if (mediaPackage != null) {
342 mediaPackage.add(composedTrack);
343 String fileName = getFileNameFromElements(videoTrack, composedTrack);
344
345
346
347
348 composedTrack.setURI(workspace.moveTo(composedTrack.getURI(), mediaPackage.getIdentifier().toString(),
349 composedTrack.getIdentifier(), fileName));
350 }
351 return composedTrack;
352 }
353
354
355
356
357
358
359
360
361
362
363
364
365 private Track findAudioTrack(Track videoTrack, MediaPackage mediaPackage, String audioMuxingSourceFlavors) {
366
367 if (audioMuxingSourceFlavors != null) {
368 String type;
369 String subtype;
370 for (String flavorStr : audioMuxingSourceFlavors.split("[\\s,]")) {
371 if (!flavorStr.isEmpty()) {
372 MediaPackageElementFlavor flavor = null;
373 try {
374 flavor = MediaPackageElementFlavor.parseFlavor(flavorStr);
375 } catch (IllegalArgumentException e) {
376 logger.error("The parameter {} contains an invalid flavor: {}", OPT_AUDIO_MUXING_SOURCE_FLAVORS, flavorStr);
377 throw e;
378 }
379 type = (QUESTION_MARK.equals(flavor.getType())) ? videoTrack.getFlavor().getType() : flavor.getType();
380 subtype = (QUESTION_MARK.equals(flavor.getSubtype()))
381 ? videoTrack.getFlavor().getSubtype() : flavor.getSubtype();
382
383 flavor = new MediaPackageElementFlavor(type, subtype);
384 for (Track track : mediaPackage.getTracks(flavor)) {
385 if (track.hasAudio()) {
386 logger.info("Audio muxing found audio source {} with flavor {}", track, track.getFlavor());
387 return track;
388 }
389 }
390 }
391 }
392 }
393 return null;
394 }
395
396 @Reference
397 @Override
398 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
399 super.setServiceRegistry(serviceRegistry);
400 }
401
402 }