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("audio-video-encoding-profile"));
167 String videoOnlyEncodingProfileName = StringUtils.trimToNull(operation.getConfiguration("video-encoding-profile"));
168 String audioOnlyEncodingProfileName = StringUtils.trimToNull(operation.getConfiguration("audio-encoding-profile"));
169
170
171 boolean rewrite = true;
172 if (StringUtils.trimToNull(operation.getConfiguration(OPT_REWRITE)) != null) {
173 rewrite = Boolean.parseBoolean(operation.getConfiguration(OPT_REWRITE));
174 }
175
176 String audioMuxingSourceFlavors = StringUtils.trimToNull(operation.getConfiguration(OPT_AUDIO_MUXING_SOURCE_FLAVORS));
177
178
179 TrackSelector trackSelector = new TrackSelector();
180 trackSelector.addFlavor(sourceFlavor);
181 Collection<Track> tracks = trackSelector.select(mediaPackage, false);
182
183 Track audioTrack = null;
184 Track videoTrack = null;
185
186 switch (tracks.size()) {
187 case 0:
188 logger.info("No audio/video tracks with flavor '{}' found to prepare", sourceFlavor);
189 return createResult(mediaPackage, Action.CONTINUE);
190 case 1:
191 videoTrack = tracks.iterator().next();
192 if (!videoTrack.hasAudio() && videoTrack.hasVideo() && (audioMuxingSourceFlavors != null)) {
193 audioTrack = findAudioTrack(videoTrack, mediaPackage, audioMuxingSourceFlavors);
194 } else {
195 audioTrack = videoTrack;
196 }
197 break;
198 case 2:
199 for (Track track : tracks) {
200 if (track.hasAudio() && !track.hasVideo()) {
201 audioTrack = track;
202 } else if (!track.hasAudio() && track.hasVideo()) {
203 videoTrack = track;
204 } else {
205 throw new WorkflowOperationException("Multiple tracks with competing audio/video streams and flavor '"
206 + sourceFlavor + "' found");
207 }
208 }
209 break;
210 default:
211 logger.error("More than two tracks with flavor {} found. No idea what we should be doing", sourceFlavor);
212 throw new WorkflowOperationException("More than two tracks with flavor '" + sourceFlavor + "' found");
213 }
214
215 Job job = null;
216 Track composedTrack = null;
217
218
219 if (audioTrack == null && videoTrack != null) {
220 if (rewrite) {
221 logger.info("Encoding video only track {} to prepared version", videoTrack);
222 if (videoOnlyEncodingProfileName == null)
223 videoOnlyEncodingProfileName = PREPARE_VONLY_PROFILE;
224
225 EncodingProfile profile = composerService.getProfile(videoOnlyEncodingProfileName);
226 if (profile == null)
227 throw new IllegalStateException("Encoding profile '" + videoOnlyEncodingProfileName + "' was not found");
228 composedTrack = prepare(videoTrack, mediaPackage, videoOnlyEncodingProfileName);
229 } else {
230 composedTrack = (Track) videoTrack.clone();
231 composedTrack.setIdentifier(null);
232 mediaPackage.add(composedTrack);
233 }
234 } else if (videoTrack == null && audioTrack != null) {
235 if (rewrite) {
236 logger.info("Encoding audio only track {} to prepared version", audioTrack);
237 if (audioOnlyEncodingProfileName == null)
238 audioOnlyEncodingProfileName = PREPARE_AONLY_PROFILE;
239
240 EncodingProfile profile = composerService.getProfile(audioOnlyEncodingProfileName);
241 if (profile == null)
242 throw new IllegalStateException("Encoding profile '" + audioOnlyEncodingProfileName + "' was not found");
243 composedTrack = prepare(audioTrack, mediaPackage, audioOnlyEncodingProfileName);
244 } else {
245 composedTrack = (Track) audioTrack.clone();
246 composedTrack.setIdentifier(null);
247 mediaPackage.add(composedTrack);
248 }
249 } else if (audioTrack == videoTrack) {
250 if (rewrite) {
251 logger.info("Encoding audiovisual track {} to prepared version", videoTrack);
252 if (audioVideoEncodingProfileName == null)
253 audioVideoEncodingProfileName = PREPARE_AV_PROFILE;
254
255 EncodingProfile profile = composerService.getProfile(audioVideoEncodingProfileName);
256 if (profile == null)
257 throw new IllegalStateException("Encoding profile '" + audioVideoEncodingProfileName + "' was not found");
258 composedTrack = prepare(videoTrack, mediaPackage, audioVideoEncodingProfileName);
259 } else {
260 composedTrack = (Track) videoTrack.clone();
261 composedTrack.setIdentifier(null);
262 mediaPackage.add(composedTrack);
263 }
264 } else {
265 logger.info("Muxing audio and video only track {} to prepared version", videoTrack);
266
267 if (audioTrack.hasVideo()) {
268 logger.info("Stripping video from track {}", audioTrack);
269 audioTrack = prepare(audioTrack, null, PREPARE_AONLY_PROFILE);
270 }
271
272 if (muxEncodingProfileName == null)
273 muxEncodingProfileName = MUX_AV_PROFILE;
274
275
276 EncodingProfile profile = composerService.getProfile(muxEncodingProfileName);
277 if (profile == null)
278 throw new IllegalStateException("Encoding profile '" + muxEncodingProfileName + "' was not found");
279
280 job = composerService.mux(videoTrack, audioTrack, profile.getIdentifier());
281 if (!waitForStatus(job).isSuccess()) {
282 throw new WorkflowOperationException("Muxing video track " + videoTrack + " and audio track " + audioTrack
283 + " failed");
284 }
285 composedTrack = (Track) MediaPackageElementParser.getFromXml(job.getPayload());
286 mediaPackage.add(composedTrack);
287 String fileName = getFileNameFromElements(videoTrack, composedTrack);
288 composedTrack.setURI(workspace.moveTo(composedTrack.getURI(), mediaPackage.getIdentifier().toString(),
289 composedTrack.getIdentifier(), fileName));
290 }
291
292 long timeInQueue = 0;
293 if (job != null) {
294
295 timeInQueue = job.getQueueTime();
296 }
297
298
299 composedTrack.setFlavor(targetFlavor);
300 logger.debug("Composed track has flavor '{}'", composedTrack.getFlavor());
301
302
303 applyTargetTagsToElement(targetTrackTags, composedTrack);
304
305 return createResult(mediaPackage, Action.CONTINUE, timeInQueue);
306 }
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322 private Track prepare(Track videoTrack, MediaPackage mediaPackage, String encodingProfile)
323 throws WorkflowOperationException, NotFoundException, IOException, EncoderException, MediaPackageException {
324 Track composedTrack = null;
325 logger.info("Encoding video only track {} to prepared version", videoTrack);
326 Job job = composerService.encode(videoTrack, encodingProfile);
327 if (!waitForStatus(job).isSuccess()) {
328 throw new WorkflowOperationException("Rewriting container for video track " + videoTrack + " failed");
329 }
330 composedTrack = (Track) MediaPackageElementParser.getFromXml(job.getPayload());
331 if (mediaPackage != null) {
332 mediaPackage.add(composedTrack);
333 String fileName = getFileNameFromElements(videoTrack, composedTrack);
334
335
336
337
338 composedTrack.setURI(workspace.moveTo(composedTrack.getURI(), mediaPackage.getIdentifier().toString(),
339 composedTrack.getIdentifier(), fileName));
340 }
341 return composedTrack;
342 }
343
344
345
346
347
348
349
350
351
352
353
354
355 private Track findAudioTrack(Track videoTrack, MediaPackage mediaPackage, String audioMuxingSourceFlavors) {
356
357 if (audioMuxingSourceFlavors != null) {
358 String type;
359 String subtype;
360 for (String flavorStr : audioMuxingSourceFlavors.split("[\\s,]")) {
361 if (!flavorStr.isEmpty()) {
362 MediaPackageElementFlavor flavor = null;
363 try {
364 flavor = MediaPackageElementFlavor.parseFlavor(flavorStr);
365 } catch (IllegalArgumentException e) {
366 logger.error("The parameter {} contains an invalid flavor: {}", OPT_AUDIO_MUXING_SOURCE_FLAVORS, flavorStr);
367 throw e;
368 }
369 type = (QUESTION_MARK.equals(flavor.getType())) ? videoTrack.getFlavor().getType() : flavor.getType();
370 subtype = (QUESTION_MARK.equals(flavor.getSubtype())) ? videoTrack.getFlavor().getSubtype() : flavor.getSubtype();
371
372 flavor = new MediaPackageElementFlavor(type, subtype);
373 for (Track track : mediaPackage.getTracks(flavor)) {
374 if (track.hasAudio()) {
375 logger.info("Audio muxing found audio source {} with flavor {}", track, track.getFlavor());
376 return track;
377 }
378 }
379 }
380 }
381 }
382 return null;
383 }
384
385 @Reference
386 @Override
387 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
388 super.setServiceRegistry(serviceRegistry);
389 }
390
391 }