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