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.editor;
23
24 import static java.util.Collections.emptyList;
25 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
26 import static org.opencastproject.util.data.Tuple.tuple;
27
28 import org.opencastproject.assetmanager.api.AssetManager;
29 import org.opencastproject.assetmanager.api.AssetManagerException;
30 import org.opencastproject.assetmanager.util.WorkflowPropertiesUtil;
31 import org.opencastproject.assetmanager.util.Workflows;
32 import org.opencastproject.editor.api.EditingData;
33 import org.opencastproject.editor.api.EditorService;
34 import org.opencastproject.editor.api.EditorServiceException;
35 import org.opencastproject.editor.api.ErrorStatus;
36 import org.opencastproject.editor.api.LockData;
37 import org.opencastproject.editor.api.SegmentData;
38 import org.opencastproject.editor.api.TrackData;
39 import org.opencastproject.editor.api.TrackSubData;
40 import org.opencastproject.editor.api.WorkflowData;
41 import org.opencastproject.elasticsearch.api.SearchIndexException;
42 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
43 import org.opencastproject.elasticsearch.index.objects.event.Event;
44 import org.opencastproject.index.service.api.IndexService;
45 import org.opencastproject.index.service.exception.IndexServiceException;
46 import org.opencastproject.index.service.impl.util.EventUtils;
47 import org.opencastproject.mediapackage.Attachment;
48 import org.opencastproject.mediapackage.Catalog;
49 import org.opencastproject.mediapackage.MediaPackage;
50 import org.opencastproject.mediapackage.MediaPackageElement;
51 import org.opencastproject.mediapackage.MediaPackageElementBuilder;
52 import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
53 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
54 import org.opencastproject.mediapackage.Publication;
55 import org.opencastproject.mediapackage.Track;
56 import org.opencastproject.mediapackage.attachment.AttachmentImpl;
57 import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
58 import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
59 import org.opencastproject.metadata.dublincore.MetadataJson;
60 import org.opencastproject.metadata.dublincore.MetadataList;
61 import org.opencastproject.security.api.AuthorizationService;
62 import org.opencastproject.security.api.Organization;
63 import org.opencastproject.security.api.SecurityConstants;
64 import org.opencastproject.security.api.SecurityService;
65 import org.opencastproject.security.api.UnauthorizedException;
66 import org.opencastproject.security.api.User;
67 import org.opencastproject.security.urlsigning.exception.UrlSigningException;
68 import org.opencastproject.security.urlsigning.service.UrlSigningService;
69 import org.opencastproject.security.urlsigning.utils.UrlSigningServiceOsgiUtil;
70 import org.opencastproject.smil.api.SmilException;
71 import org.opencastproject.smil.api.SmilResponse;
72 import org.opencastproject.smil.api.SmilService;
73 import org.opencastproject.smil.entity.api.Smil;
74 import org.opencastproject.smil.entity.media.api.SmilMediaObject;
75 import org.opencastproject.smil.entity.media.container.api.SmilMediaContainer;
76 import org.opencastproject.smil.entity.media.element.api.SmilMediaElement;
77 import org.opencastproject.util.MimeType;
78 import org.opencastproject.util.MimeTypes;
79 import org.opencastproject.util.NotFoundException;
80 import org.opencastproject.util.data.Tuple;
81 import org.opencastproject.workflow.api.ConfiguredWorkflow;
82 import org.opencastproject.workflow.api.WorkflowDatabaseException;
83 import org.opencastproject.workflow.api.WorkflowDefinition;
84 import org.opencastproject.workflow.api.WorkflowInstance;
85 import org.opencastproject.workflow.api.WorkflowService;
86 import org.opencastproject.workflow.api.WorkflowUtil;
87 import org.opencastproject.workflow.handler.distribution.InternalPublicationChannel;
88 import org.opencastproject.workspace.api.Workspace;
89
90 import org.apache.commons.io.FileUtils;
91 import org.apache.commons.io.IOUtils;
92 import org.apache.commons.lang3.BooleanUtils;
93 import org.apache.commons.lang3.StringUtils;
94 import org.osgi.service.component.ComponentContext;
95 import org.osgi.service.component.annotations.Activate;
96 import org.osgi.service.component.annotations.Component;
97 import org.osgi.service.component.annotations.Modified;
98 import org.osgi.service.component.annotations.Reference;
99 import org.slf4j.Logger;
100 import org.slf4j.LoggerFactory;
101 import org.xml.sax.SAXException;
102
103 import java.awt.datatransfer.MimeTypeParseException;
104 import java.io.ByteArrayInputStream;
105 import java.io.File;
106 import java.io.IOException;
107 import java.io.InputStream;
108 import java.net.URI;
109 import java.net.URISyntaxException;
110 import java.nio.charset.StandardCharsets;
111 import java.text.MessageFormat;
112 import java.util.ArrayList;
113 import java.util.Arrays;
114 import java.util.Base64;
115 import java.util.Collection;
116 import java.util.Collections;
117 import java.util.Comparator;
118 import java.util.Dictionary;
119 import java.util.HashMap;
120 import java.util.HashSet;
121 import java.util.Iterator;
122 import java.util.List;
123 import java.util.Map;
124 import java.util.Objects;
125 import java.util.Optional;
126 import java.util.Set;
127 import java.util.UUID;
128 import java.util.stream.Collectors;
129
130 import javax.ws.rs.WebApplicationException;
131 import javax.xml.bind.JAXBException;
132
133
134 @Component(
135 property = {
136 "service.description=Editor Service"
137 },
138 immediate = true,
139 service = EditorService.class
140 )
141 public class EditorServiceImpl implements EditorService {
142
143
144 private static final Logger logger = LoggerFactory.getLogger(EditorServiceImpl.class);
145
146
147 private static final String EDITOR_WORKFLOW_TAG = "editor";
148
149 private static EditorLock editorLock;
150
151 private long expireSeconds = UrlSigningServiceOsgiUtil.DEFAULT_URL_SIGNING_EXPIRE_DURATION;
152
153 private Boolean signWithClientIP = UrlSigningServiceOsgiUtil.DEFAULT_SIGN_WITH_CLIENT_IP;
154
155
156 private IndexService index;
157 private AssetManager assetManager;
158 private SecurityService securityService;
159 private SmilService smilService;
160 private UrlSigningService urlSigningService;
161 private WorkflowService workflowService;
162 private Workspace workspace;
163 private AuthorizationService authorizationService;
164
165
166 private MediaPackageElementFlavor smilCatalogFlavor;
167 private String previewVideoSubtype;
168 private String previewTag;
169 private String previewSubtype;
170 private String waveformSubtype;
171 private String thumbnailSubType;
172 private MediaPackageElementFlavor smilSilenceFlavor;
173 private ElasticsearchIndex searchIndex;
174 private MediaPackageElementFlavor captionsFlavor;
175 private String thumbnailWfProperty;
176 private List<MediaPackageElementFlavor> thumbnailSourcePrimary;
177 private String distributionDirectory;
178 private Boolean localPublication = null;
179
180 private static final String DEFAULT_PREVIEW_SUBTYPE = "source";
181 private static final String DEFAULT_PREVIEW_TAG = "editor";
182 private static final String DEFAULT_WAVEFORM_SUBTYPE = "waveform";
183 private static final String DEFAULT_SMIL_CATALOG_FLAVOR = "smil/cutting";
184 private static final String DEFAULT_SMIL_CATALOG_TAGS = "archive";
185 private static final String DEFAULT_SMIL_SILENCE_FLAVOR = "*/silence";
186 private static final String DEFAULT_PREVIEW_VIDEO_SUBTYPE = "video+preview";
187 private static final String DEFAULT_CAPTIONS_FLAVOR = "captions/*";
188 private static final String DEFAULT_THUMBNAIL_SUBTYPE = "player+preview";
189 private static final String DEFAULT_THUMBNAIL_WF_PROPERTY = "thumbnail_edited";
190 private static final List<MediaPackageElementFlavor> DEFAULT_THUMBNAIL_PRIORITY_FLAVOR = new ArrayList<>();
191 private static final int DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
192 private static final int DEFAULT_LOCK_REFRESH_SECONDS = 60;
193
194 public static final String OPT_PREVIEW_SUBTYPE = "preview.subtype";
195 public static final String OPT_PREVIEW_TAG = "preview.tag";
196 public static final String OPT_WAVEFORM_SUBTYPE = "waveform.subtype";
197 public static final String OPT_SMIL_CATALOG_FLAVOR = "smil.catalog.flavor";
198 public static final String OPT_SMIL_CATALOG_TAGS = "smil.catalog.tags";
199 public static final String OPT_SMIL_SILENCE_FLAVOR = "smil.silence.flavor";
200 public static final String OPT_PREVIEW_VIDEO_SUBTYPE = "preview.video.subtype";
201 public static final String OPT_CAPTIONS_FLAVOR = "captions.flavor";
202 public static final String OPT_THUMBNAILSUBTYPE = "thumbnail.subtype";
203 public static final String OPT_THUMBNAIL_WF_PROPERTY = "thumbnail.workflow.property";
204 public static final String OPT_THUMBNAIL_PRIORITY_FLAVOR = "thumbnail.priority.flavor";
205 public static final String OPT_LOCAL_PUBLICATION = "publication.local";
206 public static final String OPT_LOCK_ENABLED = "lock.enable";
207 public static final String OPT_LOCK_TIMEOUT = "lock.release.after.seconds";
208 public static final String OPT_LOCK_REFRESH = "lock.refresh.after.seconds";
209
210 private Boolean lockingActive;
211 private int lockRefresh = DEFAULT_LOCK_REFRESH_SECONDS;
212 private int lockTimeout = DEFAULT_LOCK_TIMEOUT_SECONDS;
213
214 private final Set<String> smilCatalogTagSet = new HashSet<>();
215
216 @Reference
217 void setSecurityService(SecurityService securityService) {
218 this.securityService = securityService;
219 }
220
221 @Reference
222 void setSmilService(SmilService smilService) {
223 this.smilService = smilService;
224 }
225
226 @Reference
227 void setWorkflowService(WorkflowService workflowService) {
228 this.workflowService = workflowService;
229 }
230
231 @Reference
232 void setWorkspace(Workspace workspace) {
233 this.workspace = workspace;
234 }
235
236 @Reference
237 void setUrlSigningService(UrlSigningService urlSigningService) {
238 this.urlSigningService = urlSigningService;
239 }
240
241 @Reference
242 void setAssetManager(AssetManager assetManager) {
243 this.assetManager = assetManager;
244 }
245
246 @Reference
247 public void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
248 this.searchIndex = elasticsearchIndex;
249 }
250
251 @Reference
252 public void setIndexService(IndexService index) {
253 this.index = index;
254 }
255
256 @Reference
257 public void setAuthorizationService(AuthorizationService authorizationService) {
258 this.authorizationService = authorizationService;
259 }
260
261 public MediaPackageElementFlavor getSmilCatalogFlavor() {
262 return smilCatalogFlavor;
263 }
264
265 public Set<String> getSmilCatalogTags() {
266 return smilCatalogTagSet;
267 }
268
269 public String getPreviewVideoSubtype() {
270 return previewVideoSubtype;
271 }
272
273 public MediaPackageElementFlavor getSmilSilenceFlavor() {
274 return smilSilenceFlavor;
275 }
276
277 private String getPreviewSubtype() {
278 return previewSubtype;
279 }
280
281 public String getPreviewTag() {
282 return previewTag;
283 }
284
285 private String getWaveformSubtype() {
286 return waveformSubtype;
287 }
288
289 private String getThumbnailSubtype() {
290 return thumbnailSubType;
291 }
292
293 @Activate
294 @Modified
295 public void activate(ComponentContext cc) {
296 Dictionary<String, Object> properties = cc.getProperties();
297 if (properties == null) {
298 return;
299 }
300
301 expireSeconds = UrlSigningServiceOsgiUtil.getUpdatedSigningExpiration(properties, this.getClass().getSimpleName());
302 signWithClientIP = UrlSigningServiceOsgiUtil.getUpdatedSignWithClientIP(properties,this.getClass().getSimpleName());
303
304 previewTag = Objects.toString(properties.get(OPT_PREVIEW_TAG), DEFAULT_PREVIEW_TAG);
305 logger.debug("Preview tag configuration set to '{}'", previewTag);
306
307
308 previewSubtype = Objects.toString(properties.get(OPT_PREVIEW_SUBTYPE), DEFAULT_PREVIEW_SUBTYPE);
309 logger.debug("Preview subtype configuration set to '{}'", previewSubtype);
310
311
312 waveformSubtype = Objects.toString(properties.get(OPT_WAVEFORM_SUBTYPE), DEFAULT_WAVEFORM_SUBTYPE);
313 logger.debug("Waveform subtype configuration set to '{}'", waveformSubtype);
314
315
316 smilCatalogFlavor = MediaPackageElementFlavor.parseFlavor(
317 StringUtils.defaultString((String) properties.get(OPT_SMIL_CATALOG_FLAVOR), DEFAULT_SMIL_CATALOG_FLAVOR));
318 logger.debug("Smil catalog flavor configuration set to '{}'", smilCatalogFlavor);
319
320
321 String tags = Objects.toString(properties.get(OPT_SMIL_CATALOG_TAGS), DEFAULT_SMIL_CATALOG_TAGS);
322 String[] smilCatalogTags = StringUtils.split(tags, ",");
323 smilCatalogTagSet.clear();
324 if (smilCatalogTags != null) {
325 smilCatalogTagSet.addAll(Arrays.asList(smilCatalogTags));
326 }
327
328
329 smilSilenceFlavor = MediaPackageElementFlavor.parseFlavor(
330 StringUtils.defaultString((String) properties.get(OPT_SMIL_SILENCE_FLAVOR), DEFAULT_SMIL_SILENCE_FLAVOR));
331 logger.debug("Smil silence flavor configuration set to '{}'", smilSilenceFlavor);
332
333
334 previewVideoSubtype = Objects.toString(properties.get(OPT_PREVIEW_VIDEO_SUBTYPE), DEFAULT_PREVIEW_VIDEO_SUBTYPE);
335
336 logger.debug("Preview video subtype set to '{}'", previewVideoSubtype);
337
338
339 captionsFlavor = MediaPackageElementFlavor.parseFlavor(
340 StringUtils.defaultString((String) properties.get(OPT_CAPTIONS_FLAVOR), DEFAULT_CAPTIONS_FLAVOR));
341 logger.debug("Caption flavor set to '{}'", captionsFlavor);
342
343 thumbnailSubType = Objects.toString(properties.get(OPT_THUMBNAILSUBTYPE), DEFAULT_THUMBNAIL_SUBTYPE);
344 logger.debug("Thumbnail subtype set to '{}'", thumbnailSubType);
345
346 thumbnailWfProperty = Objects.toString(properties.get(OPT_THUMBNAIL_WF_PROPERTY), DEFAULT_THUMBNAIL_WF_PROPERTY);
347 logger.debug("Thumbnail workflow property set to '{}'", thumbnailWfProperty);
348
349 String thumbnailPriorities = Objects.toString(properties.get(OPT_THUMBNAIL_PRIORITY_FLAVOR));
350 if ("null".equals(thumbnailPriorities) || thumbnailPriorities.isEmpty()) {
351 thumbnailSourcePrimary = DEFAULT_THUMBNAIL_PRIORITY_FLAVOR;
352 } else {
353 thumbnailSourcePrimary = Arrays.stream(thumbnailPriorities.split(",", -1))
354 .map(MediaPackageElementFlavor::parseFlavor)
355 .collect(Collectors.toList());
356 }
357
358 String localPublicationConfig = Objects.toString(properties.get(OPT_LOCAL_PUBLICATION), "auto");
359 if (!"auto".equals(localPublicationConfig)) {
360
361 localPublication = BooleanUtils.toBoolean(localPublicationConfig);
362 }
363
364 distributionDirectory = cc.getBundleContext().getProperty("org.opencastproject.download.directory");
365 if (StringUtils.isEmpty(distributionDirectory)) {
366 final String storageDir = cc.getBundleContext().getProperty("org.opencastproject.storage.dir");
367 if (StringUtils.isNotEmpty(storageDir)) {
368 distributionDirectory = new File(storageDir, "downloads").getPath();
369 }
370 }
371 logger.debug("Thumbnail track priority set to '{}'", thumbnailSourcePrimary);
372
373 lockingActive = Boolean.parseBoolean(StringUtils.trimToEmpty((String) properties.get(OPT_LOCK_ENABLED)));
374
375 try {
376 lockTimeout = Integer.parseUnsignedInt(
377 Objects.toString(properties.get(OPT_LOCK_TIMEOUT)));
378 } catch (NumberFormatException e) {
379 logger.info("Configuration {} contains invalid value, defaulting to {}", OPT_LOCK_TIMEOUT, lockTimeout);
380 }
381
382 try {
383 lockRefresh = Integer.parseUnsignedInt(
384 Objects.toString(properties.get(OPT_LOCK_REFRESH)));
385 } catch (NumberFormatException e) {
386 logger.info("Configuration {} contains invalid value, defaulting to {}", OPT_LOCK_REFRESH, lockRefresh);
387 }
388
389 editorLock = new EditorLock(lockTimeout);
390
391 }
392
393
394
395
396
397
398
399
400
401 private boolean isLocal(URI uri) {
402 var path = uri.normalize().getPath();
403 if (!path.startsWith("/static/")) {
404 return false;
405 }
406 var localFile = new File(distributionDirectory, path.substring("/static".length()));
407 return localFile.exists();
408 }
409
410 private Boolean elementHasPreviewTag(MediaPackageElement element) {
411 return element.getTags() != null
412 && Arrays.asList(element.getTags()).contains(getPreviewTag());
413 }
414
415 private Boolean elementHasPreviewFlavor(MediaPackageElement element) {
416 return element.getFlavor() != null
417 && getPreviewSubtype().equals(element.getFlavor().getSubtype());
418 }
419
420 private Boolean elementHasWaveformFlavor(MediaPackageElement element) {
421 return element.getFlavor() != null
422 && getWaveformSubtype().equals(element.getFlavor().getSubtype());
423 }
424
425 private String signIfNecessary(final URI uri) {
426 if (!urlSigningService.accepts(uri.toString())) {
427 return uri.toString();
428 }
429 String clientIP = signWithClientIP ? securityService.getUserIP() : null;
430 try {
431 return new URI(urlSigningService.sign(uri.toString(), expireSeconds, null, clientIP)).toString();
432 } catch (URISyntaxException | UrlSigningException e) {
433 throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
434 }
435 }
436
437
438
439
440
441
442
443
444
445
446
447
448 Smil createSmilCuttingCatalog(final EditingData editingInfo, final MediaPackage mediaPackage) throws SmilException {
449
450 SmilResponse smilResponse = smilService.createNewSmil(mediaPackage);
451
452
453 ArrayList<Track> tracks = new ArrayList<>();
454
455 for (final TrackData trackdata : editingInfo.getTracks()) {
456 String trackId = trackdata.getId();
457 Track track = mediaPackage.getTrack(trackId);
458 if (track == null) {
459 track = Arrays.stream(getInternalPublication(mediaPackage)
460 .orElseThrow(() -> new IllegalStateException("Event has no internal publication"))
461 .getTracks())
462 .filter(t -> trackId.equals(t.getIdentifier()))
463 .findFirst()
464 .orElseThrow(() -> new IllegalStateException(
465 String.format("The track '%s' doesn't exist in media package '%s'", trackId, mediaPackage)));
466 }
467 tracks.add(track);
468 }
469
470 for (SegmentData segment : editingInfo.getSegments()) {
471 smilResponse = smilService.addParallel(smilResponse.getSmil());
472 final String parentId = smilResponse.getEntity().getId();
473
474 final long duration = segment.getEnd() - segment.getStart();
475 if (!segment.isDeleted()) {
476 smilResponse = smilService.addClips(smilResponse.getSmil(), parentId, tracks.toArray(new Track[0]),
477 segment.getStart(), duration);
478 }
479 }
480
481 return smilResponse.getSmil();
482 }
483
484
485
486
487
488
489
490
491
492
493
494
495 MediaPackage addSmilToArchive(MediaPackage mediaPackage, final Smil smil) throws IOException {
496 MediaPackageElementFlavor mediaPackageElementFlavor = getSmilCatalogFlavor();
497
498 String catalogId = smil.getId();
499 Catalog[] catalogs = mediaPackage.getCatalogs();
500
501
502 for (Catalog p: catalogs) {
503 if (p.getFlavor().matches(mediaPackageElementFlavor)) {
504 logger.debug("Set Identifier for Smil-Catalog to: {}", p.getIdentifier());
505 catalogId = p.getIdentifier();
506 break;
507 }
508 }
509 Catalog catalog = mediaPackage.getCatalog(catalogId);
510
511 URI smilURI;
512 try (InputStream is = IOUtils.toInputStream(smil.toXML(), "UTF-8")) {
513 smilURI = workspace.put(mediaPackage.getIdentifier().toString(), catalogId, EditorService.TARGET_FILE_NAME, is);
514 } catch (SAXException | JAXBException e) {
515 throw new IOException("Error while serializing the SMIL catalog to XML" ,e);
516 }
517
518 if (catalog == null) {
519 MediaPackageElementBuilder mpeBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
520 catalog = (Catalog) mpeBuilder.elementFromURI(smilURI, MediaPackageElement.Type.Catalog, getSmilCatalogFlavor());
521 mediaPackage.add(catalog);
522 }
523 catalog.setURI(smilURI);
524 catalog.setIdentifier(catalogId);
525 catalog.setMimeType(MimeTypes.XML);
526 for (String tag : getSmilCatalogTags()) {
527 catalog.addTag(tag);
528 }
529
530 catalog.setChecksum(null);
531 return mediaPackage;
532 }
533
534
535
536
537
538
539
540
541
542
543
544 private MediaPackage processSubtitleTrack(MediaPackage mediaPackage, List<EditingData.Subtitle> subtitles)
545 throws IOException, IllegalArgumentException {
546 for (EditingData.Subtitle subtitle : subtitles) {
547
548 String subtitleId = UUID.randomUUID().toString();
549 String trackId = null;
550
551
552 for (Track t : mediaPackage.getTracks()) {
553 if (t.getIdentifier().matches(subtitle.getId())) {
554 logger.debug("Set Identifier for Subtitle-Track to: {}", t.getIdentifier());
555 subtitleId = t.getIdentifier();
556 trackId = t.getIdentifier();
557 break;
558 }
559 }
560
561 Track track = mediaPackage.getTrack(trackId);
562
563 if (subtitle.isDeleted()) {
564
565 if (trackId != null) {
566 mediaPackage.remove(track);
567 }
568 continue;
569 }
570
571
572 URI oldTrackURI = null;
573 if (track != null) {
574 oldTrackURI = track.getURI();
575 }
576
577
578 try (InputStream is = IOUtils.toInputStream(subtitle.getSubtitle(), "UTF-8")) {
579 URI subtitleUri = workspace.put(mediaPackage.getIdentifier().toString(), subtitleId, "subtitle.vtt", is);
580
581
582 if (track == null) {
583
584 track = (Track) mediaPackage.add(subtitleUri, MediaPackageElement.Type.Track,
585 new MediaPackageElementFlavor(captionsFlavor.getType(),"source"));
586 logger.info("Creating new subtitle track {}", track.getIdentifier());
587 }
588
589 track.setURI(subtitleUri);
590 track.setIdentifier(subtitleId);
591 track.setChecksum(null);
592 for (String tag : subtitle.getTags()) {
593 track.addTag(tag);
594 }
595
596 if (oldTrackURI != null && oldTrackURI != subtitleUri) {
597
598 logger.info("Removing old track file {}", oldTrackURI);
599 try {
600 workspace.delete(oldTrackURI);
601 } catch (NotFoundException | IOException e) {
602 logger.info("Could not remove track from workspace. Could be it was never there.");
603 }
604 }
605 }
606 }
607
608 return mediaPackage;
609 }
610
611
612
613
614
615
616
617
618
619
620
621 private MediaPackage addThumbnailsToArchive(EditingData editingData, MediaPackage mediaPackage)
622 throws MimeTypeParseException, IOException {
623 for (TrackData track : editingData.getTracks()) {
624 String id = track.getId();
625 MediaPackageElementFlavor flavor = new MediaPackageElementFlavor(track.getFlavor().getType(),
626 getThumbnailSubtype());
627 String uri = track.getThumbnailURI();
628
629
630 if (uri == null || uri.isEmpty()) {
631 continue;
632 }
633
634 if (!uri.startsWith("data")) {
635 continue;
636 }
637
638
639 uri = uri.substring(uri.indexOf(",") + 1);
640 byte[] byteArray = Base64.getMimeDecoder().decode(uri);
641 InputStream inputStream = new ByteArrayInputStream(byteArray);
642
643
644 String stringMimeType = detectMimeType(uri);
645 MimeType mimeType = MimeType.mimeType(stringMimeType.split("/")[0], stringMimeType.split("/")[1]);
646
647
648 final String filename = "thumbnail_" + id + "." + mimeType.getSubtype();
649 final String originalThumbnailId = UUID.randomUUID().toString();
650 URI tempThumbnail = null;
651 try {
652 tempThumbnail = workspace
653 .put(mediaPackage.getIdentifier().toString(), originalThumbnailId, filename, inputStream);
654 } catch (IOException e) {
655 throw new IOException("Could not add thumbnail to workspace", e);
656 }
657
658
659 final Attachment attachment = AttachmentImpl.fromURI(tempThumbnail);
660 attachment.setFlavor(flavor);
661 attachment.setMimeType(mimeType);
662 Arrays.stream(mediaPackage.getElementsByFlavor(flavor))
663 .map(MediaPackageElement::getTags)
664 .flatMap(Arrays::stream)
665 .distinct()
666 .forEach(attachment::addTag);
667
668
669 Arrays.stream(mediaPackage.getElementsByFlavor(flavor)).forEach(mediaPackage::remove);
670
671
672 mediaPackage.add(attachment);
673
674
675
676
677 WorkflowPropertiesUtil
678 .storeProperty(assetManager, mediaPackage,
679 flavor.getType() + "/" + thumbnailWfProperty, "true");
680 }
681
682 return mediaPackage;
683 }
684
685
686
687
688
689
690
691
692
693
694 private String detectMimeType(String b64) throws MimeTypeParseException {
695 var signatures = new HashMap<String, String>();
696 signatures.put("R0lGODdh", "image/gif");
697 signatures.put("iVBORw0KGgo", "image/png");
698 signatures.put("/9j/", "image/jpg");
699
700 for (var s : signatures.entrySet()) {
701 if (b64.indexOf(s.getKey()) == 0) {
702 return s.getValue();
703 }
704 }
705 throw new MimeTypeParseException("No image mimetype found");
706 }
707
708 private Optional<Publication> getInternalPublication(MediaPackage mp) {
709 return Arrays.stream(mp.getPublications())
710 .filter(publication -> InternalPublicationChannel.CHANNEL_ID.equals(publication.getChannel()))
711 .findFirst();
712 }
713
714
715
716
717
718
719
720
721 private Event getEvent(final String mediaPackageId) throws EditorServiceException {
722 try {
723 Optional<Event> optEvent = index.getEvent(mediaPackageId, searchIndex);
724 if (optEvent.isEmpty()) {
725 errorExit("Event not found", mediaPackageId,
726 ErrorStatus.MEDIAPACKAGE_NOT_FOUND);
727 } else {
728 return optEvent.get();
729 }
730 } catch (SearchIndexException e) {
731 errorExit("Error while reading event from search index:", mediaPackageId,
732 ErrorStatus.MEDIAPACKAGE_NOT_FOUND, e);
733 }
734 return null;
735 }
736
737
738
739
740
741
742
743 private List<WorkflowDefinition> getEditingWorkflows() {
744 try {
745 return workflowService.listAvailableWorkflowDefinitions().stream()
746 .filter(workflow -> workflow.containsTag(EDITOR_WORKFLOW_TAG))
747 .collect(Collectors.toList());
748 } catch (WorkflowDatabaseException e) {
749 logger.warn("Error while retrieving list of workflow definitions:", e);
750 }
751 return emptyList();
752 }
753
754
755
756
757
758
759
760
761 private List<SegmentData> getSegments(final MediaPackage mediaPackage) {
762 List<SegmentData> segments = new ArrayList<>();
763 for (Catalog smilCatalog : mediaPackage.getCatalogs(getSmilCatalogFlavor())) {
764 try {
765 Smil smil = smilService.fromXml(workspace.get(smilCatalog.getURI())).getSmil();
766 segments = mergeSegments(segments, getSegmentsFromSmil(smil));
767
768 } catch (NotFoundException e) {
769 logger.warn("File '{}' could not be loaded by workspace service:", smilCatalog.getURI(), e);
770 } catch (IOException e) {
771 logger.warn("Reading file '{}' from workspace service failed:", smilCatalog.getURI(), e);
772 } catch (SmilException e) {
773 logger.warn("Error while parsing SMIL catalog '{}':", smilCatalog.getURI(), e);
774 }
775 }
776
777 if (!segments.isEmpty()) {
778 return segments;
779 }
780
781
782 for (Catalog smilCatalog : mediaPackage.getCatalogs(getSmilSilenceFlavor())) {
783 try {
784 Smil smil = smilService.fromXml(workspace.get(smilCatalog.getURI())).getSmil();
785 segments = getSegmentsFromSmil(smil);
786 } catch (NotFoundException e) {
787 logger.warn("File '{}' could not be loaded by workspace service:", smilCatalog.getURI(), e);
788 } catch (IOException e) {
789 logger.warn("Reading file '{}' from workspace service failed:", smilCatalog.getURI(), e);
790 } catch (SmilException e) {
791 logger.warn("Error while parsing SMIL catalog '{}':", smilCatalog.getURI(), e);
792 }
793 }
794
795
796 if (segments.size() == 1) {
797 SegmentData singleSegment = segments.get(0);
798 if (singleSegment.getStart() == 0 && singleSegment.getEnd() >= mediaPackage.getDuration()) {
799 segments.remove(0);
800 }
801 }
802
803 return segments;
804 }
805
806 protected List<SegmentData> getDeletedSegments(MediaPackage mediaPackage, List<SegmentData> segments) {
807
808 long lastTime = 0;
809 List<SegmentData> deletedElements = new ArrayList<>();
810 for (int i = 0; i < segments.size(); i++) {
811 SegmentData segmentData = segments.get(i);
812 if (segmentData.getStart() != lastTime) {
813 SegmentData deleted = new SegmentData(lastTime, segmentData.getStart(), true);
814 deletedElements.add(deleted);
815 }
816 lastTime = segmentData.getEnd();
817
818 if (segments.size() - 1 == i) {
819 if (mediaPackage.getDuration() != null && lastTime < mediaPackage.getDuration()) {
820 deletedElements.add(new SegmentData(lastTime, mediaPackage.getDuration(), true));
821 }
822 }
823 }
824 return deletedElements;
825 }
826
827 protected List<SegmentData> mergeSegments(List<SegmentData> segments, List<SegmentData> segments2) {
828
829 List<SegmentData> mergedSegments = mergeInternal(segments, segments2);
830
831
832 sortSegments(mergedSegments);
833
834 return mergedSegments;
835 }
836
837 private void sortSegments(List<SegmentData> mergedSegments) {
838 mergedSegments.sort(Comparator.comparing(SegmentData::getStart));
839 }
840
841
842
843
844
845
846
847
848
849
850
851 private List<SegmentData> mergeInternal(List<SegmentData> segments, List<SegmentData> segments2) {
852 for (Iterator<SegmentData> it = segments.iterator(); it.hasNext();) {
853 SegmentData seg = it.next();
854 for (Iterator<SegmentData> it2 = segments2.iterator(); it2.hasNext();) {
855 SegmentData seg2 = it2.next();
856 long combinedStart = Math.max(seg.getStart(), seg2.getStart());
857 long combinedEnd = Math.min(seg.getEnd(), seg2.getEnd());
858 if (combinedEnd > combinedStart) {
859 it.remove();
860 it2.remove();
861 List<SegmentData> newSegments = new ArrayList<>(segments);
862 newSegments.add(new SegmentData(combinedStart, combinedEnd));
863 return mergeInternal(newSegments, segments2);
864 }
865 }
866 }
867 segments.addAll(segments2);
868 return segments;
869 }
870
871
872
873
874
875
876
877
878 List<SegmentData> getSegmentsFromSmil(Smil smil) {
879 List<SegmentData> segments = new ArrayList<>();
880 for (SmilMediaObject elem : smil.getBody().getMediaElements()) {
881 if (elem instanceof SmilMediaContainer) {
882 SmilMediaContainer mediaContainer = (SmilMediaContainer) elem;
883
884 SegmentData tuple = null;
885 for (SmilMediaObject video : mediaContainer.getElements()) {
886 if (video instanceof SmilMediaElement) {
887 SmilMediaElement videoElem = (SmilMediaElement) video;
888 try {
889
890 if (tuple == null || (videoElem.getClipEndMS()
891 - videoElem.getClipBeginMS()) > tuple.getEnd() - tuple.getStart()) {
892 tuple = new SegmentData(videoElem.getClipBeginMS(), videoElem.getClipEndMS());
893 }
894 } catch (SmilException e) {
895 logger.warn("Media element '{}' of SMIL catalog '{}' seems to be invalid",
896 videoElem, smil, e);
897 }
898 }
899 }
900 if (tuple != null) {
901 segments.add(tuple);
902 }
903 }
904 }
905 return segments;
906 }
907
908 @Override
909 public void lockMediaPackage(final String mediaPackageId, LockData lockRequest) throws EditorServiceException {
910
911 getEvent(mediaPackageId);
912
913
914 editorLock.lock(mediaPackageId, lockRequest);
915 }
916
917 @Override
918 public void unlockMediaPackage(final String mediaPackageId, LockData lockRequest) throws EditorServiceException {
919
920 getEvent(mediaPackageId);
921
922
923 editorLock.unlock(mediaPackageId, lockRequest);
924 }
925
926 @Override
927 public EditingData getEditData(final String mediaPackageId) throws EditorServiceException, UnauthorizedException {
928
929 Event event = getEvent(mediaPackageId);
930 MediaPackage mp = getMediaPackage(event);
931
932 if (!isAdmin() && !authorizationService.hasPermission(mp, "write")) {
933 throw new UnauthorizedException("User has no write access to this event");
934 }
935
936 boolean workflowActive = WorkflowUtil.isActive(event.getWorkflowState());
937
938 final Optional<Publication> internalPubOpt = getInternalPublication(mp);
939 if (internalPubOpt.isEmpty()) {
940 errorExit("No internal publication", mediaPackageId, ErrorStatus.NO_INTERNAL_PUBLICATION);
941 }
942 Publication internalPub = internalPubOpt.get();
943
944
945 List<SegmentData> segments = getSegments(mp);
946 segments.addAll(getDeletedSegments(mp, segments));
947 sortSegments(segments);
948
949
950
951 List<WorkflowData> workflows = new ArrayList<>();
952 for (WorkflowDefinition workflow : getEditingWorkflows()) {
953 workflows.add(new WorkflowData(workflow.getId(), workflow.getTitle(), workflow.getDisplayOrder(),
954 workflow.getDescription()));
955 }
956
957 final Map<String, String> latestWfProperties = WorkflowPropertiesUtil
958 .getLatestWorkflowProperties(assetManager, mediaPackageId);
959
960
961 final Collection<Tuple<String, String>> hiddens = latestWfProperties.entrySet()
962 .stream()
963 .map(property -> tuple(property.getKey().split("_"), property.getValue()))
964 .filter(property -> property.getA().length == 3)
965 .filter(property -> property.getA()[0].equals("hide"))
966 .filter(property -> property.getB().equals("true"))
967 .map(property -> tuple(property.getA()[1], property.getA()[2]))
968 .collect(Collectors.toSet());
969
970 List<Track> trackList = Arrays.stream(internalPub.getTracks()).filter(this::elementHasPreviewTag)
971 .collect(Collectors.toList());
972 if (trackList.isEmpty()) {
973 trackList = Arrays.stream(internalPub.getTracks()).filter(this::elementHasPreviewFlavor)
974 .collect(Collectors.toList());
975 if (trackList.isEmpty()) {
976 trackList = Arrays.asList(internalPub.getTracks());
977 }
978 }
979
980
981 Track[] subtitleTracks = mp.getTracks(captionsFlavor);
982 List<EditingData.Subtitle> subtitles = new ArrayList<>();
983 for (Track t: subtitleTracks) {
984 try {
985 File subtitleFile = workspace.get(t.getURI());
986 String subtitleString = FileUtils.readFileToString(subtitleFile, StandardCharsets.UTF_8);
987 subtitles.add(new EditingData.Subtitle(t.getIdentifier(), subtitleString, t.getTags()));
988 } catch (NotFoundException | IOException e) {
989 errorExit("Could not read subtitle from file", mediaPackageId, ErrorStatus.UNKNOWN);
990 }
991 }
992
993
994
995 final List<TrackData> tracks = trackList.stream().map(track -> {
996 final String uri = signIfNecessary(track.getURI());
997 final boolean audioEnabled = !hiddens.contains(tuple(track.getFlavor().getType(), "audio"));
998 final TrackSubData audio = new TrackSubData(track.hasAudio(), null,
999 audioEnabled);
1000 final boolean videoEnable = !hiddens.contains(tuple(track.getFlavor().getType(), "video"));
1001 final String videoPreview = Arrays.stream(internalPub.getAttachments())
1002 .filter(attachment -> attachment.getFlavor().getType().equals(track.getFlavor().getType()))
1003 .filter(attachment -> attachment.getFlavor().getSubtype().equals(getPreviewVideoSubtype()))
1004 .map(MediaPackageElement::getURI).map(this::signIfNecessary)
1005 .findAny()
1006 .orElse(null);
1007 final TrackSubData video = new TrackSubData(track.hasVideo(), videoPreview,
1008 videoEnable);
1009
1010
1011
1012
1013 String thumbnailURI = Arrays.stream(mp.getAttachments())
1014 .filter(attachment -> attachment.getFlavor().getType().equals(track.getFlavor().getType()))
1015 .filter(attachment -> attachment.getFlavor().getSubtype().equals(getThumbnailSubtype()))
1016 .map(MediaPackageElement::getURI).map(this::signIfNecessary)
1017 .findAny()
1018 .orElse(null);
1019
1020
1021
1022 if (thumbnailURI == null) {
1023 thumbnailURI = Arrays.stream(internalPub.getAttachments())
1024 .filter(attachment -> attachment.getFlavor().getType().equals(track.getFlavor().getType()))
1025 .filter(attachment -> attachment.getFlavor().getSubtype().equals(getThumbnailSubtype()))
1026 .map(MediaPackageElement::getURI).map(this::signIfNecessary)
1027 .findAny()
1028 .orElse(null);
1029 }
1030
1031 final int priority = thumbnailSourcePrimary.indexOf(track.getFlavor());
1032
1033 if (localPublication == null) {
1034 localPublication = isLocal(track.getURI());
1035 }
1036
1037 return new TrackData(track.getFlavor().getType(), track.getFlavor().getSubtype(), audio, video, uri,
1038 track.getIdentifier(), thumbnailURI, priority);
1039 }).collect(Collectors.toList());
1040
1041 List<String> waveformList = Arrays.stream(internalPub.getAttachments())
1042 .filter(this::elementHasWaveformFlavor)
1043 .map(Attachment::getURI).map(this::signIfNecessary)
1044 .collect(Collectors.toList());
1045
1046 User user = securityService.getUser();
1047
1048 return new EditingData(segments, tracks, workflows, mp.getDuration(), mp.getTitle(), event.getRecordingStartDate(),
1049 event.getSeriesId(), event.getSeriesName(), workflowActive, waveformList, subtitles, localPublication,
1050 lockingActive, lockRefresh, user, "");
1051 }
1052
1053
1054 private boolean isAdmin() {
1055 final User currentUser = securityService.getUser();
1056
1057
1058 if (currentUser.hasRole(SecurityConstants.GLOBAL_ADMIN_ROLE)) {
1059 return true;
1060 }
1061
1062
1063 final Organization currentOrg = securityService.getOrganization();
1064 return currentUser.getOrganization().getId().equals(currentOrg.getId())
1065 && currentUser.hasRole(currentOrg.getAdminRole());
1066 }
1067
1068 private MediaPackage getMediaPackage(Event event) throws EditorServiceException {
1069 if (event == null) {
1070 errorExit("No Event provided", "", ErrorStatus.UNKNOWN);
1071 return null;
1072 }
1073 try {
1074 return index.getEventMediapackage(event);
1075 } catch (IndexServiceException e) {
1076 errorExit("Not Found", event.getIdentifier(), ErrorStatus.MEDIAPACKAGE_NOT_FOUND);
1077 return null;
1078 }
1079 }
1080
1081 private void errorExit(final String message, final String mediaPackageId, ErrorStatus status)
1082 throws EditorServiceException {
1083 errorExit(message, mediaPackageId, status, null);
1084 }
1085
1086 private void errorExit(final String message, final String mediaPackageId, ErrorStatus status, Exception e)
1087 throws EditorServiceException {
1088 String errorMessage = MessageFormat.format("{0}. Event ID: {1}", message, mediaPackageId);
1089 throw new EditorServiceException(errorMessage, status, e);
1090 }
1091
1092 @Override
1093 public void setEditData(String mediaPackageId, EditingData editingData) throws EditorServiceException,
1094 IOException {
1095 final Event event = getEvent(mediaPackageId);
1096
1097 if (WorkflowUtil.isActive(event.getWorkflowState())) {
1098 errorExit("Workflow is running", mediaPackageId, ErrorStatus.WORKFLOW_ACTIVE);
1099 }
1100
1101 MediaPackage mediaPackage = getMediaPackage(event);
1102 Smil smil = null;
1103 try {
1104 smil = createSmilCuttingCatalog(editingData, mediaPackage);
1105 } catch (Exception e) {
1106 errorExit("Unable to create SMIL cutting catalog", mediaPackageId, ErrorStatus.UNABLE_TO_CREATE_CATALOG, e);
1107 }
1108
1109 final Map<String, String> workflowProperties = new HashMap<String, String>();
1110 for (TrackData track : editingData.getTracks()) {
1111 MediaPackageElementFlavor flavor = track.getFlavor();
1112 String type = null;
1113 if (flavor != null) {
1114 type = flavor.getType();
1115 } else {
1116 Track mpTrack = mediaPackage.getTrack(track.getId());
1117 if (mpTrack != null) {
1118 type = mpTrack.getFlavor().getType();
1119 } else {
1120 errorExit("Unable to determine track type", mediaPackageId, ErrorStatus.UNKNOWN);
1121 }
1122 }
1123 workflowProperties.put("hide_" + type + "_audio", Boolean.toString(!track.getAudio().isEnabled()));
1124 workflowProperties.put("hide_" + type + "_video", Boolean.toString(!track.getVideo().isEnabled()));
1125 }
1126 WorkflowPropertiesUtil.storeProperties(assetManager, mediaPackage, workflowProperties);
1127
1128 try {
1129 mediaPackage = addSmilToArchive(mediaPackage, smil);
1130 } catch (IOException e) {
1131 errorExit("Unable to add SMIL cutting catalog to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1132 }
1133
1134 try {
1135 if (editingData.getSubtitles() != null) {
1136 mediaPackage = processSubtitleTrack(mediaPackage, editingData.getSubtitles());
1137 }
1138 } catch (IOException e) {
1139 errorExit("Unable to add subtitle track to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1140 } catch (IllegalArgumentException e) {
1141 errorExit("Illegal subtitle given", mediaPackageId, ErrorStatus.UNKNOWN, e);
1142 }
1143
1144 try {
1145 mediaPackage = addThumbnailsToArchive(editingData, mediaPackage);
1146 } catch (MimeTypeParseException e) {
1147 errorExit("Thumbnail had an illegal MimeType", mediaPackageId, ErrorStatus.UNKNOWN, e);
1148 } catch (IOException e) {
1149 errorExit("Unable to add thumbnail to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1150 }
1151
1152 try {
1153 assetManager.takeSnapshot(mediaPackage);
1154 } catch (AssetManagerException e) {
1155 logger.error("Error while adding the updated media package ({}) to the archive",
1156 mediaPackage.getIdentifier(), e);
1157 throw new IOException(e);
1158 }
1159
1160
1161 try {
1162 index.updateAllEventMetadata(mediaPackageId, editingData.getMetadataJSON(), searchIndex);
1163 } catch (SearchIndexException | IndexServiceException | IllegalArgumentException e) {
1164 errorExit("Event metadata can't be updated.", mediaPackageId, ErrorStatus.METADATA_UPDATE_FAIL, e);
1165 } catch (NotFoundException e) {
1166 errorExit("Event not found.", mediaPackageId, ErrorStatus.MEDIAPACKAGE_NOT_FOUND, e);
1167 } catch (UnauthorizedException e) {
1168 errorExit("Not authorized to update event metadata .", mediaPackageId, ErrorStatus.NOT_AUTHORIZED, e);
1169 }
1170
1171 if (editingData.getPostProcessingWorkflow() != null) {
1172 final String workflowId = editingData.getPostProcessingWorkflow();
1173 try {
1174 final Map<String, String> workflowParameters = WorkflowPropertiesUtil
1175 .getLatestWorkflowProperties(assetManager, mediaPackage.getIdentifier().toString());
1176 final Workflows workflows = new Workflows(assetManager, workflowService);
1177 workflows.applyWorkflowToLatestVersion(Collections.singletonList(mediaPackage.getIdentifier().toString()),
1178 ConfiguredWorkflow.workflow(workflowService.getWorkflowDefinitionById(workflowId), workflowParameters));
1179 } catch (AssetManagerException e) {
1180 errorExit("Unable to start workflow" + workflowId, mediaPackageId, ErrorStatus.WORKFLOW_ERROR, e);
1181 } catch (WorkflowDatabaseException e) {
1182 errorExit("Unable to load workflow" + workflowId, mediaPackageId, ErrorStatus.WORKFLOW_ERROR, e);
1183 } catch (NotFoundException e) {
1184 errorExit("Unable to load workflow" + workflowId, mediaPackageId, ErrorStatus.WORKFLOW_NOT_FOUND, e);
1185 }
1186 }
1187 }
1188
1189 @Override
1190 public String getMetadata(String mediaPackageId) throws EditorServiceException {
1191 final Event event = getEvent(mediaPackageId);
1192 MediaPackage mediaPackage = getMediaPackage(event);
1193 MetadataList metadataList = new MetadataList();
1194 List<EventCatalogUIAdapter> catalogUIAdapters = index.getEventCatalogUIAdapters();
1195 catalogUIAdapters.remove(index.getCommonEventCatalogUIAdapter());
1196 for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
1197 metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(mediaPackage));
1198 }
1199
1200 DublinCoreMetadataCollection metadataCollection = null;
1201 try {
1202 metadataCollection = EventUtils.getEventMetadata(event,
1203 index.getCommonEventCatalogUIAdapter());
1204 } catch (Exception e) {
1205 errorExit("Unable to retrieve event metadata", mediaPackageId, ErrorStatus.UNKNOWN);
1206 }
1207 metadataList.add(index.getCommonEventCatalogUIAdapter(), metadataCollection);
1208
1209 final String wfState = event.getWorkflowState();
1210 if (wfState != null && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(wfState))) {
1211 metadataList.setLocked(MetadataList.Locked.WORKFLOW_RUNNING);
1212 }
1213
1214 return MetadataJson.listToJson(metadataList, true).toString();
1215 }
1216 }