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