VideoGridWorkflowOperationHandler extends AbstractWorkflowOperationHandler { @Override public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context) throws WorkflowOperationException { logger.debug("Running videogrid workflow operation on workflow {}", workflowInstance.getId()); final MediaPackage mediaPackage = (MediaPackage) workflowInstance.getMediaPackage().clone(); WorkflowOperationInstance operation = workflowInstance.getCurrentOperation(); final MediaPackageElementFlavor smilFlavor = MediaPackageElementFlavor.parseFlavor( getConfig(operation, SOURCE_SMIL_FLAVOR)); final MediaPackageElementFlavor targetPresenterFlavor = MediaPackageElementFlavor.parseFlavor( getConfig(operation, TARGET_FLAVOR)); String concatEncodingProfile = StringUtils.trimToNull(operation.getConfiguration(CONCAT_ENCODING_PROFILE)); String sourceFlavorNames = operation.getConfiguration(SOURCE_FLAVORS); final List<MediaPackageElementFlavor> sourceFlavors = new ArrayList<>(); for (String flavorName : asList(sourceFlavorNames)) { sourceFlavors.add(MediaPackageElementFlavor.parseFlavor(flavorName)); } final List<Track> sourceTracks = new ArrayList<>(); for (MediaPackageElementFlavor sourceFlavor: sourceFlavors) { TrackSelector trackSelector = new TrackSelector(); trackSelector.addFlavor(sourceFlavor); sourceTracks.addAll(trackSelector.select(mediaPackage, false)); } if (sourceTracks.isEmpty()) { logger.warn("No tracks in source flavors, skipping ..."); return createResult(mediaPackage, WorkflowOperationResult.Action.SKIP); } if (concatEncodingProfile == null) throw new WorkflowOperationException("Encoding profile must be set!"); EncodingProfile profile = composerService.getProfile(concatEncodingProfile); if (profile == null) throw new WorkflowOperationException("Encoding profile '" + concatEncodingProfile + "' was not found"); ImmutablePair<Integer, Integer> resolution; try { resolution = getResolution(getConfig(workflowInstance, OPT_RESOLUTION, "1280x720")); } catch (IllegalArgumentException e) { logger.warn("Given resolution was not well formatted!"); throw new WorkflowOperationException(e); } logger.info("The resolution of the final video: {}/{}", resolution.getLeft(), resolution.getRight()); String bgColor = getConfig(workflowInstance, OPT_BACKGROUND_COLOR, "0xFFFFFF"); final Pattern pattern = Pattern.compile("0x[A-Fa-f0-9]{6}"); if (!pattern.matcher(bgColor).matches()) { logger.warn("Given color {} was not well formatted!", bgColor); throw new WorkflowOperationException("Given color was not well formatted!"); } logger.info("The background color of the final video: {}", bgColor); String targetTagsOption = StringUtils.trimToNull(operation.getConfiguration(OPT_TARGET_TAGS)); List<String> targetTags = asList(targetTagsOption); LayoutArea layoutArea = new LayoutArea("webcam", 0, 0, resolution.getLeft(), resolution.getRight(), bgColor); final SMILDocument smilDocument; try { smilDocument = SmilUtil.getSmilDocumentFromMediaPackage(mediaPackage, smilFlavor, workspace); } catch (SAXException e) { throw new WorkflowOperationException("SMIL is not well formatted", e); } catch (IOException | NotFoundException e) { throw new WorkflowOperationException("SMIL could not be found", e); } final SMILParElement parallel = (SMILParElement) smilDocument.getBody().getChildNodes().item(0); final NodeList sequences = parallel.getTimeChildren(); final float trackDurationInSeconds = parallel.getDur(); final long trackDurationInMs = Math.round(trackDurationInSeconds * 1000f); long finalStartTime = 0; long finalEndTime = trackDurationInMs; List<StartStopEvent> events = new ArrayList<>(); for (int i = 0; i < sequences.getLength(); i++) { final SMILElement item = (SMILElement) sequences.item(i); NodeList children = item.getChildNodes(); for (int j = 0; j < children.getLength(); j++) { Node node = children.item(j); SMILMediaElement e = (SMILMediaElement) node; if (NODE_TYPE_VIDEO.equals(e.getNodeName())) { Track track; try { track = getTrackByID(e.getId(), sourceTracks); } catch (IllegalStateException ex) { logger.info("No track corresponding to SMIL ID found, skipping SMIL ID {}", e.getId()); continue; } double beginInSeconds = e.getBegin().item(0).getResolvedOffset(); long beginInMs = Math.round(beginInSeconds * 1000d); double durationInSeconds = e.getDur(); long durationInMs = Math.round(durationInSeconds * 1000d); VideoInfo videoInfo = new VideoInfo(); List<Track> tmpList = new ArrayList<Track>(); tmpList.add(track); LayoutArea trackDimension = determineDimension(tmpList, true); if (trackDimension == null) { throw new WorkflowOperationException("One of the source video tracks did not contain a valid video stream or dimension"); } videoInfo.aspectRatioHeight = trackDimension.getHeight(); videoInfo.aspectRatioWidth = trackDimension.getWidth(); videoInfo.startTime = 0; logger.info("Video information: Width: {}, Height {}, StartTime: {}", videoInfo.aspectRatioWidth, videoInfo.aspectRatioHeight, videoInfo.startTime); events.add(new StartStopEvent(true, getTrackPath(track), beginInMs, videoInfo)); events.add(new StartStopEvent(false, getTrackPath(track), beginInMs + durationInMs, videoInfo)); } } } if (events.isEmpty()) { logger.warn("Could not generate sections from given SMIL catalogue for tracks in given flavor, skipping ..."); return createResult(mediaPackage, WorkflowOperationResult.Action.SKIP); } Collections.sort(events); List<EditDecisionListSection> videoEdl = new ArrayList<EditDecisionListSection>(); HashMap<String, StartStopEvent> activeVideos = new HashMap<>(); EditDecisionListSection start = new EditDecisionListSection(); start.timeStamp = finalStartTime; videoEdl.add(start); for (StartStopEvent event : events) { if (event.start) { logger.info("Add start event at {}", event.timeStamp); activeVideos.put(event.filename, event); } else { logger.info("Add stop event at {}", event); activeVideos.remove(event.filename); } videoEdl.add(createEditDecisionList(event, activeVideos)); } EditDecisionListSection endVideo = new EditDecisionListSection(); endVideo.timeStamp = finalEndTime; endVideo.nextTimeStamp = finalEndTime; videoEdl.add(endVideo); for (int i = 0; i < videoEdl.size() - 1; i++) { videoEdl.get(i).nextTimeStamp = videoEdl.get(i + 1).timeStamp; } List<List<String>> commands = new ArrayList<>(); for (EditDecisionListSection edl : videoEdl) { if (edl.nextTimeStamp - edl.timeStamp < 50) { logger.info("Skipping {}-length edl entry", edl.nextTimeStamp - edl.timeStamp); continue; } commands.add(compositeSection(layoutArea, edl)); } Job job; try { job = videoGridService.createPartialTracks(commands); } catch (VideoGridServiceException e) { throw new WorkflowOperationException(e); } if (!waitForStatus(job).isSuccess()) { throw new WorkflowOperationException(String.format("VideoGrid job for media package '%s' failed", mediaPackage)); } Gson gson = new Gson(); List<URI> uris = gson.fromJson(job.getPayload(), new TypeToken<List<URI>>() { }.getType()); List<Track> tracks = new ArrayList<>(); for (URI uri : uris) { TrackImpl track = new TrackImpl(); track.setFlavor(targetPresenterFlavor); track.setURI(uri); Job inspection = null; try { inspection = inspectionService.enrich(track, true); } catch (MediaInspectionException | MediaPackageException e) { throw new WorkflowOperationException("Inspection service could not enrich track", e); } if (!waitForStatus(inspection).isSuccess()) { throw new WorkflowOperationException(String.format("Failed to add metadata to track.")); } try { tracks.add((TrackImpl) MediaPackageElementParser.getFromXml(inspection.getPayload())); } catch (MediaPackageException e) { throw new WorkflowOperationException("Could not parse track returned by inspection service", e); } } Job concatJob = null; try { concatJob = composerService.concat(composerService.getProfile(concatEncodingProfile).getIdentifier(), new Dimension(layoutArea.width,layoutArea.height) , true, tracks.toArray(new Track[tracks.size()])); } catch (EncoderException | MediaPackageException e) { throw new WorkflowOperationException("The concat job failed", e); } if (!waitForStatus(concatJob).isSuccess()) { throw new WorkflowOperationException("The concat job did not complete successfully."); } if (concatJob.getPayload().length() > 0) { Track concatTrack; try { concatTrack = (Track) MediaPackageElementParser.getFromXml(concatJob.getPayload()); } catch (MediaPackageException e) { throw new WorkflowOperationException("Could not parse track returned by concat service", e); } concatTrack.setFlavor(targetPresenterFlavor); concatTrack.setURI(concatTrack.getURI()); for (String tag : targetTags) { concatTrack.addTag(tag); } mediaPackage.add(concatTrack); } else { throw new WorkflowOperationException("Concat operation unsuccessful, no payload returned."); } try { workspace.cleanup(mediaPackage.getIdentifier()); } catch (IOException e) { throw new WorkflowOperationException(e); } final WorkflowOperationResult result = createResult(mediaPackage, WorkflowOperationResult.Action.CONTINUE); logger.debug("Video Grid operation completed"); return result; }  void setWorkspace(Workspace workspace); void setVideoGridService(VideoGridService videoGridService); void setComposerService(ComposerService composerService); @Override WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context);  }
@Test public void testComplete() throws Exception { EasyMock.expect(instance.getConfiguration(SOURCE_FLAVOR)).andReturn(SOURCE_FLAVOR_KEY).anyTimes(); EasyMock.expect(instance.getConfiguration(SOURCE_SMIL_FLAVOR)).andReturn(SOURCE_SMIL_FLAVOR_KEY).anyTimes(); EasyMock.expect(instance.getConfiguration(ENCODING_PROFILE)).andReturn(ENCODING_PROFILE_ID).anyTimes(); EasyMock.replay(instance); WorkflowOperationResult result = handler.start(workflow, null); Assert.assertEquals(WorkflowOperationResult.Action.CONTINUE, result.getAction()); MediaPackage mpNew = result.getMediaPackage(); Track[] tracks = mpNew.getTracks(MediaPackageElementFlavor.parseFlavor(TARGET_FLAVOR_RETURN)); Assert.assertEquals(1, tracks.length); }
@Test public void testNoTracks() throws Exception { EasyMock.expect(instance.getConfiguration(SOURCE_FLAVOR)).andReturn("*/nothing").anyTimes(); EasyMock.expect(instance.getConfiguration(SOURCE_SMIL_FLAVOR)).andReturn(SOURCE_SMIL_FLAVOR_KEY).anyTimes(); EasyMock.expect(instance.getConfiguration(ENCODING_PROFILE)).andReturn(ENCODING_PROFILE_ID).anyTimes(); EasyMock.replay(instance); Assert.assertTrue(handler.start(workflow, null).allowsContinue()); }
@Test public void testNoSmil() { EasyMock.expect(instance.getConfiguration(SOURCE_FLAVOR)).andReturn(SOURCE_FLAVOR_KEY).anyTimes(); EasyMock.expect(instance.getConfiguration(SOURCE_SMIL_FLAVOR)).andReturn("smil/nothing").anyTimes(); EasyMock.expect(instance.getConfiguration(ENCODING_PROFILE)).andReturn(ENCODING_PROFILE_ID).anyTimes(); EasyMock.replay(instance); try { handler.start(workflow, null); } catch (Exception e) { return; } Assert.fail(); }
@Test public void testNoEncodingProfile() { EasyMock.expect(instance.getConfiguration(SOURCE_FLAVOR)).andReturn(SOURCE_FLAVOR_KEY).anyTimes(); EasyMock.expect(instance.getConfiguration(SOURCE_SMIL_FLAVOR)).andReturn(SOURCE_SMIL_FLAVOR_KEY).anyTimes(); EasyMock.expect(instance.getConfiguration(ENCODING_PROFILE)).andReturn("nothing").anyTimes(); EasyMock.replay(instance); try { handler.start(workflow, null); } catch (Exception e) { return; } Assert.fail(); }