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.composer.impl;
23
24 import static java.lang.String.format;
25 import static org.opencastproject.composer.impl.EncoderEngine.CMD_SUFFIX;
26 import static org.opencastproject.serviceregistry.api.Incidents.NO_DETAILS;
27 import static org.opencastproject.util.data.Option.none;
28 import static org.opencastproject.util.data.Option.some;
29 import static org.opencastproject.util.data.Tuple.tuple;
30
31 import org.opencastproject.composer.api.ComposerService;
32 import org.opencastproject.composer.api.EncoderException;
33 import org.opencastproject.composer.api.EncodingProfile;
34 import org.opencastproject.composer.api.LaidOutElement;
35 import org.opencastproject.composer.api.VideoClip;
36 import org.opencastproject.composer.layout.Dimension;
37 import org.opencastproject.composer.layout.Layout;
38 import org.opencastproject.composer.layout.Serializer;
39 import org.opencastproject.inspection.api.MediaInspectionException;
40 import org.opencastproject.inspection.api.MediaInspectionService;
41 import org.opencastproject.job.api.AbstractJobProducer;
42 import org.opencastproject.job.api.Job;
43 import org.opencastproject.job.api.JobBarrier;
44 import org.opencastproject.mediapackage.AdaptivePlaylist;
45 import org.opencastproject.mediapackage.Attachment;
46 import org.opencastproject.mediapackage.MediaPackageElement;
47 import org.opencastproject.mediapackage.MediaPackageElementBuilder;
48 import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
49 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
50 import org.opencastproject.mediapackage.MediaPackageElementParser;
51 import org.opencastproject.mediapackage.MediaPackageException;
52 import org.opencastproject.mediapackage.Track;
53 import org.opencastproject.mediapackage.VideoStream;
54 import org.opencastproject.mediapackage.identifier.IdImpl;
55 import org.opencastproject.security.api.OrganizationDirectoryService;
56 import org.opencastproject.security.api.SecurityService;
57 import org.opencastproject.security.api.UserDirectoryService;
58 import org.opencastproject.serviceregistry.api.ServiceRegistry;
59 import org.opencastproject.serviceregistry.api.ServiceRegistryException;
60 import org.opencastproject.smil.api.SmilException;
61 import org.opencastproject.smil.api.SmilService;
62 import org.opencastproject.smil.entity.api.Smil;
63 import org.opencastproject.smil.entity.media.api.SmilMediaObject;
64 import org.opencastproject.smil.entity.media.container.api.SmilMediaContainer;
65 import org.opencastproject.smil.entity.media.element.api.SmilMediaElement;
66 import org.opencastproject.smil.entity.media.param.api.SmilMediaParam;
67 import org.opencastproject.smil.entity.media.param.api.SmilMediaParamGroup;
68 import org.opencastproject.util.FileSupport;
69 import org.opencastproject.util.JsonObj;
70 import org.opencastproject.util.LoadUtil;
71 import org.opencastproject.util.MimeTypes;
72 import org.opencastproject.util.NotFoundException;
73 import org.opencastproject.util.ReadinessIndicator;
74 import org.opencastproject.util.UnknownFileTypeException;
75 import org.opencastproject.util.data.Collections;
76 import org.opencastproject.util.data.Option;
77 import org.opencastproject.util.data.Tuple;
78 import org.opencastproject.workspace.api.Workspace;
79
80 import com.google.gson.Gson;
81
82 import org.apache.commons.io.FileUtils;
83 import org.apache.commons.io.FilenameUtils;
84 import org.apache.commons.io.IOUtils;
85 import org.apache.commons.lang3.StringUtils;
86 import org.apache.commons.lang3.math.NumberUtils;
87 import org.osgi.service.cm.ConfigurationException;
88 import org.osgi.service.cm.ManagedService;
89 import org.osgi.service.component.ComponentContext;
90 import org.osgi.service.component.annotations.Activate;
91 import org.osgi.service.component.annotations.Component;
92 import org.osgi.service.component.annotations.Deactivate;
93 import org.osgi.service.component.annotations.Modified;
94 import org.osgi.service.component.annotations.Reference;
95 import org.slf4j.Logger;
96 import org.slf4j.LoggerFactory;
97
98 import java.io.File;
99 import java.io.FileInputStream;
100 import java.io.FileWriter;
101 import java.io.IOException;
102 import java.io.InputStream;
103 import java.io.PrintWriter;
104 import java.net.URI;
105 import java.net.URISyntaxException;
106 import java.text.DecimalFormat;
107 import java.text.DecimalFormatSymbols;
108 import java.util.ArrayList;
109 import java.util.Arrays;
110 import java.util.Collection;
111 import java.util.Dictionary;
112 import java.util.HashMap;
113 import java.util.HashSet;
114 import java.util.LinkedList;
115 import java.util.List;
116 import java.util.Locale;
117 import java.util.Map;
118 import java.util.Map.Entry;
119 import java.util.Optional;
120 import java.util.Properties;
121 import java.util.Set;
122 import java.util.stream.Collectors;
123
124
125 @Component(
126 property = {
127 "service.description=Composer (Encoder) Local Service",
128 "service.pid=org.opencastproject.composer.impl.ComposerServiceImpl"
129 },
130 immediate = true,
131 service = { ComposerService.class, ManagedService.class }
132 )
133 public class ComposerServiceImpl extends AbstractJobProducer implements ComposerService, ManagedService {
134
135
136
137 private static final int BACKGROUND_COLOR_INDEX = 6;
138 private static final int COMPOSITE_TRACK_SIZE_INDEX = 5;
139 private static final int LOWER_TRACK_INDEX = 1;
140 private static final int LOWER_TRACK_LAYOUT_INDEX = 2;
141 private static final int PROFILE_ID_INDEX = 0;
142 private static final int UPPER_TRACK_INDEX = 3;
143 private static final int UPPER_TRACK_LAYOUT_INDEX = 4;
144 private static final int WATERMARK_INDEX = 8;
145 private static final int WATERMARK_LAYOUT_INDEX = 9;
146 private static final int AUDIO_SOURCE_INDEX = 7;
147
148
149
150 private static final int WORKSPACE_GET_IO_EXCEPTION = 1;
151 private static final int WORKSPACE_GET_NOT_FOUND = 2;
152 private static final int WORKSPACE_PUT_COLLECTION_IO_EXCEPTION = 3;
153 private static final int PROFILE_NOT_FOUND = 4;
154 private static final int ENCODING_FAILED = 7;
155 private static final int TRIMMING_FAILED = 8;
156 private static final int COMPOSITE_FAILED = 9;
157 private static final int CONCAT_FAILED = 10;
158 private static final int CONCAT_LESS_TRACKS = 11;
159 private static final int CONCAT_NO_DIMENSION = 12;
160 private static final int IMAGE_TO_VIDEO_FAILED = 13;
161 private static final int CONVERT_IMAGE_FAILED = 14;
162 private static final int IMAGE_EXTRACTION_FAILED = 15;
163 private static final int IMAGE_EXTRACTION_UNKNOWN_DURATION = 16;
164 private static final int IMAGE_EXTRACTION_TIME_OUTSIDE_DURATION = 17;
165 private static final int IMAGE_EXTRACTION_NO_VIDEO = 18;
166 private static final int PROCESS_SMIL_FAILED = 19;
167 private static final int MULTI_ENCODE_FAILED = 20;
168 private static final int NO_STREAMS = 23;
169
170
171 private static final Logger logger = LoggerFactory.getLogger(ComposerServiceImpl.class);
172
173
174 private static final String FFMPEG_BINARY_DEFAULT = "ffmpeg";
175
176
177 private static final String CONFIG_FFMPEG_PATH = "org.opencastproject.composer.ffmpeg.path";
178
179
180 private static final String COLLECTION = "composer";
181
182
183 private static final String NOT_AVAILABLE = "n/a";
184
185
186 private static final DecimalFormat df = new DecimalFormat("#.#");
187
188
189 public static final String PROCESS_SMIL_CLIP_TRANSITION_DURATION = "org.composer.process_smil.edit.transition.duration";
190
191
192 public static final float DEFAULT_PROCESS_SMIL_CLIP_TRANSITION_DURATION = 2.0f;
193
194
195 public static final float DEFAULT_JOB_LOAD_MAX_MULTIPLE_PROFILES = 0.8f;
196
197 public static final float DEFAULT_PROCESS_SMIL_JOB_LOAD_FACTOR = 0.5f;
198 public static final float DEFAULT_MULTI_ENCODE_JOB_LOAD_FACTOR = 0.5f;
199
200 public static final String JOB_LOAD_MAX_MULTIPLE_PROFILES = "job.load.max.multiple.profiles";
201 public static final String JOB_LOAD_FACTOR_PROCESS_SMIL = "job.load.factor.process.smil";
202 public static final String JOB_LOAD_FACTOR_MULTI_ENCODE = "job.load.factor.multiencode";
203
204 private float maxMultipleProfilesJobLoad = DEFAULT_JOB_LOAD_MAX_MULTIPLE_PROFILES;
205 private float processSmilJobLoadFactor = DEFAULT_PROCESS_SMIL_JOB_LOAD_FACTOR;
206 private float multiEncodeJobLoadFactor = DEFAULT_MULTI_ENCODE_JOB_LOAD_FACTOR;
207
208
209
210 public static final int DEFAULT_MULTI_ENCODE_TRIM_MILLISECONDS = 0;
211 public static final String MULTI_ENCODE_TRIM_MILLISECONDS = "org.composer.multi_encode.trim.milliseconds";
212 private int multiEncodeTrim = DEFAULT_MULTI_ENCODE_TRIM_MILLISECONDS;
213
214
215 public static final int DEFAULT_MULTI_ENCODE_FADE_MILLISECONDS = 0;
216 public static final String MULTI_ENCODE_FADE_MILLISECONDS = "org.composer.multi_encode.fade.milliseconds";
217 private int multiEncodeFade = DEFAULT_MULTI_ENCODE_FADE_MILLISECONDS;
218
219
220 private int transitionDuration = (int) (DEFAULT_PROCESS_SMIL_CLIP_TRANSITION_DURATION * 1000);
221
222
223 enum Operation {
224 Encode, Image, ImageConversion, Mux, Trim, Composite, Concat, ImageToVideo, ParallelEncode, Demux, ProcessSmil, MultiEncode
225 }
226
227
228 private Set<EncoderEngine> activeEncoder = new HashSet<>();
229
230
231 private EncodingProfileScanner profileScanner = null;
232
233
234 private MediaInspectionService inspectionService = null;
235
236
237 private Workspace workspace = null;
238
239
240 private ServiceRegistry serviceRegistry;
241
242
243 private OrganizationDirectoryService organizationDirectoryService = null;
244
245
246 private SecurityService securityService = null;
247
248
249 private SmilService smilService;
250
251
252 private UserDirectoryService userDirectoryService = null;
253
254
255 private String ffmpegBinary = FFMPEG_BINARY_DEFAULT;
256
257
258 public ComposerServiceImpl() {
259 super(JOB_TYPE);
260 }
261
262
263
264
265
266
267
268 @Override
269 @Activate
270 public void activate(ComponentContext cc) {
271 super.activate(cc);
272 ffmpegBinary = StringUtils.defaultString(cc.getBundleContext().getProperty(CONFIG_FFMPEG_PATH),
273 FFMPEG_BINARY_DEFAULT);
274 logger.debug("ffmpeg binary: {}", ffmpegBinary);
275 logger.info("Activating composer service");
276 }
277
278
279
280
281 @Deactivate
282 public void deactivate() {
283 logger.info("Deactivating composer service");
284 for (EncoderEngine engine: activeEncoder) {
285 engine.close();
286 }
287 logger.debug("Closed encoder engine factory");
288 }
289
290
291
292
293
294
295
296 @Override
297 public Job encode(Track sourceTrack, String profileId) throws EncoderException, MediaPackageException {
298 try {
299 final EncodingProfile profile = profileScanner.getProfile(profileId);
300 return serviceRegistry.createJob(JOB_TYPE, Operation.Encode.toString(),
301 Arrays.asList(profileId, MediaPackageElementParser.getAsXml(sourceTrack)), profile.getJobLoad());
302 } catch (ServiceRegistryException e) {
303 throw new EncoderException("Unable to create a job", e);
304 }
305 }
306
307
308
309
310
311
312
313
314
315
316 private File loadTrackIntoWorkspace(final Job job, final String name, final Track track, boolean unique)
317 throws EncoderException {
318 try {
319 return workspace.get(track.getURI(), unique);
320 } catch (NotFoundException e) {
321 incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e,
322 getWorkspaceMediapackageParams(name, track), NO_DETAILS);
323 throw new EncoderException(format("%s track %s not found", name, track));
324 } catch (IOException e) {
325 incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e,
326 getWorkspaceMediapackageParams(name, track), NO_DETAILS);
327 throw new EncoderException(format("Unable to access %s track %s", name, track));
328 }
329 }
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344 private File loadURIIntoWorkspace(final Job job, final String name, final URI uri) throws EncoderException {
345 try {
346 return workspace.get(uri);
347 } catch (NotFoundException e) {
348 incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceCollectionParams(name, name, uri),
349 NO_DETAILS);
350 throw new EncoderException(String.format("%s uri %s not found", name, uri));
351 } catch (IOException e) {
352 incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceCollectionParams(name, name, uri),
353 NO_DETAILS);
354 throw new EncoderException(String.format("Unable to access %s uri %s", name, uri));
355 }
356 }
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373 private Option<Track> encode(final Job job, Map<String, Track> tracks, String profileId)
374 throws EncoderException, MediaPackageException {
375
376 final String targetTrackId = IdImpl.fromUUID().toString();
377
378 Map<String, File> files = new HashMap<>();
379
380 for (Entry<String, Track> track: tracks.entrySet()) {
381 files.put(track.getKey(), loadTrackIntoWorkspace(job, track.getKey(), track.getValue(), false));
382 }
383
384
385 final EncodingProfile profile = getProfile(job, profileId);
386
387 List <String> trackMsg = new LinkedList<>();
388 for (Entry<String, Track> track: tracks.entrySet()) {
389 trackMsg.add(format("%s: %s", track.getKey(), track.getValue().getIdentifier()));
390 }
391 logger.info("Encoding {} into {} using profile {}", StringUtils.join(trackMsg, ", "), targetTrackId, profileId);
392
393
394 final EncoderEngine encoder = getEncoderEngine();
395 List<File> output;
396 try {
397 output = encoder.process(files, profile, null);
398 } catch (EncoderException e) {
399 Map<String, String> params = new HashMap<>();
400 for (Entry<String, Track> track: tracks.entrySet()) {
401 params.put(track.getKey(), track.getValue().getIdentifier());
402 }
403 params.put("profile", profile.getIdentifier());
404 params.put("properties", "EMPTY");
405 incident().recordFailure(job, ENCODING_FAILED, e, params, detailsFor(e, encoder));
406 throw e;
407 } finally {
408 activeEncoder.remove(encoder);
409 }
410
411
412 if (output.size() == 0) {
413 return none();
414 } else if (output.size() != 1) {
415
416 for (File file : output) {
417 FileUtils.deleteQuietly(file);
418 }
419 throw new EncoderException("Composite does not support multiple files as output");
420 }
421
422
423 URI workspaceURI = putToCollection(job, output.get(0), "encoded file");
424
425
426 Track inspectedTrack = inspect(job, workspaceURI);
427 inspectedTrack.setIdentifier(targetTrackId);
428
429 return some(inspectedTrack);
430 }
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447 private List <Track> parallelEncode(Job job, Track mediaTrack, String profileId)
448 throws EncoderException, MediaPackageException {
449 if (job == null) {
450 throw new EncoderException("The Job parameter must not be null");
451 }
452
453 final File mediaFile = loadTrackIntoWorkspace(job, "source", mediaTrack, false);
454
455
456 final EncodingProfile profile = getProfile(profileId);
457 final EncoderEngine encoderEngine = getEncoderEngine();
458
459
460 final Optional<VideoStream> videoStream = Arrays.stream(mediaTrack.getStreams())
461 .filter((stream -> stream instanceof VideoStream))
462 .map(stream -> (VideoStream) stream)
463 .findFirst();
464 final int height = videoStream.map(vs -> vs.getFrameHeight()).orElse(0);
465 final int width = videoStream.map(vs -> vs.getFrameWidth()).orElse(0);
466 Map<String, String> properties = new HashMap<>();
467 for (String key: profile.getExtensions().keySet()) {
468 if (key.startsWith(CMD_SUFFIX + ".if-height-geq-")) {
469 final int heightCondition = Integer.parseInt(key.substring((CMD_SUFFIX + ".if-height-geq-").length()));
470 if (heightCondition <= height) {
471 properties.put(key, profile.getExtension(key));
472 }
473 } else if (key.startsWith(CMD_SUFFIX + ".if-height-lt-")) {
474 final int heightCondition = Integer.parseInt(key.substring((CMD_SUFFIX + ".if-height-lt-").length()));
475 if (heightCondition > height) {
476 properties.put(key, profile.getExtension(key));
477 }
478 } else if (key.startsWith(CMD_SUFFIX + ".if-width-or-height-geq-")) {
479 final String[] resCondition = key.substring((CMD_SUFFIX + ".if-width-or-height-geq-").length()).split("-");
480 final int widthCondition = Integer.parseInt(resCondition[0]);
481 final int heightCondition = Integer.parseInt(resCondition[1]);
482
483 if (heightCondition <= height || widthCondition <= width) {
484 properties.put(key, profile.getExtension(key));
485 }
486 }
487 }
488
489
490 LinkedList<Track> encodedTracks = new LinkedList<>();
491
492 Map<String, File> source = new HashMap<>();
493 source.put("video", mediaFile);
494 List<File> outputFiles = encoderEngine.process(source, profile, properties);
495 var returnURLs = new ArrayList<URI>();
496 var tagsForUrls = new ArrayList<List<String>>();
497 activeEncoder.remove(encoderEngine);
498 int i = 0;
499 var fileMapping = new HashMap<String, String>();
500 for (File file: outputFiles) {
501 fileMapping.put(file.getName(), job.getId() + "_" + i + "." + FilenameUtils.getExtension(file.getName()));
502 i++;
503 }
504 boolean isHLS = false;
505 for (File file: outputFiles) {
506
507 if (AdaptivePlaylist.isPlaylist(file)) {
508 isHLS = true;
509 logger.debug("Rewriting HLS references in {}", file);
510 try {
511 AdaptivePlaylist.hlsRewriteFileReference(file, fileMapping);
512 } catch (IOException e) {
513 throw new EncoderException("Unable to rewrite HLS references", e);
514 }
515 }
516
517
518 final List<String> tags = profile.getTags();
519 try (InputStream in = new FileInputStream(file)) {
520 var encodedFileName = file.getName();
521 var workspaceFilename = fileMapping.get(encodedFileName);
522 var url = workspace.putInCollection(COLLECTION, workspaceFilename, in);
523 returnURLs.add(url);
524
525 var tagsForUrl = new ArrayList<String>();
526 for (final String tag : tags) {
527 if (encodedFileName.endsWith(profile.getSuffix(tag))) {
528 tagsForUrl.add(tag);
529 }
530 }
531 tagsForUrls.add(tagsForUrl);
532
533 logger.info("Copied the encoded file to the workspace at {}", url);
534 } catch (Exception e) {
535 throw new EncoderException("Unable to put the encoded file into the workspace", e);
536 }
537 }
538
539
540 for (Track inspectedTrack: inspect(job, returnURLs, tagsForUrls)) {
541 final String targetTrackId = IdImpl.fromUUID().toString();
542 inspectedTrack.setIdentifier(targetTrackId);
543 if (isHLS) {
544 AdaptivePlaylist.setLogicalName(inspectedTrack);
545 }
546
547 encodedTracks.add(inspectedTrack);
548 }
549
550
551 for (File encodingOutput: outputFiles) {
552 if (encodingOutput.delete()) {
553 logger.info("Deleted the local copy of the encoded file at {}", encodingOutput.getAbsolutePath());
554 } else {
555 logger.warn("Unable to delete the encoding output at {}", encodingOutput);
556 }
557 }
558
559 return encodedTracks;
560 }
561
562
563
564
565
566
567
568 @Override
569 public Job parallelEncode(Track sourceTrack, String profileId) throws EncoderException, MediaPackageException {
570 try {
571 final EncodingProfile profile = profileScanner.getProfile(profileId);
572 logger.info("Starting parallel encode with profile {} with job load {}", profileId, df.format(profile.getJobLoad()));
573 return serviceRegistry.createJob(JOB_TYPE, Operation.ParallelEncode.toString(),
574 Arrays.asList(profileId, MediaPackageElementParser.getAsXml(sourceTrack)), profile.getJobLoad());
575 } catch (ServiceRegistryException e) {
576 throw new EncoderException("Unable to create a job", e);
577 }
578 }
579
580
581
582
583
584
585
586 @Override
587 public Job trim(final Track sourceTrack, final String profileId, final long start, final long duration)
588 throws EncoderException, MediaPackageException {
589 try {
590 final EncodingProfile profile = profileScanner.getProfile(profileId);
591 return serviceRegistry.createJob(JOB_TYPE, Operation.Trim.toString(),
592 Arrays.asList(profileId, MediaPackageElementParser.getAsXml(sourceTrack), Long.toString(start),
593 Long.toString(duration)), profile.getJobLoad());
594 } catch (ServiceRegistryException e) {
595 throw new EncoderException("Unable to create a job", e);
596 }
597 }
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618 private Option<Track> trim(Job job, Track sourceTrack, String profileId, long start, long duration)
619 throws EncoderException {
620 String targetTrackId = IdImpl.fromUUID().toString();
621
622
623 final File trackFile = loadTrackIntoWorkspace(job, "source", sourceTrack, false);
624
625
626 final EncodingProfile profile = getProfile(job, profileId);
627
628
629 final EncoderEngine encoderEngine = getEncoderEngine();
630
631 File output;
632 try {
633 output = encoderEngine.trim(trackFile, profile, start, duration, null);
634 } catch (EncoderException e) {
635 Map<String, String> params = new HashMap<>();
636 params.put("track", sourceTrack.getURI().toString());
637 params.put("profile", profile.getIdentifier());
638 params.put("start", Long.toString(start));
639 params.put("duration", Long.toString(duration));
640 incident().recordFailure(job, TRIMMING_FAILED, e, params, detailsFor(e, encoderEngine));
641 throw e;
642 } finally {
643 activeEncoder.remove(encoderEngine);
644 }
645
646
647 if (!output.exists() || output.length() == 0)
648 return none();
649
650
651 URI workspaceURI = putToCollection(job, output, "trimmed file");
652
653
654 Track inspectedTrack = inspect(job, workspaceURI);
655 inspectedTrack.setIdentifier(targetTrackId);
656 return some(inspectedTrack);
657 }
658
659
660
661
662
663
664
665 @Override
666 public Job mux(Track videoTrack, Track audioTrack, String profileId) throws EncoderException, MediaPackageException {
667 try {
668 final EncodingProfile profile = profileScanner.getProfile(profileId);
669 return serviceRegistry.createJob(JOB_TYPE, Operation.Mux.toString(),
670 Arrays.asList(profileId, MediaPackageElementParser.getAsXml(videoTrack),
671 MediaPackageElementParser.getAsXml(audioTrack)), profile.getJobLoad());
672 } catch (ServiceRegistryException e) {
673 throw new EncoderException("Unable to create a job", e);
674 }
675 }
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694 private Option<Track> mux(Job job, Track videoTrack, Track audioTrack, String profileId) throws EncoderException,
695 MediaPackageException {
696 return encode(job, Collections.map(tuple("audio", audioTrack), tuple("video", videoTrack)), profileId);
697 }
698
699
700
701
702 @Override
703 public Job composite(Dimension compositeTrackSize, Option<LaidOutElement<Track>> upperTrack,
704 LaidOutElement<Track> lowerTrack, Option<LaidOutElement<Attachment>> watermark, String profileId,
705 String background, String sourceAudioName) throws EncoderException, MediaPackageException {
706 List<String> arguments = new ArrayList<>(10);
707 arguments.add(PROFILE_ID_INDEX, profileId);
708 arguments.add(LOWER_TRACK_INDEX, MediaPackageElementParser.getAsXml(lowerTrack.getElement()));
709 arguments.add(LOWER_TRACK_LAYOUT_INDEX, Serializer.json(lowerTrack.getLayout()).toJson());
710 if (upperTrack.isNone()) {
711 arguments.add(UPPER_TRACK_INDEX, NOT_AVAILABLE);
712 arguments.add(UPPER_TRACK_LAYOUT_INDEX, NOT_AVAILABLE);
713 } else {
714 arguments.add(UPPER_TRACK_INDEX, MediaPackageElementParser.getAsXml(upperTrack.get().getElement()));
715 arguments.add(UPPER_TRACK_LAYOUT_INDEX, Serializer.json(upperTrack.get().getLayout()).toJson());
716 }
717 arguments.add(COMPOSITE_TRACK_SIZE_INDEX, Serializer.json(compositeTrackSize).toJson());
718 arguments.add(BACKGROUND_COLOR_INDEX, background);
719 arguments.add(AUDIO_SOURCE_INDEX, sourceAudioName);
720 if (watermark.isSome()) {
721 LaidOutElement<Attachment> watermarkLaidOutElement = watermark.get();
722 arguments.add(WATERMARK_INDEX, MediaPackageElementParser.getAsXml(watermarkLaidOutElement.getElement()));
723 arguments.add(WATERMARK_LAYOUT_INDEX, Serializer.json(watermarkLaidOutElement.getLayout()).toJson());
724 }
725 try {
726 final EncodingProfile profile = profileScanner.getProfile(profileId);
727 return serviceRegistry.createJob(JOB_TYPE, Operation.Composite.toString(), arguments, profile.getJobLoad());
728 } catch (ServiceRegistryException e) {
729 throw new EncoderException("Unable to create composite job", e);
730 }
731 }
732
733 private Option<Track> composite(Job job, Dimension compositeTrackSize, LaidOutElement<Track> lowerLaidOutElement,
734 Option<LaidOutElement<Track>> upperLaidOutElement, Option<LaidOutElement<Attachment>> watermarkOption,
735 String profileId, String backgroundColor, String audioSourceName) throws EncoderException, MediaPackageException {
736
737
738 final EncodingProfile profile = getProfile(job, profileId);
739
740
741 final EncoderEngine encoderEngine = getEncoderEngine();
742
743 final String targetTrackId = IdImpl.fromUUID().toString();
744 Option<File> upperVideoFile = Option.none();
745 try {
746
747 final File lowerVideoFile = loadTrackIntoWorkspace(job, "lower video", lowerLaidOutElement.getElement(), false);
748
749 if (upperLaidOutElement.isSome()) {
750 upperVideoFile = Option.option(
751 loadTrackIntoWorkspace(job, "upper video", upperLaidOutElement.get().getElement(), false));
752 }
753 File watermarkFile = null;
754 if (watermarkOption.isSome()) {
755 try {
756 watermarkFile = workspace.get(watermarkOption.get().getElement().getURI());
757 } catch (NotFoundException e) {
758 incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e,
759 getWorkspaceMediapackageParams("watermark image", watermarkOption.get().getElement()),
760 NO_DETAILS);
761 throw new EncoderException("Requested watermark image " + watermarkOption.get().getElement()
762 + " is not found");
763 } catch (IOException e) {
764 incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e,
765 getWorkspaceMediapackageParams("watermark image", watermarkOption.get().getElement()),
766 NO_DETAILS);
767 throw new EncoderException("Unable to access right watermark image " + watermarkOption.get().getElement());
768 }
769 if (upperLaidOutElement.isSome()) {
770 logger.info("Composing lower video track {} {} and upper video track {} {} including watermark {} {} into {}",
771 lowerLaidOutElement.getElement().getIdentifier(), lowerLaidOutElement.getElement().getURI(),
772 upperLaidOutElement.get().getElement().getIdentifier(), upperLaidOutElement.get().getElement().getURI(),
773 watermarkOption.get().getElement().getIdentifier(), watermarkOption.get().getElement().getURI(),
774 targetTrackId);
775 } else {
776 logger.info("Composing video track {} {} including watermark {} {} into {}",
777 lowerLaidOutElement.getElement().getIdentifier(), lowerLaidOutElement.getElement().getURI(),
778 watermarkOption.get().getElement().getIdentifier(), watermarkOption.get().getElement().getURI(),
779 targetTrackId);
780 }
781 } else {
782 if (upperLaidOutElement.isSome()) {
783 logger.info("Composing lower video track {} {} and upper video track {} {} into {}",
784 lowerLaidOutElement.getElement().getIdentifier(), lowerLaidOutElement.getElement().getURI(),
785 upperLaidOutElement.get().getElement().getIdentifier(), upperLaidOutElement.get().getElement().getURI(),
786 targetTrackId);
787 } else {
788 logger.info("Composing video track {} {} into {}", lowerLaidOutElement.getElement().getIdentifier(),
789 lowerLaidOutElement.getElement().getURI(), targetTrackId);
790 }
791 }
792
793
794 Map<String, String> properties = new HashMap<>();
795
796
797 final Layout lowerLayout = lowerLaidOutElement.getLayout();
798 final String lowerPosition = lowerLayout.getOffset().getX() + ":" + lowerLayout.getOffset().getY();
799 final String scaleLower = lowerLayout.getDimension().getWidth() + ":"
800 + lowerLayout.getDimension().getHeight();
801 final String padLower = compositeTrackSize.getWidth() + ":" + compositeTrackSize.getHeight() + ":"
802 + lowerPosition + ":" + backgroundColor;
803 properties.put("scaleLower", scaleLower);
804 properties.put("padLower", padLower);
805
806
807 if (upperVideoFile.isSome() && upperLaidOutElement.isSome()) {
808 final Layout upperLayout = upperLaidOutElement.get().getLayout();
809 final String upperPosition = upperLayout.getOffset().getX() + ":" + upperLayout.getOffset().getY();
810 final String scaleUpper = upperLayout.getDimension().getWidth() + ":"
811 + upperLayout.getDimension().getHeight();
812
813 properties.put("scaleUpper", scaleUpper);
814 properties.put("upperPosition", upperPosition);
815 properties.put("upperFile", upperVideoFile.get().getAbsolutePath());
816
817
818 boolean lowerAudio = lowerLaidOutElement.getElement().hasAudio();
819 boolean upperAudio = upperLaidOutElement.get().getElement().hasAudio();
820
821 if (lowerAudio && upperAudio && (ComposerService.BOTH.equalsIgnoreCase(audioSourceName)
822 || StringUtils.isBlank(audioSourceName))) {
823 properties.put(CMD_SUFFIX + ".audioMapping", ";[0:a][1:a]amix=inputs=2[aout] -map [out] -map [aout]");
824 } else if (lowerAudio && !ComposerService.UPPER.equalsIgnoreCase(audioSourceName)) {
825 properties.put(CMD_SUFFIX + ".audioMapping", " -map [out] -map 0:a");
826 } else if (upperAudio && !ComposerService.LOWER.equalsIgnoreCase(audioSourceName)) {
827 properties.put(CMD_SUFFIX + ".audioMapping", " -map [out] -map 1:a");
828 } else {
829 properties.put(CMD_SUFFIX + ".audioMapping", " -map [out]");
830 }
831 }
832
833
834 if (watermarkOption.isSome()) {
835 final LaidOutElement<Attachment> watermarkLayout = watermarkOption.get();
836 String watermarkPosition =
837 watermarkLayout.getLayout().getOffset().getX() + ":" + watermarkLayout.getLayout().getOffset().getY();
838 properties.put("watermarkPosition", watermarkPosition);
839 properties.put("watermarkFile", watermarkFile.getAbsoluteFile().toString());
840 }
841
842
843 for (String key: profile.getExtensions().keySet()) {
844 if (key.startsWith(CMD_SUFFIX + ".if-single-stream")
845 && (upperLaidOutElement.isNone() || upperVideoFile.isNone())) {
846 properties.put(key, profile.getExtension(key));
847 } else if (key.startsWith(CMD_SUFFIX + ".if-dual-stream")
848 && upperVideoFile.isSome() && upperLaidOutElement.isSome()) {
849 properties.put(key, profile.getExtension(key));
850 } else if (key.startsWith(CMD_SUFFIX + ".if-watermark") && watermarkOption.isSome()) {
851 properties.put(key, profile.getExtension(key));
852 } else if (key.startsWith(CMD_SUFFIX + ".if-no-watermark") && watermarkOption.isNone()) {
853 properties.put(key, profile.getExtension(key));
854 }
855 }
856
857 List<File> output;
858 try {
859 Map<String, File> source = new HashMap<>();
860 if (upperVideoFile.isSome()) {
861 source.put("audio", upperVideoFile.get());
862 }
863 source.put("video", lowerVideoFile);
864 output = encoderEngine.process(source, profile, properties);
865 } catch (EncoderException e) {
866 Map<String, String> params = new HashMap<>();
867 if (upperLaidOutElement.isSome()) {
868 params.put("upper", upperLaidOutElement.get().getElement().getURI().toString());
869 }
870 params.put("lower", lowerLaidOutElement.getElement().getURI().toString());
871 if (watermarkFile != null)
872 params.put("watermark", watermarkOption.get().getElement().getURI().toString());
873 params.put("profile", profile.getIdentifier());
874 params.put("properties", properties.toString());
875 incident().recordFailure(job, COMPOSITE_FAILED, e, params, detailsFor(e, encoderEngine));
876 throw e;
877 } finally {
878 activeEncoder.remove(encoderEngine);
879 }
880
881
882 if (output.size() != 1) {
883
884 for (File file : output) {
885 FileUtils.deleteQuietly(file);
886 }
887 throw new EncoderException("Composite does not support multiple files as output");
888 }
889
890
891
892 URI workspaceURI = putToCollection(job, output.get(0), "compound file");
893
894
895
896 Track inspectedTrack = inspect(job, workspaceURI);
897 inspectedTrack.setIdentifier(targetTrackId);
898
899 return some(inspectedTrack);
900 } catch (Exception e) {
901 if (upperLaidOutElement.isSome()) {
902 logger.warn("Error composing {} and {}:",
903 lowerLaidOutElement.getElement(), upperLaidOutElement.get().getElement(), e);
904 } else {
905 logger.warn("Error composing {}:", lowerLaidOutElement.getElement(), e);
906 }
907 if (e instanceof EncoderException) {
908 throw (EncoderException) e;
909 } else {
910 throw new EncoderException(e);
911 }
912 }
913 }
914
915 @Override
916 public Job concat(String profileId, Dimension outputDimension, boolean sameCodec, Track... tracks) throws EncoderException,
917 MediaPackageException {
918 return concat(profileId, outputDimension, -1.0f, sameCodec, tracks);
919 }
920
921 @Override
922 public Job concat(String profileId, Dimension outputDimension, float outputFrameRate, boolean sameCodec, Track... tracks) throws EncoderException,
923 MediaPackageException {
924 ArrayList<String> arguments = new ArrayList<String>();
925 arguments.add(0, profileId);
926 if (outputDimension != null) {
927 arguments.add(1, Serializer.json(outputDimension).toJson());
928 } else {
929 arguments.add(1, "");
930 }
931 arguments.add(2, String.format(Locale.US, "%f", outputFrameRate));
932 arguments.add(3, Boolean.toString(sameCodec));
933 for (int i = 0; i < tracks.length; i++) {
934 arguments.add(i + 4, MediaPackageElementParser.getAsXml(tracks[i]));
935 }
936 try {
937 final EncodingProfile profile = profileScanner.getProfile(profileId);
938 return serviceRegistry.createJob(JOB_TYPE, Operation.Concat.toString(), arguments, profile.getJobLoad());
939 } catch (ServiceRegistryException e) {
940 throw new EncoderException("Unable to create concat job", e);
941 }
942 }
943
944 private Option<Track> concat(Job job, List<Track> tracks, String profileId, Dimension outputDimension,
945 float outputFrameRate, boolean sameCodec)
946 throws EncoderException, MediaPackageException {
947
948 if (tracks.size() < 2) {
949 Map<String, String> params = new HashMap<>();
950 params.put("tracks-size", Integer.toString(tracks.size()));
951 params.put("tracks", StringUtils.join(tracks, ","));
952 incident().recordFailure(job, CONCAT_LESS_TRACKS, params);
953 throw new EncoderException("The track parameter must at least have two tracks present");
954 }
955
956 boolean onlyAudio = true;
957 for (Track t : tracks) {
958 if (t.hasVideo()) {
959 onlyAudio = false;
960 break;
961 }
962 }
963
964 if (!sameCodec && !onlyAudio && outputDimension == null) {
965 Map<String, String> params = new HashMap<>();
966 params.put("tracks", StringUtils.join(tracks, ","));
967 incident().recordFailure(job, CONCAT_NO_DIMENSION, params);
968 throw new EncoderException("The output dimension id parameter must not be null when concatenating video");
969 }
970
971 final String targetTrackId = IdImpl.fromUUID().toString();
972
973 List<File> trackFiles = new ArrayList<>();
974 int i = 0;
975 for (Track track : tracks) {
976 if (!track.hasAudio() && !track.hasVideo()) {
977 Map<String, String> params = new HashMap<>();
978 params.put("track-id", track.getIdentifier());
979 params.put("track-url", track.getURI().toString());
980 incident().recordFailure(job, NO_STREAMS, params);
981 throw new EncoderException("Track has no audio or video stream available: " + track);
982 }
983 trackFiles.add(i++, loadTrackIntoWorkspace(job, "concat", track, false));
984 }
985
986
987 final EncoderEngine encoderEngine = getEncoderEngine();
988
989 if (onlyAudio) {
990 logger.info("Concatenating audio tracks {} into {}", trackFiles, targetTrackId);
991 } else {
992 logger.info("Concatenating video tracks {} into {}", trackFiles, targetTrackId);
993 }
994
995
996 EncodingProfile profile = getProfile(job, profileId);
997 String concatCommand;
998 File fileList = null;
999
1000 if (sameCodec) {
1001
1002 fileList = new File(workspace.rootDirectory(), "concat_tracklist_" + job.getId() + ".txt");
1003 fileList.deleteOnExit();
1004 try (PrintWriter printer = new PrintWriter(new FileWriter(fileList, true))) {
1005 for (Track track : tracks) {
1006 printer.append("file '").append(workspace.get(track.getURI()).getAbsolutePath()).append("'\n");
1007 }
1008 } catch (IOException e) {
1009 throw new EncoderException("Cannot create file list for concat", e);
1010 } catch (NotFoundException e) {
1011 throw new EncoderException("Cannot find track filename in workspace for concat", e);
1012 }
1013 concatCommand = "-f concat -safe 0 -i " + fileList.getAbsolutePath();
1014 } else {
1015 concatCommand = buildConcatCommand(onlyAudio, outputDimension, outputFrameRate, trackFiles, tracks);
1016 }
1017
1018 Map<String, String> properties = new HashMap<>();
1019 properties.put(CMD_SUFFIX + ".concatCommand", concatCommand);
1020
1021 File output;
1022 try {
1023 output = encoderEngine.encode(trackFiles.get(0), profile, properties);
1024 } catch (EncoderException e) {
1025 Map<String, String> params = new HashMap<>();
1026 List<String> trackList = new ArrayList<>();
1027 for (Track t : tracks) {
1028 trackList.add(t.getURI().toString());
1029 }
1030 params.put("tracks", StringUtils.join(trackList, ","));
1031 params.put("profile", profile.getIdentifier());
1032 params.put("properties", properties.toString());
1033 incident().recordFailure(job, CONCAT_FAILED, e, params, detailsFor(e, encoderEngine));
1034 throw e;
1035 } finally {
1036 activeEncoder.remove(encoderEngine);
1037 if (fileList != null) {
1038 FileSupport.deleteQuietly(fileList);
1039 }
1040 }
1041
1042
1043 if (!output.exists() || output.length() == 0)
1044 return none();
1045
1046
1047 URI workspaceURI = putToCollection(job, output, "concatenated file");
1048
1049
1050 Track inspectedTrack = inspect(job, workspaceURI);
1051 inspectedTrack.setIdentifier(targetTrackId);
1052
1053 return some(inspectedTrack);
1054 }
1055
1056 @Override
1057 public Job imageToVideo(Attachment sourceImageAttachment, String profileId, double time) throws EncoderException,
1058 MediaPackageException {
1059 try {
1060 final EncodingProfile profile = profileScanner.getProfile(profileId);
1061 if (profile == null) {
1062 throw new MediaPackageException(String.format("Encoding profile %s not found", profileId));
1063 }
1064 return serviceRegistry.createJob(JOB_TYPE, Operation.ImageToVideo.toString(), Arrays.asList(
1065 profileId, MediaPackageElementParser.getAsXml(sourceImageAttachment), Double.toString(time)),
1066 profile.getJobLoad());
1067 } catch (ServiceRegistryException e) {
1068 throw new EncoderException("Unable to create image to video job", e);
1069 }
1070 }
1071
1072 private Option<Track> imageToVideo(Job job, Attachment sourceImage, String profileId, Double time)
1073 throws EncoderException, MediaPackageException {
1074
1075
1076 final EncodingProfile profile = getProfile(job, profileId);
1077
1078 final String targetTrackId = IdImpl.fromUUID().toString();
1079
1080 File imageFile;
1081 try {
1082 imageFile = workspace.get(sourceImage.getURI());
1083 } catch (NotFoundException e) {
1084 incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e,
1085 getWorkspaceMediapackageParams("source image", sourceImage), NO_DETAILS);
1086 throw new EncoderException("Requested source image " + sourceImage + " is not found");
1087 } catch (IOException e) {
1088 incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e,
1089 getWorkspaceMediapackageParams("source image", sourceImage), NO_DETAILS);
1090 throw new EncoderException("Unable to access source image " + sourceImage);
1091 }
1092
1093
1094 final EncoderEngine encoderEngine = getEncoderEngine();
1095
1096 logger.info("Converting image attachment {} into video {}", sourceImage.getIdentifier(), targetTrackId);
1097
1098 Map<String, String> properties = new HashMap<>();
1099 if (time == -1)
1100 time = 0D;
1101
1102 DecimalFormatSymbols ffmpegFormat = new DecimalFormatSymbols();
1103 ffmpegFormat.setDecimalSeparator('.');
1104 DecimalFormat df = new DecimalFormat("0.000", ffmpegFormat);
1105 properties.put("time", df.format(time));
1106
1107 File output;
1108 try {
1109 output = encoderEngine.encode(imageFile, profile, properties);
1110 } catch (EncoderException e) {
1111 Map<String, String> params = new HashMap<>();
1112 params.put("image", sourceImage.getURI().toString());
1113 params.put("profile", profile.getIdentifier());
1114 params.put("properties", properties.toString());
1115 incident().recordFailure(job, IMAGE_TO_VIDEO_FAILED, e, params, detailsFor(e, encoderEngine));
1116 throw e;
1117 } finally {
1118 activeEncoder.remove(encoderEngine);
1119 }
1120
1121
1122 if (!output.exists() || output.length() == 0)
1123 return none();
1124
1125
1126 URI workspaceURI = putToCollection(job, output, "converted image file");
1127
1128
1129 Track inspectedTrack = inspect(job, workspaceURI);
1130 inspectedTrack.setIdentifier(targetTrackId);
1131
1132 return some(inspectedTrack);
1133 }
1134
1135
1136
1137
1138
1139
1140 @Override
1141 public Job image(Track sourceTrack, String profileId, double... times) throws EncoderException, MediaPackageException {
1142 if (sourceTrack == null)
1143 throw new IllegalArgumentException("SourceTrack cannot be null");
1144
1145 if (times.length == 0)
1146 throw new IllegalArgumentException("At least one time argument has to be specified");
1147
1148 List<String> parameters = new ArrayList<>();
1149 parameters.add(profileId);
1150 parameters.add(MediaPackageElementParser.getAsXml(sourceTrack));
1151 parameters.add(Boolean.TRUE.toString());
1152 for (double time : times) {
1153 parameters.add(Double.toString(time));
1154 }
1155
1156 try {
1157 final EncodingProfile profile = profileScanner.getProfile(profileId);
1158 return serviceRegistry.createJob(JOB_TYPE, Operation.Image.toString(), parameters, profile.getJobLoad());
1159 } catch (ServiceRegistryException e) {
1160 throw new EncoderException("Unable to create a job", e);
1161 }
1162 }
1163
1164 @Override
1165 public List<Attachment> imageSync(Track sourceTrack, String profileId, double... time) throws EncoderException,
1166 MediaPackageException {
1167 Job job = null;
1168 try {
1169 final EncodingProfile profile = profileScanner.getProfile(profileId);
1170 job = serviceRegistry
1171 .createJob(
1172 JOB_TYPE, Operation.Image.toString(), null, null, false, profile.getJobLoad());
1173 job.setStatus(Job.Status.RUNNING);
1174 job = serviceRegistry.updateJob(job);
1175 final List<Attachment> images = extractImages(job, sourceTrack, profileId, null, time);
1176 job.setStatus(Job.Status.FINISHED);
1177 return images;
1178 } catch (ServiceRegistryException | NotFoundException e) {
1179 throw new EncoderException("Unable to create a job", e);
1180 } finally {
1181 finallyUpdateJob(job);
1182 }
1183 }
1184
1185 @Override
1186 public Job image(Track sourceTrack, String profileId, Map<String, String> properties) throws EncoderException,
1187 MediaPackageException {
1188 if (sourceTrack == null)
1189 throw new IllegalArgumentException("SourceTrack cannot be null");
1190
1191 List<String> arguments = new ArrayList<String>();
1192 arguments.add(profileId);
1193 arguments.add(MediaPackageElementParser.getAsXml(sourceTrack));
1194 arguments.add(Boolean.FALSE.toString());
1195 arguments.add(getPropertiesAsString(properties));
1196
1197 try {
1198 final EncodingProfile profile = profileScanner.getProfile(profileId);
1199 return serviceRegistry.createJob(JOB_TYPE, Operation.Image.toString(), arguments, profile.getJobLoad());
1200 } catch (ServiceRegistryException e) {
1201 throw new EncoderException("Unable to create a job", e);
1202 }
1203 }
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222 private List<Attachment> extractImages(Job job, Track sourceTrack, String profileId, Map<String, String> properties,
1223 double... times) throws EncoderException {
1224 if (sourceTrack == null) {
1225 throw new EncoderException("SourceTrack cannot be null");
1226 }
1227 logger.info("creating an image using video track {}", sourceTrack.getIdentifier());
1228
1229
1230 final EncodingProfile profile = getProfile(job, profileId);
1231
1232
1233 final EncoderEngine encoderEngine = getEncoderEngine();
1234
1235
1236 File videoFile = loadTrackIntoWorkspace(job, "video", sourceTrack, true);
1237
1238
1239 List<File> encodingOutput;
1240 try {
1241 encodingOutput = encoderEngine.extract(videoFile, profile, properties, times);
1242
1243 if (encodingOutput == null || encodingOutput.isEmpty()) {
1244 logger.error("Image extraction from video {} with profile {} failed: no images were produced",
1245 sourceTrack.getURI(), profile.getIdentifier());
1246 throw new EncoderException("Image extraction failed: no images were produced");
1247 }
1248 } catch (EncoderException e) {
1249 Map<String, String> params = new HashMap<>();
1250 params.put("video", sourceTrack.getURI().toString());
1251 params.put("profile", profile.getIdentifier());
1252 params.put("positions", Arrays.toString(times));
1253 incident().recordFailure(job, IMAGE_EXTRACTION_FAILED, e, params, detailsFor(e, encoderEngine));
1254 throw e;
1255 } finally {
1256 activeEncoder.remove(encoderEngine);
1257 }
1258
1259 int i = 0;
1260 List<URI> workspaceURIs = new LinkedList<>();
1261 for (File output : encodingOutput) {
1262
1263 if (!output.exists() || output.length() == 0) {
1264 logger.warn("Extracted image {} is empty!", output);
1265 throw new EncoderException("Extracted image " + output.toString() + " is empty!");
1266 }
1267
1268
1269
1270 try (InputStream in = new FileInputStream(output)) {
1271 URI returnURL = workspace.putInCollection(COLLECTION,
1272 job.getId() + "_" + i++ + "." + FilenameUtils.getExtension(output.getAbsolutePath()), in);
1273 logger.debug("Copied image file to the workspace at {}", returnURL);
1274 workspaceURIs.add(returnURL);
1275 } catch (Exception e) {
1276 cleanup(encodingOutput.toArray(new File[encodingOutput.size()]));
1277 cleanupWorkspace(workspaceURIs.toArray(new URI[workspaceURIs.size()]));
1278 incident().recordFailure(job, WORKSPACE_PUT_COLLECTION_IO_EXCEPTION, e,
1279 getWorkspaceCollectionParams("extracted image file", COLLECTION, output.toURI()), NO_DETAILS);
1280 throw new EncoderException("Unable to put image file into the workspace", e);
1281 }
1282 }
1283
1284
1285 cleanup(encodingOutput.toArray(new File[encodingOutput.size()]));
1286 cleanup(videoFile);
1287
1288 MediaPackageElementBuilder builder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
1289 List<Attachment> imageAttachments = new LinkedList<>();
1290 for (URI url : workspaceURIs) {
1291 Attachment attachment = (Attachment) builder.elementFromURI(url, Attachment.TYPE, null);
1292 try {
1293 attachment.setSize(workspace.get(url).length());
1294 } catch (NotFoundException | IOException e) {
1295 logger.warn("Could not get file size of {}", url);
1296 }
1297 imageAttachments.add(attachment);
1298 }
1299
1300 return imageAttachments;
1301 }
1302
1303
1304
1305
1306
1307
1308
1309 @Override
1310 public Job convertImage(Attachment image, String... profileIds) throws EncoderException, MediaPackageException {
1311 if (image == null)
1312 throw new IllegalArgumentException("Source image cannot be null");
1313
1314 if (profileIds == null)
1315 throw new IllegalArgumentException("At least one encoding profile must be set");
1316
1317 Gson gson = new Gson();
1318 List<String> params = Arrays.asList(gson.toJson(profileIds), MediaPackageElementParser.getAsXml(image));
1319 float jobLoad = Arrays.stream(profileIds)
1320 .map(p -> profileScanner.getProfile(p).getJobLoad())
1321 .max(Float::compare)
1322 .orElse(0.f);
1323 try {
1324 return serviceRegistry.createJob(JOB_TYPE, Operation.ImageConversion.toString(), params, jobLoad);
1325 } catch (ServiceRegistryException e) {
1326 throw new EncoderException("Unable to create a job", e);
1327 }
1328 }
1329
1330
1331
1332
1333
1334
1335
1336 @Override
1337 public List<Attachment> convertImageSync(Attachment image, String... profileIds) throws EncoderException,
1338 MediaPackageException {
1339 Job job = null;
1340 try {
1341 final float jobLoad = (float) Arrays.stream(profileIds)
1342 .map(p -> profileScanner.getProfile(p))
1343 .mapToDouble(EncodingProfile::getJobLoad)
1344 .max()
1345 .orElse(0);
1346 job = serviceRegistry
1347 .createJob(
1348 JOB_TYPE, Operation.Image.toString(), null, null, false, jobLoad);
1349 job.setStatus(Job.Status.RUNNING);
1350 job = serviceRegistry.updateJob(job);
1351 List<Attachment> results = convertImage(job, image, profileIds);
1352 job.setStatus(Job.Status.FINISHED);
1353 if (results.isEmpty()) {
1354 throw new EncoderException(format(
1355 "Unable to convert image %s with encoding profiles %s. The result set is empty.",
1356 image.getURI().toString(), profileIds));
1357 }
1358 return results;
1359 } catch (ServiceRegistryException | NotFoundException e) {
1360 throw new EncoderException("Unable to create a job", e);
1361 } finally {
1362 finallyUpdateJob(job);
1363 }
1364 }
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379 private List<Attachment> convertImage(Job job, Attachment sourceImage, String... profileIds) throws EncoderException,
1380 MediaPackageException {
1381 List<Attachment> convertedImages = new ArrayList<>();
1382 final EncoderEngine encoderEngine = getEncoderEngine();
1383 try {
1384 for (String profileId : profileIds) {
1385 logger.info("Converting {} using encoding profile {}", sourceImage, profileId);
1386
1387
1388 final EncodingProfile profile = getProfile(job, profileId);
1389
1390
1391 File imageFile;
1392 try {
1393 imageFile = workspace.get(sourceImage.getURI());
1394 } catch (NotFoundException e) {
1395 incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e,
1396 getWorkspaceMediapackageParams("source image", sourceImage), NO_DETAILS);
1397 throw new EncoderException("Requested attachment " + sourceImage + " was not found", e);
1398 } catch (IOException e) {
1399 incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e,
1400 getWorkspaceMediapackageParams("source image", sourceImage), NO_DETAILS);
1401 throw new EncoderException("Error accessing attachment " + sourceImage, e);
1402 }
1403
1404
1405 File output;
1406 try {
1407 output = encoderEngine.encode(imageFile, profile, null);
1408 } catch (EncoderException e) {
1409 Map<String, String> params = new HashMap<>();
1410 params.put("image", sourceImage.getURI().toString());
1411 params.put("profile", profile.getIdentifier());
1412 incident().recordFailure(job, CONVERT_IMAGE_FAILED, e, params, detailsFor(e, encoderEngine));
1413 throw e;
1414 }
1415
1416
1417 if (!output.exists() || output.length() == 0)
1418 throw new EncoderException(format(
1419 "Image conversion job %d didn't created an output file for the source image %s with encoding profile %s",
1420 job.getId(), sourceImage.getURI().toString(), profileId));
1421
1422
1423 URI workspaceURI = putToCollection(job, output, "converted image file");
1424
1425 MediaPackageElementBuilder builder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
1426 Attachment convertedImage = (Attachment) builder.elementFromURI(workspaceURI, Attachment.TYPE, null);
1427 convertedImage.setSize(output.length());
1428 convertedImage.setIdentifier(IdImpl.fromUUID().toString());
1429 try {
1430 convertedImage.setMimeType(MimeTypes.fromURI(convertedImage.getURI()));
1431 } catch (UnknownFileTypeException e) {
1432 logger.warn("Mime type unknown for file {}. Setting none.", convertedImage.getURI(), e);
1433 }
1434
1435 convertedImages.add(convertedImage);
1436 }
1437 } catch (Throwable t) {
1438 for (Attachment convertedImage : convertedImages) {
1439 try {
1440 workspace.delete(convertedImage.getURI());
1441 } catch (NotFoundException ex) {
1442
1443 } catch (IOException ex) {
1444 logger.warn("Unable to delete converted image {} from workspace", convertedImage.getURI(), ex);
1445 }
1446 }
1447 throw t;
1448 } finally {
1449 activeEncoder.remove(encoderEngine);
1450 }
1451 return convertedImages;
1452 }
1453
1454
1455
1456
1457
1458
1459 @Override
1460 protected String process(Job job) throws ServiceRegistryException {
1461 String operation = job.getOperation();
1462 List<String> arguments = job.getArguments();
1463 try {
1464 Operation op = Operation.valueOf(operation);
1465 Track firstTrack;
1466 Track secondTrack;
1467 String encodingProfile = arguments.get(0);
1468 final String serialized;
1469
1470 switch (op) {
1471 case Encode:
1472 firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(1));
1473 serialized = encode(job, Collections.map(tuple("video", firstTrack)), encodingProfile).map(
1474 MediaPackageElementParser.getAsXml()).getOrElse("");
1475 break;
1476 case ParallelEncode:
1477 firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(1));
1478 serialized = MediaPackageElementParser.getArrayAsXml(parallelEncode(job, firstTrack, encodingProfile));
1479 break;
1480 case Image:
1481 firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(1));
1482 List<Attachment> resultingElements;
1483 if (Boolean.parseBoolean(arguments.get(2))) {
1484 double[] times = new double[arguments.size() - 3];
1485 for (int i = 3; i < arguments.size(); i++) {
1486 times[i - 3] = Double.parseDouble(arguments.get(i));
1487 }
1488 resultingElements = extractImages(job, firstTrack, encodingProfile, null, times);
1489 } else {
1490 Map<String, String> properties = parseProperties(arguments.get(3));
1491 resultingElements = extractImages(job, firstTrack, encodingProfile, properties);
1492 }
1493 serialized = MediaPackageElementParser.getArrayAsXml(resultingElements);
1494 break;
1495 case ImageConversion:
1496 Gson gson = new Gson();
1497 String[] encodingProfilesArr = gson.fromJson(arguments.get(0), String[].class);
1498 Attachment sourceImage = (Attachment) MediaPackageElementParser.getFromXml(arguments.get(1));
1499 List<Attachment> convertedImages = convertImage(job, sourceImage, encodingProfilesArr);
1500 serialized = MediaPackageElementParser.getArrayAsXml(convertedImages);
1501 break;
1502 case Mux:
1503 firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(1));
1504 secondTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(2));
1505 serialized = mux(job, firstTrack, secondTrack, encodingProfile).map(
1506 MediaPackageElementParser.getAsXml()).getOrElse("");
1507 break;
1508 case Trim:
1509 firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(1));
1510 long start = Long.parseLong(arguments.get(2));
1511 long duration = Long.parseLong(arguments.get(3));
1512 serialized = trim(job, firstTrack, encodingProfile, start, duration).map(
1513 MediaPackageElementParser.getAsXml()).getOrElse("");
1514 break;
1515 case Composite:
1516 Attachment watermarkAttachment;
1517 firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(LOWER_TRACK_INDEX));
1518 Layout lowerLayout = Serializer.layout(JsonObj.jsonObj(arguments.get(LOWER_TRACK_LAYOUT_INDEX)));
1519 LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<>(firstTrack, lowerLayout);
1520 Option<LaidOutElement<Track>> upperLaidOutElement = Option.none();
1521 if (NOT_AVAILABLE.equals(arguments.get(UPPER_TRACK_INDEX))
1522 && NOT_AVAILABLE.equals(arguments.get(UPPER_TRACK_LAYOUT_INDEX))) {
1523 logger.trace("This composite action does not use a second track.");
1524 } else {
1525 secondTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(UPPER_TRACK_INDEX));
1526 Layout upperLayout = Serializer.layout(JsonObj.jsonObj(arguments.get(UPPER_TRACK_LAYOUT_INDEX)));
1527 upperLaidOutElement = Option.option(new LaidOutElement<Track>(secondTrack, upperLayout));
1528 }
1529 Dimension compositeTrackSize = Serializer
1530 .dimension(JsonObj.jsonObj(arguments.get(COMPOSITE_TRACK_SIZE_INDEX)));
1531 String backgroundColor = arguments.get(BACKGROUND_COLOR_INDEX);
1532 String audioSourceName = arguments.get(AUDIO_SOURCE_INDEX);
1533
1534 Option<LaidOutElement<Attachment>> watermarkOption = Option.none();
1535
1536 if (arguments.size() > WATERMARK_INDEX && arguments.size() <= WATERMARK_LAYOUT_INDEX + 1) {
1537 watermarkAttachment = (Attachment) MediaPackageElementParser.getFromXml(arguments.get(WATERMARK_INDEX));
1538 Layout watermarkLayout = Serializer.layout(JsonObj.jsonObj(arguments.get(WATERMARK_LAYOUT_INDEX)));
1539 watermarkOption = Option.some(new LaidOutElement<>(watermarkAttachment, watermarkLayout));
1540 } else if (arguments.size() > WATERMARK_LAYOUT_INDEX + 1) {
1541 throw new IndexOutOfBoundsException("Too many composite arguments!");
1542 }
1543 serialized = composite(job, compositeTrackSize, lowerLaidOutElement, upperLaidOutElement, watermarkOption,
1544 encodingProfile, backgroundColor, audioSourceName).map(MediaPackageElementParser.getAsXml()).getOrElse("");
1545 break;
1546 case Concat:
1547 String dimensionString = arguments.get(1);
1548 String frameRateString = arguments.get(2);
1549 Dimension outputDimension = null;
1550 if (StringUtils.isNotBlank(dimensionString))
1551 outputDimension = Serializer.dimension(JsonObj.jsonObj(dimensionString));
1552 float outputFrameRate = NumberUtils.toFloat(frameRateString, -1.0f);
1553 boolean sameCodec = Boolean.parseBoolean(arguments.get(3));
1554 List<Track> tracks = new ArrayList<>();
1555 for (int i = 4; i < arguments.size(); i++) {
1556 tracks.add(i - 4, (Track) MediaPackageElementParser.getFromXml(arguments.get(i)));
1557 }
1558 serialized = concat(job, tracks, encodingProfile, outputDimension, outputFrameRate, sameCodec).map(
1559 MediaPackageElementParser.getAsXml()).getOrElse("");
1560 break;
1561 case ImageToVideo:
1562 Attachment image = (Attachment) MediaPackageElementParser.getFromXml(arguments.get(1));
1563 double time = Double.parseDouble(arguments.get(2));
1564 serialized = imageToVideo(job, image, encodingProfile, time)
1565 .map(MediaPackageElementParser.getAsXml()).getOrElse("");
1566 break;
1567 case Demux:
1568 firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(1));
1569 List<Track> outTracks = demux(job, firstTrack, encodingProfile);
1570 serialized = StringUtils.trimToEmpty(MediaPackageElementParser.getArrayAsXml(outTracks));
1571 break;
1572 case ProcessSmil:
1573 Smil smil = this.smilService.fromXml(arguments.get(0)).getSmil();
1574 String trackParamGroupId = arguments.get(1);
1575 String mediaType = arguments.get(2);
1576 List<String> encodingProfiles = arguments.subList(3, arguments.size());
1577 outTracks = processSmil(job, smil, trackParamGroupId, mediaType, encodingProfiles);
1578 serialized = StringUtils.trimToEmpty(MediaPackageElementParser.getArrayAsXml(outTracks));
1579 break;
1580 case MultiEncode:
1581 firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(0));
1582 List<String> encodingProfiles2 = arguments.subList(1, arguments.size());
1583 outTracks = multiEncode(job, firstTrack, encodingProfiles2);
1584 serialized = StringUtils.trimToEmpty(MediaPackageElementParser.getArrayAsXml(outTracks));
1585 break;
1586 default:
1587 throw new IllegalStateException("Don't know how to handle operation '" + operation + "'");
1588 }
1589
1590 return serialized;
1591 } catch (IllegalArgumentException e) {
1592 throw new ServiceRegistryException(format("Cannot handle operations of type '%s'", operation), e);
1593 } catch (IndexOutOfBoundsException e) {
1594 throw new ServiceRegistryException(format("Invalid arguments for operation '%s'", operation), e);
1595 } catch (Exception e) {
1596 throw new ServiceRegistryException(format("Error handling operation '%s'", operation), e);
1597 }
1598 }
1599
1600
1601
1602
1603
1604
1605 @Override
1606 public EncodingProfile[] listProfiles() {
1607 Collection<EncodingProfile> profiles = profileScanner.getProfiles().values();
1608 return profiles.toArray(new EncodingProfile[profiles.size()]);
1609 }
1610
1611
1612
1613
1614
1615
1616 @Override
1617 public EncodingProfile getProfile(String profileId) {
1618 return profileScanner.getProfiles().get(profileId);
1619 }
1620
1621 protected List<Track> inspect(Job job, List<URI> uris, List<List<String>> tags) throws EncoderException {
1622
1623 Job[] inspectionJobs = new Job[uris.size()];
1624 for (int i = 0; i < uris.size(); i++) {
1625 try {
1626 inspectionJobs[i] = inspectionService.inspect(uris.get(i));
1627 } catch (MediaInspectionException e) {
1628 incident().recordJobCreationIncident(job, e);
1629 throw new EncoderException(String.format("Media inspection of %s failed", uris.get(i)), e);
1630 }
1631 }
1632
1633
1634 JobBarrier barrier = new JobBarrier(job, serviceRegistry, inspectionJobs);
1635 if (!barrier.waitForJobs().isSuccess()) {
1636 for (Map.Entry<Job, Job.Status> result : barrier.getStatus().getStatus().entrySet()) {
1637 if (result.getValue() != Job.Status.FINISHED) {
1638 logger.error("Media inspection failed in job {}: {}", result.getKey(), result.getValue());
1639 }
1640 }
1641 throw new EncoderException("Inspection of encoded file failed");
1642 }
1643
1644
1645 List<Track> results = new ArrayList<>(uris.size());
1646 int i = 0;
1647 for (Job inspectionJob: inspectionJobs) {
1648 try {
1649 Track track = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload());
1650 List<String> tagsForTrack = tags.get(i);
1651 for (String tag : tagsForTrack) {
1652 track.addTag(tag);
1653 }
1654 results.add(track);
1655 } catch (MediaPackageException e) {
1656 throw new EncoderException(e);
1657 }
1658 i++;
1659 }
1660 return results;
1661 }
1662
1663 protected List<Track> inspect(Job job, List<URI> uris) throws EncoderException {
1664 List<List<String>> tags = java.util.Collections.nCopies(uris.size(), new ArrayList<String>());
1665 return inspect(job, uris, tags);
1666 }
1667
1668 protected Track inspect(Job job, URI workspaceURI) throws EncoderException {
1669 return inspect(job, java.util.Collections.singletonList(workspaceURI)).get(0);
1670 }
1671
1672
1673
1674
1675
1676
1677
1678 private void cleanup(File... encodingOutput) {
1679 for (File file : encodingOutput) {
1680 if (file != null && file.isFile()) {
1681 String path = file.getAbsolutePath();
1682 if (file.delete()) {
1683 logger.info("Deleted local copy of encoding file at {}", path);
1684 } else {
1685 logger.warn("Could not delete local copy of encoding file at {}", path);
1686 }
1687 }
1688 }
1689 }
1690
1691 private void cleanupWorkspace(URI... workspaceURIs) {
1692 for (URI url : workspaceURIs) {
1693 try {
1694 workspace.delete(url);
1695 } catch (Exception e) {
1696 logger.warn("Could not delete {} from workspace: {}", url, e.getMessage());
1697 }
1698 }
1699 }
1700
1701 private EncoderEngine getEncoderEngine() {
1702 EncoderEngine engine = new EncoderEngine(ffmpegBinary);
1703 activeEncoder.add(engine);
1704 return engine;
1705 }
1706
1707 private EncodingProfile getProfile(Job job, String profileId) throws EncoderException {
1708 final EncodingProfile profile = profileScanner.getProfile(profileId);
1709 if (profile == null) {
1710 final String msg = format("Profile %s is unknown", profileId);
1711 logger.error(msg);
1712 incident().recordFailure(job, PROFILE_NOT_FOUND, Collections.map(tuple("profile", profileId)));
1713 throw new EncoderException(msg);
1714 }
1715 return profile;
1716 }
1717
1718 private Map<String, String> getWorkspaceMediapackageParams(String description, MediaPackageElement element) {
1719 return Collections.map(tuple("description", description),
1720 tuple("type", element.getElementType().toString()),
1721 tuple("url", element.getURI().toString()));
1722 }
1723
1724 private Map<String, String> getWorkspaceCollectionParams(String description, String collectionId, URI url) {
1725 Map<String, String> params = new HashMap<>();
1726 params.put("description", description);
1727 params.put("collection", collectionId);
1728 params.put("url", url.toString());
1729 return params;
1730 }
1731
1732 private String buildConcatCommand(boolean onlyAudio, Dimension dimension, float outputFrameRate, List<File> files, List<Track> tracks) {
1733 StringBuilder sb = new StringBuilder();
1734
1735
1736 for (File f : files) {
1737 sb.append("-i ").append(f.getAbsolutePath()).append(" ");
1738 }
1739 sb.append("-filter_complex ");
1740
1741 boolean hasAudio = false;
1742 if (!onlyAudio) {
1743
1744 String fpsFilter = StringUtils.EMPTY;
1745 if (outputFrameRate > 0) {
1746 fpsFilter = format(Locale.US, "fps=fps=%f,", outputFrameRate);
1747 }
1748
1749 int characterCount = 0;
1750 for (int i = 0; i < files.size(); i++) {
1751 if ((i % 25) == 0)
1752 characterCount++;
1753 sb.append("[").append(i).append(":v]").append(fpsFilter)
1754 .append("scale=iw*min(").append(dimension.getWidth()).append("/iw\\,").append(dimension.getHeight())
1755 .append("/ih):ih*min(").append(dimension.getWidth()).append("/iw\\,").append(dimension.getHeight())
1756 .append("/ih),pad=").append(dimension.getWidth()).append(":").append(dimension.getHeight())
1757 .append(":(ow-iw)/2:(oh-ih)/2").append(",setdar=")
1758 .append((float) dimension.getWidth() / (float) dimension.getHeight()).append("[");
1759 int character = ('a' + i + 1 - ((characterCount - 1) * 25));
1760 for (int y = 0; y < characterCount; y++) {
1761 sb.append((char) character);
1762 }
1763 sb.append("];");
1764 if (tracks.get(i).hasAudio())
1765 hasAudio = true;
1766 }
1767
1768
1769 if (hasAudio) {
1770 for (int i = 0; i < files.size(); i++) {
1771 if (!tracks.get(i).hasAudio())
1772 sb.append("aevalsrc=0:d=1[silent").append(i + 1).append("];");
1773 }
1774 }
1775 }
1776
1777
1778 int characterCount = 0;
1779 for (int i = 0; i < files.size(); i++) {
1780 if ((i % 25) == 0)
1781 characterCount++;
1782
1783 int character = ('a' + i + 1 - ((characterCount - 1) * 25));
1784 if (!onlyAudio) {
1785 sb.append("[");
1786 for (int y = 0; y < characterCount; y++) {
1787 sb.append((char) character);
1788 }
1789 sb.append("]");
1790 }
1791
1792 if (tracks.get(i).hasAudio()) {
1793 sb.append("[").append(i).append(":a]");
1794 } else if (hasAudio) {
1795 sb.append("[silent").append(i + 1).append("]");
1796 }
1797 }
1798
1799
1800 sb.append("concat=n=").append(files.size()).append(":v=");
1801 if (onlyAudio) {
1802 sb.append("0");
1803 } else {
1804 sb.append("1");
1805 }
1806 sb.append(":a=");
1807
1808 if (!onlyAudio) {
1809 if (hasAudio) {
1810 sb.append("1[v][a] -map [v] -map [a] ");
1811 } else {
1812 sb.append("0[v] -map [v] ");
1813 }
1814 } else {
1815 sb.append("1[a] -map [a]");
1816 }
1817 return sb.toString();
1818 }
1819
1820
1821
1822 protected void hlsFixReference(long id, List<File> outputs) throws IOException {
1823 Map<String, String> nameMap = outputs.stream().collect(Collectors.<File, String, String> toMap(
1824 file -> FilenameUtils.getName(file.getAbsolutePath()), file -> renameJobFile(id, file)));
1825 for (File file : outputs) {
1826 if (AdaptivePlaylist.isPlaylist(file))
1827 AdaptivePlaylist.hlsRewriteFileReference(file, nameMap);
1828 }
1829 }
1830
1831
1832
1833
1834 private String renameJobFile(long jobId, File file) {
1835 return workspace.toSafeName(format("%s.%s", jobId, FilenameUtils.getName(file.getAbsolutePath())));
1836 }
1837
1838 protected void hlsSetReference(Track track) throws IOException {
1839 if (!AdaptivePlaylist.checkForMaster(new File(track.getURI().getPath())))
1840 track.setLogicalName(FilenameUtils.getName(track.getURI().getPath()));
1841 }
1842
1843 private List<URI> putToCollection(Job job, List<File> files, String description) throws EncoderException {
1844 List<URI> returnURLs = new ArrayList<>(files.size());
1845 for (File file: files) {
1846 try (InputStream in = new FileInputStream(file)) {
1847 URI newFileURI = workspace.putInCollection(COLLECTION, renameJobFile(job.getId(), file), in);
1848 logger.info("Copied the {} to the workspace at {}", description, newFileURI);
1849 returnURLs.add(newFileURI);
1850 } catch (Exception e) {
1851 incident().recordFailure(job, WORKSPACE_PUT_COLLECTION_IO_EXCEPTION, e,
1852 getWorkspaceCollectionParams(description, COLLECTION, file.toURI()), NO_DETAILS);
1853 returnURLs.forEach(this::cleanupWorkspace);
1854 throw new EncoderException("Unable to put the " + description + " into the workspace", e);
1855 } finally {
1856 cleanup(file);
1857 }
1858 }
1859 return returnURLs;
1860 }
1861
1862 private URI putToCollection(Job job, File output, String description) throws EncoderException {
1863 return putToCollection(job, java.util.Collections.singletonList(output), description).get(0);
1864 }
1865
1866 private static List<Tuple<String, String>> detailsFor(EncoderException ex, EncoderEngine engine) {
1867 final List<Tuple<String, String>> d = new ArrayList<>();
1868 d.add(tuple("encoder-engine-class", engine.getClass().getName()));
1869 return d;
1870 }
1871
1872 private Map<String, String> parseProperties(String serializedProperties) throws IOException {
1873 Properties properties = new Properties();
1874 try (InputStream in = IOUtils.toInputStream(serializedProperties, "UTF-8")) {
1875 properties.load(in);
1876 Map<String, String> map = new HashMap<>();
1877 for (Entry<Object, Object> e : properties.entrySet()) {
1878 map.put((String) e.getKey(), (String) e.getValue());
1879 }
1880 return map;
1881 }
1882 }
1883
1884 private String getPropertiesAsString(Map<String, String> props) {
1885 StringBuilder sb = new StringBuilder();
1886 for (Entry<String, String> entry : props.entrySet()) {
1887 sb.append(entry.getKey());
1888 sb.append("=");
1889 sb.append(entry.getValue());
1890 sb.append("\n");
1891 }
1892 return sb.toString();
1893 }
1894
1895
1896
1897
1898
1899
1900
1901 @Reference
1902 protected void setMediaInspectionService(MediaInspectionService mediaInspectionService) {
1903 this.inspectionService = mediaInspectionService;
1904 }
1905
1906
1907
1908
1909
1910
1911
1912 @Reference
1913 protected void setWorkspace(Workspace workspace) {
1914 this.workspace = workspace;
1915 }
1916
1917
1918
1919
1920
1921
1922
1923 @Reference
1924 protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
1925 this.serviceRegistry = serviceRegistry;
1926 }
1927
1928
1929
1930
1931
1932
1933 @Override
1934 protected ServiceRegistry getServiceRegistry() {
1935 return serviceRegistry;
1936 }
1937
1938
1939
1940
1941
1942
1943
1944 @Reference
1945 protected void setProfileScanner(EncodingProfileScanner scanner) {
1946 this.profileScanner = scanner;
1947 }
1948
1949
1950
1951
1952
1953
1954
1955 @Reference
1956 public void setSecurityService(SecurityService securityService) {
1957 this.securityService = securityService;
1958 }
1959
1960
1961
1962
1963
1964
1965
1966 @Reference
1967 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
1968 this.userDirectoryService = userDirectoryService;
1969 }
1970
1971
1972
1973
1974
1975
1976
1977 @Reference
1978 public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
1979 this.organizationDirectoryService = organizationDirectory;
1980 }
1981
1982
1983
1984
1985
1986
1987 @Override
1988 protected SecurityService getSecurityService() {
1989 return securityService;
1990 }
1991
1992 @Reference
1993 public void setSmilService(SmilService smilService) {
1994 this.smilService = smilService;
1995 }
1996
1997
1998
1999
2000
2001
2002 @Override
2003 protected UserDirectoryService getUserDirectoryService() {
2004 return userDirectoryService;
2005 }
2006
2007
2008
2009
2010
2011
2012 @Override
2013 protected OrganizationDirectoryService getOrganizationDirectoryService() {
2014 return organizationDirectoryService;
2015 }
2016
2017 @Reference(target = "(artifact=encodingprofile)")
2018 public void setEncodingProfileReadinessIndicator(ReadinessIndicator unused) {
2019
2020 }
2021
2022 @Override
2023 public Job demux(Track sourceTrack, String profileId) throws EncoderException, MediaPackageException {
2024 try {
2025 return serviceRegistry.createJob(JOB_TYPE, Operation.Demux.toString(),
2026 Arrays.asList(profileId, MediaPackageElementParser.getAsXml(sourceTrack)));
2027 } catch (ServiceRegistryException e) {
2028 throw new EncoderException("Unable to create a job", e);
2029 }
2030 }
2031
2032 private List<Track> demux(final Job job, Track videoTrack, String encodingProfile) throws EncoderException {
2033 if (job == null)
2034 throw new IllegalArgumentException("The Job parameter must not be null");
2035
2036 try {
2037
2038 final File videoFile = (videoTrack != null) ? loadTrackIntoWorkspace(job, "source", videoTrack, false) : null;
2039
2040
2041 EncodingProfile profile = getProfile(job, encodingProfile);
2042
2043 logger.info("Encoding video track {} using profile '{}'", videoTrack.getIdentifier(), profile);
2044 final EncoderEngine encoderEngine = getEncoderEngine();
2045
2046
2047 List<File> outputs;
2048 try {
2049 Map<String, File> source = new HashMap<>();
2050 source.put("video", videoFile);
2051 outputs = encoderEngine.process(source, profile, null);
2052 } catch (EncoderException e) {
2053 Map<String, String> params = new HashMap<>();
2054 params.put("video", (videoFile != null) ? videoTrack.getURI().toString() : "EMPTY");
2055 params.put("profile", profile.getIdentifier());
2056 params.put("properties", "EMPTY");
2057 incident().recordFailure(job, ENCODING_FAILED, e, params, detailsFor(e, encoderEngine));
2058 throw e;
2059 } finally {
2060 activeEncoder.remove(encoderEngine);
2061 }
2062
2063
2064 if (outputs.isEmpty() || !outputs.get(0).exists() || outputs.get(0).length() == 0)
2065 return null;
2066
2067 List<URI> workspaceURIs = putToCollection(job, outputs, "demuxed file");
2068 List<Track> tracks = inspect(job, workspaceURIs);
2069 tracks.forEach(track -> track.setIdentifier(IdImpl.fromUUID().toString()));
2070 return tracks;
2071 } catch (Exception e) {
2072 logger.warn("Demux/MultiOutputEncode operation failed to encode " + videoTrack, e);
2073 if (e instanceof EncoderException) {
2074 throw (EncoderException) e;
2075 } else {
2076 throw new EncoderException(e);
2077 }
2078 }
2079 }
2080
2081
2082
2083
2084
2085
2086 @Modified
2087 public void modified(Map<String, Object> config)
2088 throws ConfigurationException {
2089 logger.debug("Modified");
2090 }
2091
2092 @Override
2093 public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
2094 if (properties == null) {
2095 logger.info("No configuration available, using defaults");
2096 return;
2097 }
2098
2099 maxMultipleProfilesJobLoad = LoadUtil.getConfiguredLoadValue(properties, JOB_LOAD_MAX_MULTIPLE_PROFILES,
2100 DEFAULT_JOB_LOAD_MAX_MULTIPLE_PROFILES, serviceRegistry);
2101 processSmilJobLoadFactor = LoadUtil.getConfiguredLoadValue(properties, JOB_LOAD_FACTOR_PROCESS_SMIL,
2102 DEFAULT_PROCESS_SMIL_JOB_LOAD_FACTOR, serviceRegistry);
2103 multiEncodeJobLoadFactor = LoadUtil.getConfiguredLoadValue(properties, JOB_LOAD_FACTOR_MULTI_ENCODE,
2104 DEFAULT_MULTI_ENCODE_JOB_LOAD_FACTOR, serviceRegistry);
2105
2106 String multiEncodeFadeStr = StringUtils.trimToNull((String) properties.get(MULTI_ENCODE_FADE_MILLISECONDS));
2107 multiEncodeTrim = DEFAULT_MULTI_ENCODE_FADE_MILLISECONDS;
2108 if (multiEncodeFadeStr != null) {
2109 multiEncodeFade = Integer.parseInt(multiEncodeFadeStr);
2110 }
2111 String multiEncodeTrimStr = StringUtils.trimToNull((String) properties.get(MULTI_ENCODE_TRIM_MILLISECONDS));
2112 multiEncodeTrim = DEFAULT_MULTI_ENCODE_TRIM_MILLISECONDS;
2113 if (multiEncodeTrimStr != null) {
2114 multiEncodeTrim = Integer.parseInt(multiEncodeTrimStr);
2115 }
2116 transitionDuration = (int) (1000 * LoadUtil.getConfiguredLoadValue(properties,
2117 PROCESS_SMIL_CLIP_TRANSITION_DURATION, DEFAULT_PROCESS_SMIL_CLIP_TRANSITION_DURATION, serviceRegistry));
2118 }
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138 @Override
2139 public Job processSmil(Smil smil, String trackparamId, String mediaType, List<String> profileIds)
2140 throws EncoderException, MediaPackageException {
2141 try {
2142 ArrayList<String> al = new ArrayList<>();
2143 al.add(smil.toXML());
2144 al.add(trackparamId);
2145 al.add(mediaType);
2146 for (String i : profileIds) {
2147 al.add(i);
2148 }
2149 float load = calculateJobLoadForMultipleProfiles(profileIds, processSmilJobLoadFactor);
2150 try {
2151 for (SmilMediaParamGroup paramGroup : smil.getHead().getParamGroups()) {
2152 for (SmilMediaParam param : paramGroup.getParams()) {
2153 if (SmilMediaParam.PARAM_NAME_TRACK_ID.equals(param.getName())) {
2154 if (trackparamId == null || trackparamId.equals(paramGroup.getId())) {
2155 al.set(1, paramGroup.getId());
2156 return (serviceRegistry.createJob(JOB_TYPE, Operation.ProcessSmil.toString(), al, load));
2157 }
2158 }
2159 }
2160 }
2161 } catch (ServiceRegistryException e) {
2162 throw new EncoderException("Unable to create a job", e);
2163 } catch (Exception e) {
2164 throw new EncoderException("Unable to create a job - Exception in Parsing Smil", e);
2165 }
2166 } catch (Exception e) {
2167 throw new EncoderException("Unable to create a job - Exception processing XML in ProcessSmil", e);
2168 }
2169 throw new EncoderException("Unable to create a job - Cannot find paramGroup");
2170 }
2171
2172 private List<EncodingProfile> findSuitableProfiles(List<String> encodingProfiles, String mediaType) {
2173 List<EncodingProfile> profiles = new ArrayList<>();
2174 for (String profileId1 : encodingProfiles) {
2175 EncodingProfile profile = profileScanner.getProfile(profileId1);
2176
2177 if (VIDEO_ONLY.equals(mediaType) && profile.getApplicableMediaType() == EncodingProfile.MediaType.Audio) {
2178 logger.warn("Profile '{}' supports {} but media is Video Only", profileId1, profile.getApplicableMediaType());
2179 } else if (AUDIO_ONLY.equals(mediaType) && profile.getApplicableMediaType() == EncodingProfile.MediaType.Visual) {
2180 logger.warn("Profile '{}' supports {} but media is Audio Only", profileId1, profile.getApplicableMediaType());
2181 }
2182 profiles.add(profile);
2183 }
2184 return (profiles);
2185 }
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197 private SmilMediaParamGroup getSmilMediaParamGroup(Smil smil, String trackParamGroupId) throws EncoderException {
2198 try {
2199 if (trackParamGroupId == null)
2200 for (SmilMediaParamGroup paramGroup : smil.getHead().getParamGroups()) {
2201 for (SmilMediaParam param : paramGroup.getParams()) {
2202 if (SmilMediaParam.PARAM_NAME_TRACK_ID.equals(param.getName())) {
2203 trackParamGroupId = paramGroup.getId();
2204 break;
2205 }
2206 }
2207 }
2208 return ((SmilMediaParamGroup) smil.get(trackParamGroupId));
2209 } catch (SmilException ex) {
2210 throw new EncoderException("Smil does not contain a paramGroup element with Id " + trackParamGroupId, ex);
2211 }
2212 }
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235 protected List<Track> processSmil(Job job, Smil smil, String trackParamGroupId, String mediaType,
2236 List<String> encodingProfiles) throws EncoderException, MediaPackageException, URISyntaxException {
2237
2238 List<EncodingProfile> profiles = findSuitableProfiles(encodingProfiles, mediaType);
2239
2240 if (profiles.size() == 0)
2241 throw new EncoderException(
2242 "ProcessSmil - Media is not supported by the assigned encoding Profiles '" + encodingProfiles + "'");
2243
2244 SmilMediaParamGroup trackParamGroup;
2245 ArrayList<String> inputfile = new ArrayList<>();
2246 Map<String, String> props = new HashMap<>();
2247
2248 ArrayList<VideoClip> videoclips = new ArrayList<>();
2249 trackParamGroup = getSmilMediaParamGroup(smil, trackParamGroupId);
2250
2251 String sourceTrackId = null;
2252 MediaPackageElementFlavor sourceTrackFlavor = null;
2253 String sourceTrackUri = null;
2254 File sourceFile = null;
2255
2256
2257 for (SmilMediaParam param : trackParamGroup.getParams()) {
2258 if (SmilMediaParam.PARAM_NAME_TRACK_ID.equals(param.getName())) {
2259 sourceTrackId = param.getValue();
2260 } else if (SmilMediaParam.PARAM_NAME_TRACK_SRC.equals(param.getName())) {
2261 sourceTrackUri = param.getValue();
2262 } else if (SmilMediaParam.PARAM_NAME_TRACK_FLAVOR.equals(param.getName())) {
2263 sourceTrackFlavor = MediaPackageElementFlavor.parseFlavor(param.getValue());
2264 }
2265 }
2266
2267 logger.info("ProcessSmil: Start processing track {}", sourceTrackUri);
2268 sourceFile = loadURIIntoWorkspace(job, "source", new URI(sourceTrackUri));
2269 inputfile.add(sourceFile.getAbsolutePath());
2270 props.put("in.video.path", sourceFile.getAbsolutePath());
2271 int srcIndex = inputfile.indexOf(sourceFile.getAbsolutePath());
2272 try {
2273 List<File> outputs;
2274
2275 for (SmilMediaObject element : smil.getBody().getMediaElements()) {
2276
2277 if (element.isContainer()) {
2278 SmilMediaContainer container = (SmilMediaContainer) element;
2279 if (SmilMediaContainer.ContainerType.PAR == container.getContainerType()) {
2280
2281 for (SmilMediaObject elementChild : container.getElements()) {
2282 if (!elementChild.isContainer()) {
2283 SmilMediaElement media = (SmilMediaElement) elementChild;
2284 if (trackParamGroupId.equals(media.getParamGroup())) {
2285 long begin = media.getClipBeginMS();
2286 long end = media.getClipEndMS();
2287 URI clipTrackURI = media.getSrc();
2288 File clipSourceFile = null;
2289 if (clipTrackURI != null) {
2290 clipSourceFile = loadURIIntoWorkspace(job, "Source", clipTrackURI);
2291 }
2292 if (sourceFile == null) {
2293 sourceFile = clipSourceFile;
2294 }
2295 int index = -1;
2296
2297 if (clipSourceFile != null) {
2298 index = inputfile.indexOf(clipSourceFile.getAbsolutePath());
2299 if (index < 0) {
2300 inputfile.add(clipSourceFile.getAbsolutePath());
2301 props.put("in.video.path" + index, sourceFile.getAbsolutePath());
2302 index = inputfile.indexOf(clipSourceFile.getAbsolutePath());
2303 }
2304 } else {
2305 index = srcIndex;
2306 }
2307 logger.debug("Adding edit clip index " + index + " begin " + begin + " end " + end + " to "
2308 + sourceTrackId);
2309 videoclips.add(new VideoClip(index, begin, end));
2310 }
2311 } else {
2312 throw new EncoderException("Smil container '"
2313 + ((SmilMediaContainer) elementChild).getContainerType().toString() + "'is not supported yet");
2314 }
2315 }
2316 } else {
2317 throw new EncoderException(
2318 "Smil container '" + container.getContainerType().toString() + "'is not supported yet");
2319 }
2320 }
2321 }
2322 List<Long> edits = new ArrayList<>();
2323 for (VideoClip clip : videoclips) {
2324 edits.add((long) clip.getSrc());
2325 edits.add(clip.getStartMS());
2326 edits.add(clip.getEndMS());
2327 }
2328 List<File> inputs = new ArrayList<>();
2329 for (String f : inputfile) {
2330 inputs.add(new File(f));
2331 }
2332 EncoderEngine encoderEngine = getEncoderEngine();
2333 try {
2334 outputs = encoderEngine.multiTrimConcat(inputs, edits, profiles, transitionDuration,
2335 !AUDIO_ONLY.equals(mediaType), !VIDEO_ONLY.equals(mediaType));
2336 } catch (EncoderException e) {
2337 Map<String, String> params = new HashMap<>();
2338 List<String> profileList = new ArrayList<>();
2339 for (EncodingProfile p : profiles) {
2340 profileList.add(p.getIdentifier().toString());
2341 }
2342 params.put("videos", StringUtils.join(inputs, ","));
2343 params.put("profiles", StringUtils.join(profileList,","));
2344 incident().recordFailure(job, PROCESS_SMIL_FAILED, e, params, detailsFor(e, encoderEngine));
2345 throw e;
2346 } finally {
2347 activeEncoder.remove(encoderEngine);
2348 }
2349 logger.info("ProcessSmil returns {} media files ", outputs.size());
2350 boolean isHLS = outputs.parallelStream().anyMatch(AdaptivePlaylist.isHLSFilePred);
2351
2352
2353
2354
2355 if (isHLS) {
2356 hlsFixReference(job.getId(), outputs);
2357 }
2358 logger.info("ProcessSmil/MultiTrimConcat returns {} media files {}", outputs.size(), outputs);
2359 List<URI> workspaceURIs = putToCollection(job, outputs, "processSmil files");
2360 List<Track> tracks = inspect(job, workspaceURIs);
2361 if (isHLS) {
2362 tracks.forEach(eachtrack -> AdaptivePlaylist.setLogicalName(eachtrack));
2363 }
2364 tracks.forEach(track -> track.setIdentifier(IdImpl.fromUUID().toString()));
2365 return tracks;
2366 } catch (Exception e) {
2367 throw new EncoderException("ProcessSmil operation failed to run ", e);
2368 }
2369 }
2370
2371 @Override
2372 public Job multiEncode(Track sourceTrack, List<String> profileIds) throws EncoderException, MediaPackageException {
2373 try {
2374
2375 float load = calculateJobLoadForMultipleProfiles(profileIds, multiEncodeJobLoadFactor);
2376 ArrayList<String> args = new ArrayList<>();
2377 args.add(MediaPackageElementParser.getAsXml(sourceTrack));
2378 args.addAll(profileIds);
2379 return serviceRegistry.createJob(JOB_TYPE, Operation.MultiEncode.toString(), args, load);
2380 } catch (ServiceRegistryException e) {
2381 throw new EncoderException("Unable to create a job", e);
2382 }
2383 }
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402 protected List<Track> multiEncode(final Job job, Track track, List<String> profileIds)
2403 throws EncoderException, IllegalArgumentException {
2404 if (job == null)
2405 throw new IllegalArgumentException("The Job parameter must not be null");
2406 if (track == null)
2407 throw new IllegalArgumentException("Source track cannot be null");
2408 if (profileIds == null || profileIds.isEmpty())
2409 throw new IllegalArgumentException("Cannot encode without encoding profiles");
2410 List<File> outputs = null;
2411 try {
2412 final File videoFile = loadTrackIntoWorkspace(job, "source", track, false);
2413
2414 List<EncodingProfile> profiles = new ArrayList<>();
2415 for (String profileId : profileIds) {
2416 EncodingProfile profile = getProfile(job, profileId);
2417 profiles.add(profile);
2418 }
2419 final long nominalTrim = multiEncodeTrim;
2420 List<Long> edits = null;
2421 if (nominalTrim > 0) {
2422 edits = new ArrayList<>();
2423 edits.add((long) 0);
2424 edits.add(nominalTrim);
2425 edits.add(track.getDuration() - nominalTrim);
2426 }
2427 logger.info("Encoding source track {} using profiles '{}'", track.getIdentifier(), profileIds);
2428
2429 EncoderEngine encoderEngine = getEncoderEngine();
2430 try {
2431 outputs = encoderEngine.multiTrimConcat(Arrays.asList(videoFile), null, profiles, multiEncodeFade,
2432 track.hasVideo(),
2433 track.hasAudio());
2434 } catch (EncoderException e) {
2435 Map<String, String> params = new HashMap<>();
2436 params.put("videos", videoFile.getName());
2437 params.put("profiles", StringUtils.join(profileIds, ","));
2438 incident().recordFailure(job, MULTI_ENCODE_FAILED, e, params, detailsFor(e, encoderEngine));
2439 throw e;
2440 } finally {
2441 activeEncoder.remove(encoderEngine);
2442 }
2443 logger.info("MultiEncode returns {} media files {} ", outputs.size(), outputs);
2444 List<File> saveFiles = outputs;
2445 boolean isHLS = outputs.parallelStream().anyMatch(AdaptivePlaylist.isHLSFilePred);
2446 if (isHLS)
2447 hlsFixReference(job.getId(), outputs);
2448
2449 List<URI> workspaceURIs = putToCollection(job, saveFiles, "multiencode files");
2450 List<Track> tracks = inspect(job, workspaceURIs);
2451 if (isHLS)
2452 tracks.forEach(eachtrack -> AdaptivePlaylist.setLogicalName(eachtrack));
2453 tracks.forEach(eachtrack -> eachtrack.setIdentifier(IdImpl.fromUUID().toString()));
2454 return tracks;
2455 } catch (Exception e) {
2456 throw new EncoderException("MultiEncode operation failed to run ", e);
2457 }
2458 }
2459
2460 private float calculateJobLoadForMultipleProfiles(List<String> profileIds, float adjustmentFactor)
2461 throws EncoderException {
2462
2463
2464
2465 float load = 0.0f;
2466 for (String profileId : profileIds) {
2467 EncodingProfile profile = profileScanner.getProfile(profileId);
2468 if (profile == null) {
2469 throw new EncoderException("Encoding profile not found: " + profileId);
2470 }
2471 load += profile.getJobLoad();
2472 }
2473 load *= adjustmentFactor;
2474 if (load > maxMultipleProfilesJobLoad) {
2475 load = maxMultipleProfilesJobLoad;
2476 }
2477 return load;
2478 }
2479 }