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