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